Browse Source

Compound member access syntax. (#1233)

Implement initial support for `A.(B)` syntax, per #989. Specifically, this supports:

* `object.(Type.member)` for instance members,
* `object.(Interface.member)` for instance and non-instance members,
* `object.(Type.(Interface.member))` for instance members,
* `Type.(Interface.member)` for non-instance members.

Three new AST nodes are introduced:

* `CompoundFieldAccessExpression` represents the `A.(B)` syntax.
* `MemberName` is a `Value` that represents the result of evaluating an expression such as `Type.member` or `Interface.member` or `Type.(Interface.member)`.
* `TypeOfMemberName` is the type of a `MemberName` value.

In order to handle members of classes and interfaces which have corresponding declarations and may need substitution into their types, and members of structs which don't have declarations but also don't need substitution, a class `Member` is introduced that can refer to either of these kinds of member.

Co-authored-by: Geoff Romer <gromer@google.com>
Co-authored-by: Jon Meow <jperkins@google.com>
Richard Smith 4 years ago
parent
commit
235cb88a8e
28 changed files with 939 additions and 40 deletions
  1. 6 0
      common/fuzzing/carbon.proto
  2. 9 0
      common/fuzzing/proto_to_carbon.cpp
  3. 1 0
      explorer/ast/ast_rtti.txt
  4. 6 0
      explorer/ast/expression.cpp
  5. 64 0
      explorer/ast/expression.h
  6. 12 0
      explorer/fuzzing/ast_to_proto.cpp
  7. 105 18
      explorer/interpreter/interpreter.cpp
  8. 6 0
      explorer/interpreter/resolve_names.cpp
  9. 255 15
      explorer/interpreter/type_checker.cpp
  10. 4 0
      explorer/interpreter/type_checker.h
  11. 46 6
      explorer/interpreter/value.cpp
  12. 95 0
      explorer/interpreter/value.h
  13. 5 0
      explorer/syntax/parser.ypp
  14. 25 0
      explorer/testdata/class/fail_method_deduced.carbon
  15. 1 1
      explorer/testdata/class/fail_method_from_class.carbon
  16. 24 0
      explorer/testdata/class/fail_method_in_var.carbon
  17. 24 0
      explorer/testdata/class/fail_return_method.carbon
  18. 19 0
      explorer/testdata/member_access/fail_qualified_non_member.carbon
  19. 22 0
      explorer/testdata/member_access/fail_vacuous_access.carbon
  20. 25 0
      explorer/testdata/member_access/fail_vacuous_access_via_type_param.carbon
  21. 22 0
      explorer/testdata/member_access/nearly_vacuous_access_with_impl_lookup.carbon
  22. 22 0
      explorer/testdata/member_access/nearly_vacuous_access_with_instance_binding.carbon
  23. 29 0
      explorer/testdata/member_access/param_qualified_interface_member.carbon
  24. 21 0
      explorer/testdata/member_access/qualified_class_member.carbon
  25. 26 0
      explorer/testdata/member_access/qualified_interface_member.carbon
  26. 29 0
      explorer/testdata/member_access/qualified_param_member.carbon
  27. 15 0
      explorer/testdata/member_access/qualified_struct_member.carbon
  28. 21 0
      explorer/testdata/member_access/type_qualified_interface_member.carbon

+ 6 - 0
common/fuzzing/carbon.proto

@@ -28,6 +28,11 @@ message FieldAccessExpression {
   optional Expression aggregate = 2;
 }
 
+message CompoundFieldAccessExpression {
+  optional Expression object = 1;
+  optional Expression path = 2;
+}
+
 message IndexExpression {
   optional Expression aggregate = 1;
   optional Expression offset = 2;
@@ -138,6 +143,7 @@ message Expression {
     TypeTypeLiteral type_type_literal = 19;
     UnimplementedExpression unimplemented_expression = 20;
     ArrayTypeLiteral array_type_literal = 21;
+    CompoundFieldAccessExpression compound_field_access = 22;
   }
 }
 

+ 9 - 0
common/fuzzing/proto_to_carbon.cpp

@@ -214,6 +214,15 @@ static auto ExpressionToCarbon(const Fuzzing::Expression& expression,
       break;
     }
 
+    case Fuzzing::Expression::kCompoundFieldAccess: {
+      const auto& field_access = expression.compound_field_access();
+      ExpressionToCarbon(field_access.object(), out);
+      out << ".(";
+      ExpressionToCarbon(field_access.path(), out);
+      out << ")";
+      break;
+    }
+
     case Fuzzing::Expression::kIndex: {
       const auto& index = expression.index();
       ExpressionToCarbon(index.aggregate(), out);

+ 1 - 0
explorer/ast/ast_rtti.txt

@@ -41,6 +41,7 @@ abstract class Expression : AstNode;
   class CallExpression : Expression;
   class FunctionTypeLiteral : Expression;
   class FieldAccessExpression : Expression;
+  class CompoundFieldAccessExpression : Expression;
   class IndexExpression : Expression;
   class IntTypeLiteral : Expression;
   class ContinuationTypeLiteral : Expression;

+ 6 - 0
explorer/ast/expression.cpp

@@ -95,6 +95,11 @@ void Expression::Print(llvm::raw_ostream& out) const {
       out << access.aggregate() << "." << access.field();
       break;
     }
+    case ExpressionKind::CompoundFieldAccessExpression: {
+      const auto& access = cast<CompoundFieldAccessExpression>(*this);
+      out << access.object() << ".(" << access.path() << ")";
+      break;
+    }
     case ExpressionKind::TupleLiteral: {
       out << "(";
       llvm::ListSeparator sep;
@@ -233,6 +238,7 @@ void Expression::PrintID(llvm::raw_ostream& out) const {
       break;
     case ExpressionKind::IndexExpression:
     case ExpressionKind::FieldAccessExpression:
+    case ExpressionKind::CompoundFieldAccessExpression:
     case ExpressionKind::IfExpression:
     case ExpressionKind::TupleLiteral:
     case ExpressionKind::StructLiteral:

+ 64 - 0
explorer/ast/expression.h

@@ -24,6 +24,7 @@
 namespace Carbon {
 
 class Value;
+class MemberName;
 class VariableType;
 class ImplBinding;
 
@@ -181,6 +182,69 @@ class FieldAccessExpression : public Expression {
   std::optional<Nonnull<const ImplBinding*>> impl_;
 };
 
+// A compound member access expression of the form `object.(path)`.
+//
+// `path` is required to have `TypeOfMemberName` type, and describes the member
+// being accessed, which is one of:
+//
+// -   An instance member of a type: `object.(Type.member)`.
+// -   A non-instance member of an interface: `Type.(Interface.member)` or
+//     `object.(Interface.member)`.
+// -   An instance member of an interface: `object.(Interface.member)` or
+//     `object.(Type.(Interface.member))`.
+//
+// Note that the `path` is evaluated during type-checking, not at runtime, so
+// the corresponding `member` is determined statically.
+class CompoundFieldAccessExpression : public Expression {
+ public:
+  explicit CompoundFieldAccessExpression(SourceLocation source_loc,
+                                         Nonnull<Expression*> object,
+                                         Nonnull<Expression*> path)
+      : Expression(AstNodeKind::CompoundFieldAccessExpression, source_loc),
+        object_(object),
+        path_(path) {}
+
+  static auto classof(const AstNode* node) -> bool {
+    return InheritsFromCompoundFieldAccessExpression(node->kind());
+  }
+
+  auto object() const -> const Expression& { return *object_; }
+  auto object() -> Expression& { return *object_; }
+  auto path() const -> const Expression& { return *path_; }
+  auto path() -> Expression& { return *path_; }
+
+  // Returns the `MemberName` value that evaluation of the path produced.
+  // Should not be called before typechecking.
+  auto member() const -> const MemberName& {
+    CARBON_CHECK(member_.has_value());
+    return **member_;
+  }
+
+  // Can only be called once, during typechecking.
+  void set_member(Nonnull<const MemberName*> member) {
+    CARBON_CHECK(!member_.has_value());
+    member_ = member;
+  }
+
+  // Returns the expression to use to compute the witness table, if this
+  // expression names an interface member.
+  auto impl() const -> std::optional<Nonnull<const Expression*>> {
+    return impl_;
+  }
+
+  // Can only be called once, during typechecking.
+  void set_impl(Nonnull<const Expression*> impl) {
+    CARBON_CHECK(!impl_.has_value());
+    impl_ = impl;
+  }
+
+ private:
+  Nonnull<Expression*> object_;
+  Nonnull<Expression*> path_;
+  std::optional<Nonnull<const MemberName*>> member_;
+  std::optional<Nonnull<const Expression*>> impl_;
+};
+
 class IndexExpression : public Expression {
  public:
   explicit IndexExpression(SourceLocation source_loc,

+ 12 - 0
explorer/fuzzing/ast_to_proto.cpp

@@ -111,6 +111,18 @@ static auto ExpressionToProto(const Expression& expression)
       break;
     }
 
+    case ExpressionKind::CompoundFieldAccessExpression: {
+      const auto& field_access =
+          cast<CompoundFieldAccessExpression>(expression);
+      auto* field_access_proto =
+          expression_proto.mutable_compound_field_access();
+      *field_access_proto->mutable_object() =
+          ExpressionToProto(field_access.object());
+      *field_access_proto->mutable_path() =
+          ExpressionToProto(field_access.path());
+      break;
+    }
+
     case ExpressionKind::IndexExpression: {
       const auto& index = cast<IndexExpression>(expression);
       auto* index_proto = expression_proto.mutable_index();

+ 105 - 18
explorer/interpreter/interpreter.cpp

@@ -337,6 +337,22 @@ auto Interpreter::StepLvalue() -> ErrorOr<Success> {
         return todo_.FinishAction(arena_->New<LValue>(field));
       }
     }
+    case ExpressionKind::CompoundFieldAccessExpression: {
+      const auto& access = cast<CompoundFieldAccessExpression>(exp);
+      if (act.pos() == 0) {
+        return todo_.Spawn(std::make_unique<LValAction>(&access.object()));
+      } else {
+        CARBON_CHECK(!access.member().interface().has_value())
+            << "unexpected lvalue interface member";
+        CARBON_ASSIGN_OR_RETURN(
+            Nonnull<const Value*> val,
+            Convert(act.results()[0], *access.member().base_type(),
+                    exp.source_loc()));
+        Address object = cast<LValue>(*val).address();
+        Address field = object.SubobjectAddress(access.member().name());
+        return todo_.FinishAction(arena_->New<LValue>(field));
+      }
+    }
     case ExpressionKind::IndexExpression: {
       if (act.pos() == 0) {
         //    { {e[i] :: C, E, F} :: S, H}
@@ -495,7 +511,9 @@ auto Interpreter::Convert(Nonnull<const Value*> value,
     case Value::Kind::TypeOfInterfaceType:
     case Value::Kind::TypeOfChoiceType:
     case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
     case Value::Kind::StaticArrayType:
+    case Value::Kind::MemberName:
       // TODO: add `CARBON_CHECK(TypeEqual(type, value->dynamic_type()))`, once
       // we have Value::dynamic_type.
       return value;
@@ -773,30 +791,99 @@ auto Interpreter::StepExp() -> ErrorOr<Success> {
     case ExpressionKind::FieldAccessExpression: {
       const auto& access = cast<FieldAccessExpression>(exp);
       if (act.pos() == 0) {
-        //    { { e.f :: C, E, F} :: S, H}
-        // -> { { e :: [].f :: C, E, F} :: S, H}
         return todo_.Spawn(
             std::make_unique<ExpressionAction>(&access.aggregate()));
       } else {
-        //    { { v :: [].f :: C, E, F} :: S, H}
-        // -> { { v_f :: C, E, F} : S, H}
-        std::optional<Nonnull<const Witness*>> witness = std::nullopt;
-        if (access.impl().has_value()) {
+        if (const auto* member_name_type =
+                dyn_cast<TypeOfMemberName>(&access.static_type())) {
+          // The result is a member name, such as in `Type.field_name`. Form a
+          // suitable member name value.
+          CARBON_CHECK(phase() == Phase::CompileTime)
+              << "should not form MemberNames at runtime";
+          std::optional<const InterfaceType*> iface_result;
+          std::optional<const Value*> type_result;
+          if (auto* iface_type = dyn_cast<InterfaceType>(act.results()[0])) {
+            iface_result = iface_type;
+          } else {
+            type_result = act.results()[0];
+            if (access.impl().has_value()) {
+              iface_result =
+                  cast<InterfaceType>(access.impl().value()->interface());
+            }
+          }
+          MemberName* member_name = arena_->New<MemberName>(
+              type_result, iface_result, member_name_type->member());
+          return todo_.FinishAction(member_name);
+        } else {
+          // The result is the value of the named field, such as in
+          // `value.field_name`. Extract the value within the given object.
+          std::optional<Nonnull<const Witness*>> witness;
+          if (access.impl().has_value()) {
+            CARBON_ASSIGN_OR_RETURN(
+                auto witness_addr,
+                todo_.ValueOfNode(*access.impl(), access.source_loc()));
+            CARBON_ASSIGN_OR_RETURN(
+                Nonnull<const Value*> witness_value,
+                heap_.Read(llvm::cast<LValue>(witness_addr)->address(),
+                           access.source_loc()));
+            witness = cast<Witness>(witness_value);
+          }
+          FieldPath::Component field(access.field(), witness);
           CARBON_ASSIGN_OR_RETURN(
-              auto witness_addr,
-              todo_.ValueOfNode(*access.impl(), access.source_loc()));
+              Nonnull<const Value*> member,
+              act.results()[0]->GetField(arena_, FieldPath(field),
+                                         exp.source_loc()));
+          return todo_.FinishAction(member);
+        }
+      }
+    }
+    case ExpressionKind::CompoundFieldAccessExpression: {
+      const auto& access = cast<CompoundFieldAccessExpression>(exp);
+      bool forming_member_name = isa<TypeOfMemberName>(&access.static_type());
+      if (act.pos() == 0) {
+        // First, evaluate the first operand.
+        return todo_.Spawn(
+            std::make_unique<ExpressionAction>(&access.object()));
+      } else if (act.pos() == 1 && access.impl().has_value() &&
+                 !forming_member_name) {
+        // Next, if we're accessing an interface member, evaluate the `impl`
+        // expression to find the corresponding witness.
+        return todo_.Spawn(
+            std::make_unique<ExpressionAction>(access.impl().value()));
+      } else {
+        // Finally, produce the result.
+        if (forming_member_name) {
+          // If we're forming a member name, we must be in the outer evaluation
+          // in `Type.(Interface.method)`. Produce the same method name with
+          // its `type` field set.
+          CARBON_CHECK(phase() == Phase::CompileTime)
+              << "should not form MemberNames at runtime";
+          CARBON_CHECK(!access.member().base_type().has_value())
+              << "compound member access forming a member name should be "
+                 "performing impl lookup";
+          auto* member_name = arena_->New<MemberName>(
+              act.results()[0], access.member().interface(),
+              access.member().member());
+          return todo_.FinishAction(member_name);
+        } else {
+          // Access the object to find the named member.
+          Nonnull<const Value*> object = act.results()[0];
+          std::optional<Nonnull<const Witness*>> witness;
+          if (access.impl().has_value()) {
+            witness = cast<Witness>(act.results()[1]);
+          } else {
+            CARBON_CHECK(access.member().base_type().has_value())
+                << "compound access should have base type or impl";
+            CARBON_ASSIGN_OR_RETURN(
+                object, Convert(object, *access.member().base_type(),
+                                exp.source_loc()));
+          }
+          FieldPath::Component field(access.member().name(), witness);
           CARBON_ASSIGN_OR_RETURN(
-              Nonnull<const Value*> witness_value,
-              heap_.Read(llvm::cast<LValue>(witness_addr)->address(),
-                         access.source_loc()));
-          witness = cast<Witness>(witness_value);
+              Nonnull<const Value*> member,
+              object->GetField(arena_, FieldPath(field), exp.source_loc()));
+          return todo_.FinishAction(member);
         }
-        FieldPath::Component field(access.field(), witness);
-        CARBON_ASSIGN_OR_RETURN(
-            Nonnull<const Value*> member,
-            act.results()[0]->GetField(arena_, FieldPath(field),
-                                       exp.source_loc()));
-        return todo_.FinishAction(member);
       }
     }
     case ExpressionKind::IdentifierExpression: {

+ 6 - 0
explorer/interpreter/resolve_names.cpp

@@ -111,6 +111,12 @@ static auto ResolveNames(Expression& expression,
           ResolveNames(cast<FieldAccessExpression>(expression).aggregate(),
                        enclosing_scope));
       break;
+    case ExpressionKind::CompoundFieldAccessExpression: {
+      auto& access = cast<CompoundFieldAccessExpression>(expression);
+      CARBON_RETURN_IF_ERROR(ResolveNames(access.object(), enclosing_scope));
+      CARBON_RETURN_IF_ERROR(ResolveNames(access.path(), enclosing_scope));
+      break;
+    }
     case ExpressionKind::IndexExpression: {
       auto& index = cast<IndexExpression>(expression);
       CARBON_RETURN_IF_ERROR(ResolveNames(index.aggregate(), enclosing_scope));

+ 255 - 15
explorer/interpreter/type_checker.cpp

@@ -61,6 +61,57 @@ static auto ExpectPointerType(SourceLocation source_loc,
   return Success();
 }
 
+// Returns whether the value is a type whose values are themselves known to be
+// types.
+static auto IsTypeOfType(Nonnull<const Value*> value) -> bool {
+  switch (value->kind()) {
+    case Value::Kind::IntValue:
+    case Value::Kind::FunctionValue:
+    case Value::Kind::BoundMethodValue:
+    case Value::Kind::PointerValue:
+    case Value::Kind::LValue:
+    case Value::Kind::BoolValue:
+    case Value::Kind::StructValue:
+    case Value::Kind::NominalClassValue:
+    case Value::Kind::AlternativeValue:
+    case Value::Kind::BindingPlaceholderValue:
+    case Value::Kind::AlternativeConstructorValue:
+    case Value::Kind::ContinuationValue:
+    case Value::Kind::StringValue:
+    case Value::Kind::Witness:
+    case Value::Kind::ParameterizedEntityName:
+    case Value::Kind::MemberName:
+    case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
+      // These are values, not types.
+      return false;
+    case Value::Kind::IntType:
+    case Value::Kind::BoolType:
+    case Value::Kind::FunctionType:
+    case Value::Kind::PointerType:
+    case Value::Kind::StructType:
+    case Value::Kind::NominalClassType:
+    case Value::Kind::ChoiceType:
+    case Value::Kind::ContinuationType:
+    case Value::Kind::StringType:
+    case Value::Kind::StaticArrayType:
+    case Value::Kind::TupleValue:
+      // These are types whose values are not types.
+      return false;
+    case Value::Kind::AutoType:
+    case Value::Kind::VariableType:
+      // A value of one of these types could be a type, but isn't known to be.
+      return false;
+    case Value::Kind::TypeType:
+    case Value::Kind::InterfaceType:
+    case Value::Kind::TypeOfClassType:
+    case Value::Kind::TypeOfInterfaceType:
+    case Value::Kind::TypeOfChoiceType:
+      // A value of one of these types is itself always a type.
+      return true;
+  }
+}
+
 // Returns whether the value is a valid result from a type expression,
 // as opposed to a non-type value.
 static auto IsType(Nonnull<const Value*> value) -> bool {
@@ -80,6 +131,12 @@ static auto IsType(Nonnull<const Value*> value) -> bool {
     case Value::Kind::StringValue:
     case Value::Kind::Witness:
     case Value::Kind::ParameterizedEntityName:
+    case Value::Kind::MemberName:
+      return false;
+    case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
+      // Names aren't first-class values, and their types aren't first-class
+      // types.
       return false;
     case Value::Kind::IntType:
     case Value::Kind::BoolType:
@@ -96,7 +153,6 @@ static auto IsType(Nonnull<const Value*> value) -> bool {
     case Value::Kind::TypeOfClassType:
     case Value::Kind::TypeOfInterfaceType:
     case Value::Kind::TypeOfChoiceType:
-    case Value::Kind::TypeOfParameterizedEntityName:
     case Value::Kind::StaticArrayType:
     case Value::Kind::AutoType:
       return true;
@@ -141,6 +197,9 @@ static auto IsConcreteType(Nonnull<const Value*> value) -> bool {
     case Value::Kind::StringValue:
     case Value::Kind::Witness:
     case Value::Kind::ParameterizedEntityName:
+    case Value::Kind::MemberName:
+    case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
       return false;
     case Value::Kind::IntType:
     case Value::Kind::BoolType:
@@ -157,7 +216,6 @@ static auto IsConcreteType(Nonnull<const Value*> value) -> bool {
     case Value::Kind::TypeOfClassType:
     case Value::Kind::TypeOfInterfaceType:
     case Value::Kind::TypeOfChoiceType:
-    case Value::Kind::TypeOfParameterizedEntityName:
     case Value::Kind::StaticArrayType:
       return true;
     case Value::Kind::AutoType:
@@ -509,10 +567,12 @@ auto TypeChecker::ArgumentDeduction(
     case Value::Kind::TypeOfInterfaceType:
     case Value::Kind::TypeOfChoiceType:
     case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
       return handle_non_deduced_type();
     // The rest of these cases should never happen.
     case Value::Kind::Witness:
     case Value::Kind::ParameterizedEntityName:
+    case Value::Kind::MemberName:
     case Value::Kind::IntValue:
     case Value::Kind::BoolValue:
     case Value::Kind::FunctionValue:
@@ -562,6 +622,8 @@ auto TypeChecker::Substitute(
       const auto& fn_type = cast<FunctionType>(*type);
       auto param = Substitute(dict, &fn_type.parameters());
       auto ret = Substitute(dict, &fn_type.return_type());
+      // FIXME: Only remove the bindings that are in `dict`; we may still need
+      // to do deduction.
       return arena_->New<FunctionType>(param, llvm::None, ret, llvm::None,
                                        llvm::None);
     }
@@ -609,10 +671,12 @@ auto TypeChecker::Substitute(
     case Value::Kind::TypeOfInterfaceType:
     case Value::Kind::TypeOfChoiceType:
     case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
       return type;
     // The rest of these cases should never happen.
     case Value::Kind::Witness:
     case Value::Kind::ParameterizedEntityName:
+    case Value::Kind::MemberName:
     case Value::Kind::IntValue:
     case Value::Kind::BoolValue:
     case Value::Kind::FunctionValue:
@@ -760,8 +824,10 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
     }
     case ExpressionKind::TupleLiteral: {
       std::vector<Nonnull<const Value*>> arg_types;
-      for (auto& arg : cast<TupleLiteral>(*e).fields()) {
+      for (auto* arg : cast<TupleLiteral>(*e).fields()) {
         CARBON_RETURN_IF_ERROR(TypeCheckExp(arg, impl_scope));
+        CARBON_RETURN_IF_ERROR(
+            ExpectIsConcreteType(arg->source_loc(), &arg->static_type()));
         arg_types.push_back(&arg->static_type());
       }
       e->set_static_type(arena_->New<TupleValue>(std::move(arg_types)));
@@ -772,6 +838,8 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
       std::vector<NamedValue> arg_types;
       for (auto& arg : cast<StructLiteral>(*e).fields()) {
         CARBON_RETURN_IF_ERROR(TypeCheckExp(&arg.expression(), impl_scope));
+        CARBON_RETURN_IF_ERROR(ExpectIsConcreteType(
+            arg.expression().source_loc(), &arg.expression().static_type()));
         arg_types.push_back({arg.name(), &arg.expression().static_type()});
       }
       e->set_static_type(arena_->New<StructType>(std::move(arg_types)));
@@ -817,6 +885,35 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
                  << "struct " << struct_type << " does not have a field named "
                  << access.field();
         }
+        case Value::Kind::TypeType: {
+          CARBON_ASSIGN_OR_RETURN(
+              Nonnull<const Value*> type,
+              InterpExp(&access.aggregate(), arena_, trace_stream_));
+          if (const auto* struct_type = dyn_cast<StructType>(type)) {
+            for (const auto& field : struct_type->fields()) {
+              if (access.field() == field.name) {
+                access.set_static_type(
+                    arena_->New<TypeOfMemberName>(Member(&field)));
+                access.set_value_category(ValueCategory::Let);
+                return Success();
+              }
+            }
+            return CompilationError(access.source_loc())
+                   << "struct " << *struct_type
+                   << " does not have a field named " << access.field();
+          }
+          // FIXME: We should handle all types here, not only structs. For
+          // example:
+          //   fn Main() -> i32 {
+          //     class Class { var n: i32; };
+          //     let T:! Type = Class;
+          //     let x: T = {.n = 0};
+          //     return x.(T.n);
+          //   }
+          // is valid, and the type of `T` here is `Type`, not `typeof(Class)`.
+          return CompilationError(access.source_loc())
+                 << "unsupported member access into type " << *type;
+        }
         case Value::Kind::NominalClassType: {
           const auto& t_class = cast<NominalClassType>(aggregate_type);
           if (std::optional<Nonnull<const Declaration*>> member =
@@ -881,11 +978,29 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
               default:
                 break;
             }
+            access.set_static_type(
+                arena_->New<TypeOfMemberName>(Member(*member)));
+            access.set_value_category(ValueCategory::Let);
+            return Success();
+          } else {
             return CompilationError(access.source_loc())
-                   << access.field() << " is not a class function";
+                   << class_type << " does not have a member named "
+                   << access.field();
+          }
+        }
+        case Value::Kind::TypeOfInterfaceType: {
+          const InterfaceType& iface_type =
+              cast<TypeOfInterfaceType>(aggregate_type).interface_type();
+          if (std::optional<Nonnull<const Declaration*>> member = FindMember(
+                  access.field(), iface_type.declaration().members());
+              member.has_value()) {
+            access.set_static_type(
+                arena_->New<TypeOfMemberName>(Member(*member)));
+            access.set_value_category(ValueCategory::Let);
+            return Success();
           } else {
             return CompilationError(access.source_loc())
-                   << class_type << " does not have a class function named "
+                   << iface_type << " does not have a member named "
                    << access.field();
           }
         }
@@ -940,14 +1055,33 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
           if (std::optional<Nonnull<const Declaration*>> member =
                   FindMember(access.field(), iface_decl.members());
               member.has_value()) {
-            const Value& member_type = (*member)->static_type();
-            BindingMap binding_map = iface_type.args();
-            binding_map[iface_decl.self()] = &var_type;
-            Nonnull<const Value*> inst_member_type =
-                Substitute(binding_map, &member_type);
-            access.set_static_type(inst_member_type);
             CARBON_CHECK(var_type.binding().impl_binding().has_value());
             access.set_impl(*var_type.binding().impl_binding());
+
+            switch ((*member)->kind()) {
+              case DeclarationKind::FunctionDeclaration: {
+                const auto& func = cast<FunctionDeclaration>(*member);
+                if (func->is_method()) {
+                  break;
+                }
+                const Value& member_type = (*member)->static_type();
+                BindingMap binding_map = iface_type.args();
+                binding_map[iface_decl.self()] = &var_type;
+                Nonnull<const Value*> inst_member_type =
+                    Substitute(binding_map, &member_type);
+                access.set_static_type(inst_member_type);
+                return Success();
+              }
+              default:
+                break;
+            }
+            // FIXME: Consider setting the static type of all interface member
+            // declarations and instance member declarations to be member name
+            // types, rather than special-casing member accesses that name
+            // them.
+            access.set_static_type(
+                arena_->New<TypeOfMemberName>(Member(*member)));
+            access.set_value_category(ValueCategory::Let);
             return Success();
           } else {
             return CompilationError(e->source_loc())
@@ -962,6 +1096,106 @@ auto TypeChecker::TypeCheckExp(Nonnull<Expression*> e,
                  << *e;
       }
     }
+    case ExpressionKind::CompoundFieldAccessExpression: {
+      auto& access = cast<CompoundFieldAccessExpression>(*e);
+      CARBON_RETURN_IF_ERROR(TypeCheckExp(&access.object(), impl_scope));
+      CARBON_RETURN_IF_ERROR(TypeCheckExp(&access.path(), impl_scope));
+      if (!isa<TypeOfMemberName>(access.path().static_type())) {
+        return CompilationError(e->source_loc())
+               << "expected name of instance member or interface member in "
+                  "compound member access, found "
+               << access.path().static_type();
+      }
+
+      // Evaluate the member name expression to determine which member we're
+      // accessing.
+      CARBON_ASSIGN_OR_RETURN(Nonnull<const Value*> member_name_value,
+                              InterpExp(&access.path(), arena_, trace_stream_));
+      const auto& member_name = cast<MemberName>(*member_name_value);
+      access.set_member(&member_name);
+
+      bool has_instance = true;
+      std::optional<Nonnull<const Value*>> base_type = member_name.base_type();
+      if (!base_type.has_value()) {
+        if (IsTypeOfType(&access.object().static_type())) {
+          // This is `Type.(member_name)`, where `member_name` doesn't specify
+          // a type. This access doesn't perform instance binding.
+          CARBON_ASSIGN_OR_RETURN(
+              base_type, InterpExp(&access.object(), arena_, trace_stream_));
+          has_instance = false;
+        } else {
+          // This is `value.(member_name)`, where `member_name` doesn't specify
+          // a type. The member will be found in the type of `value`, or in a
+          // corresponding `impl` if `member_name` is an interface member.
+          base_type = &access.object().static_type();
+        }
+      } else {
+        // This is `value.(member_name)`, where `member_name` specifies a type.
+        // `value` is implicitly converted to that type.
+        CARBON_RETURN_IF_ERROR(ExpectType(e->source_loc(),
+                                          "compound member access", *base_type,
+                                          &access.object().static_type()));
+      }
+
+      // Perform impl selection if necessary.
+      if (std::optional<Nonnull<const Value*>> iface =
+              member_name.interface()) {
+        CARBON_ASSIGN_OR_RETURN(
+            Nonnull<Expression*> impl,
+            impl_scope.Resolve(*iface, *base_type, e->source_loc(), *this));
+        access.set_impl(impl);
+      }
+
+      auto SubstituteIntoMemberType = [&]() {
+        Nonnull<const Value*> member_type = &member_name.member().type();
+        if (member_name.interface()) {
+          Nonnull<const InterfaceType*> iface_type = *member_name.interface();
+          BindingMap binding_map = iface_type->args();
+          binding_map[iface_type->declaration().self()] = *base_type;
+          return Substitute(binding_map, member_type);
+        }
+        if (auto* class_type = dyn_cast<NominalClassType>(base_type.value())) {
+          return Substitute(class_type->type_args(), member_type);
+        }
+        return member_type;
+      };
+
+      switch (std::optional<Nonnull<const Declaration*>> decl =
+                  member_name.member().declaration();
+              decl ? decl.value()->kind()
+                   : DeclarationKind::VariableDeclaration) {
+        case DeclarationKind::VariableDeclaration:
+          if (has_instance) {
+            access.set_static_type(SubstituteIntoMemberType());
+            access.set_value_category(access.object().value_category());
+            return Success();
+          }
+          break;
+        case DeclarationKind::FunctionDeclaration: {
+          bool is_method = cast<FunctionDeclaration>(*decl.value()).is_method();
+          if (has_instance || !is_method) {
+            // This should not be possible: the name of a static member
+            // function should have function type not member name type.
+            CARBON_CHECK(!has_instance || is_method ||
+                         !member_name.base_type().has_value())
+                << "vacuous compound member access";
+            access.set_static_type(SubstituteIntoMemberType());
+            access.set_value_category(ValueCategory::Let);
+            return Success();
+          }
+          break;
+        }
+        default:
+          CARBON_FATAL() << "member " << member_name
+                         << " is not a field or method";
+          break;
+      }
+
+      access.set_static_type(
+          arena_->New<TypeOfMemberName>(member_name.member()));
+      access.set_value_category(ValueCategory::Let);
+      return Success();
+    }
     case ExpressionKind::IdentifierExpression: {
       auto& ident = cast<IdentifierExpression>(*e);
       if (ident.value_node().base().kind() ==
@@ -1367,14 +1601,20 @@ void TypeChecker::BringImplsIntoScope(
   }
 }
 
-void TypeChecker::BringImplIntoScope(Nonnull<const ImplBinding*> impl_binding,
-                                     ImplScope& impl_scope) {
-  CARBON_CHECK(impl_binding->type_var()->symbolic_identity().has_value());
+auto TypeChecker::CreateImplReference(Nonnull<const ImplBinding*> impl_binding)
+    -> Nonnull<Expression*> {
   auto impl_id =
       arena_->New<IdentifierExpression>(impl_binding->source_loc(), "impl");
   impl_id->set_value_node(impl_binding);
+  return impl_id;
+}
+
+void TypeChecker::BringImplIntoScope(Nonnull<const ImplBinding*> impl_binding,
+                                     ImplScope& impl_scope) {
+  CARBON_CHECK(impl_binding->type_var()->symbolic_identity().has_value());
   impl_scope.Add(impl_binding->interface(),
-                 *impl_binding->type_var()->symbolic_identity(), impl_id);
+                 *impl_binding->type_var()->symbolic_identity(),
+                 CreateImplReference(impl_binding));
 }
 
 auto TypeChecker::TypeCheckPattern(

+ 4 - 0
explorer/interpreter/type_checker.h

@@ -122,6 +122,10 @@ class TypeChecker {
   void BringPatternImplsIntoScope(Nonnull<const Pattern*> p,
                                   ImplScope& impl_scope);
 
+  // Create a reference to the given `impl` binding.
+  auto CreateImplReference(Nonnull<const ImplBinding*> impl_binding)
+      -> Nonnull<Expression*>;
+
   // Add the given ImplBinding to the given `impl_scope`.
   void BringImplIntoScope(Nonnull<const ImplBinding*> impl_binding,
                           ImplScope& impl_scope);

+ 46 - 6
explorer/interpreter/value.cpp

@@ -354,6 +354,25 @@ void Value::Print(llvm::raw_ostream& out) const {
     case Value::Kind::ParameterizedEntityName:
       out << *GetName(cast<ParameterizedEntityName>(*this).declaration());
       break;
+    case Value::Kind::MemberName: {
+      const auto& member_name = cast<MemberName>(*this);
+      if (member_name.base_type().has_value()) {
+        out << *member_name.base_type().value();
+      }
+      if (member_name.base_type().has_value() &&
+          member_name.interface().has_value()) {
+        out << "(";
+      }
+      if (member_name.interface().has_value()) {
+        out << *member_name.interface().value();
+      }
+      out << "." << member_name.name();
+      if (member_name.base_type().has_value() &&
+          member_name.interface().has_value()) {
+        out << ")";
+      }
+      break;
+    }
     case Value::Kind::ChoiceType:
       out << "choice " << cast<ChoiceType>(*this).name();
       break;
@@ -388,9 +407,13 @@ void Value::Print(llvm::raw_ostream& out) const {
           << ")";
       break;
     case Value::Kind::TypeOfParameterizedEntityName:
-      out << "typeof(" << cast<TypeOfParameterizedEntityName>(*this).name()
-          << ")";
+      out << "parameterized entity name "
+          << cast<TypeOfParameterizedEntityName>(*this).name();
+      break;
+    case Value::Kind::TypeOfMemberName: {
+      out << "member name " << cast<TypeOfMemberName>(*this).member().name();
       break;
+    }
     case Value::Kind::StaticArrayType: {
       const auto& array_type = cast<StaticArrayType>(*this);
       out << "[" << array_type.element_type() << "; " << array_type.size()
@@ -522,10 +545,6 @@ auto TypeEqual(Nonnull<const Value*> t1, Nonnull<const Value*> t2) -> bool {
     case Value::Kind::TypeOfChoiceType:
       return TypeEqual(&cast<TypeOfChoiceType>(*t1).choice_type(),
                        &cast<TypeOfChoiceType>(*t2).choice_type());
-    case Value::Kind::TypeOfParameterizedEntityName: {
-      return ValueEqual(&cast<TypeOfParameterizedEntityName>(*t1).name(),
-                        &cast<TypeOfParameterizedEntityName>(*t2).name());
-    }
     case Value::Kind::StaticArrayType: {
       const auto& array1 = cast<StaticArrayType>(*t1);
       const auto& array2 = cast<StaticArrayType>(*t2);
@@ -546,6 +565,9 @@ auto TypeEqual(Nonnull<const Value*> t1, Nonnull<const Value*> t2) -> bool {
     case Value::Kind::BindingPlaceholderValue:
     case Value::Kind::ContinuationValue:
     case Value::Kind::ParameterizedEntityName:
+    case Value::Kind::MemberName:
+    case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
       CARBON_FATAL() << "TypeEqual used to compare non-type values\n"
                      << *t1 << "\n"
                      << *t2;
@@ -645,6 +667,7 @@ auto ValueEqual(Nonnull<const Value*> v1, Nonnull<const Value*> v2) -> bool {
     case Value::Kind::TypeOfInterfaceType:
     case Value::Kind::TypeOfChoiceType:
     case Value::Kind::TypeOfParameterizedEntityName:
+    case Value::Kind::TypeOfMemberName:
     case Value::Kind::StaticArrayType:
       return TypeEqual(v1, v2);
     case Value::Kind::NominalClassValue:
@@ -654,6 +677,7 @@ auto ValueEqual(Nonnull<const Value*> v1, Nonnull<const Value*> v2) -> bool {
     case Value::Kind::ContinuationValue:
     case Value::Kind::PointerValue:
     case Value::Kind::LValue:
+    case Value::Kind::MemberName:
       // TODO: support pointer comparisons once we have a clearer distinction
       // between pointers and lvalues.
       CARBON_FATAL() << "ValueEqual does not support this kind of value: "
@@ -703,6 +727,22 @@ auto FindMember(const std::string& name,
   return std::nullopt;
 }
 
+auto Member::name() const -> std::string {
+  if (const Declaration* decl = member_.dyn_cast<const Declaration*>()) {
+    return GetName(*decl).value();
+  } else {
+    return member_.get<const NamedValue*>()->name;
+  }
+}
+
+auto Member::type() const -> const Value& {
+  if (const Declaration* decl = member_.dyn_cast<const Declaration*>()) {
+    return decl->static_type();
+  } else {
+    return *member_.get<const NamedValue*>()->value;
+  }
+}
+
 void ImplBinding::Print(llvm::raw_ostream& out) const {
   out << "impl binding " << *type_var_ << " as " << *iface_;
 }

+ 95 - 0
explorer/interpreter/value.h

@@ -17,6 +17,7 @@
 #include "explorer/interpreter/address.h"
 #include "explorer/interpreter/field_path.h"
 #include "explorer/interpreter/stack.h"
+#include "llvm/ADT/PointerUnion.h"
 #include "llvm/Support/Compiler.h"
 
 namespace Carbon {
@@ -58,6 +59,7 @@ class Value {
     ContinuationType,  // The type of a continuation.
     VariableType,      // e.g., generic type parameters.
     ParameterizedEntityName,
+    MemberName,
     BindingPlaceholderValue,
     AlternativeConstructorValue,
     ContinuationValue,  // A first-class continuation value.
@@ -67,6 +69,7 @@ class Value {
     TypeOfInterfaceType,
     TypeOfChoiceType,
     TypeOfParameterizedEntityName,
+    TypeOfMemberName,
     StaticArrayType,
   };
 
@@ -757,6 +760,73 @@ class ParameterizedEntityName : public Value {
   Nonnull<const TuplePattern*> params_;
 };
 
+// A member of a type.
+//
+// This is either a declared member of a class, interface, or similar, or a
+// member of a struct with no declaration.
+class Member {
+ public:
+  explicit Member(const Declaration* declaration) : member_(declaration) {}
+  explicit Member(const NamedValue* struct_member) : member_(struct_member) {}
+
+  // The name of the member.
+  auto name() const -> std::string;
+  // The declared type of the member, which might include type variables.
+  auto type() const -> const Value&;
+  // A declaration of the member, if any exists.
+  auto declaration() const -> std::optional<Nonnull<const Declaration*>> {
+    if (const Declaration* decl = member_.dyn_cast<const Declaration*>()) {
+      return decl;
+    }
+    return std::nullopt;
+  }
+
+ private:
+  llvm::PointerUnion<Nonnull<const Declaration*>, Nonnull<const NamedValue*>>
+      member_;
+};
+
+// The name of a member of a class or interface.
+//
+// These values are used to represent the second operand of a compound member
+// access expression: `x.(A.B)`, and can also be the value of an alias
+// declaration, but cannot be used in most other contexts.
+class MemberName : public Value {
+ public:
+  MemberName(std::optional<Nonnull<const Value*>> base_type,
+             std::optional<Nonnull<const InterfaceType*>> interface,
+             Member member)
+      : Value(Kind::MemberName),
+        base_type_(base_type),
+        interface_(interface),
+        member_(member) {
+    CARBON_CHECK(base_type || interface)
+        << "member name must be in a type, an interface, or both";
+  }
+
+  static auto classof(const Value* value) -> bool {
+    return value->kind() == Kind::MemberName;
+  }
+
+  // The type for which `name` is a member or a member of an `impl`.
+  auto base_type() const -> std::optional<Nonnull<const Value*>> {
+    return base_type_;
+  }
+  // The interface for which `name` is a member, if any.
+  auto interface() const -> std::optional<Nonnull<const InterfaceType*>> {
+    return interface_;
+  }
+  // The member.
+  auto member() const -> Member { return member_; }
+  // The name of the member.
+  auto name() const -> std::string { return member().name(); }
+
+ private:
+  std::optional<Nonnull<const Value*>> base_type_;
+  std::optional<Nonnull<const InterfaceType*>> interface_;
+  Member member_;
+};
+
 // A first-class continuation representation of a fragment of the stack.
 // A continuation value behaves like a pointer to the underlying stack
 // fragment, which is exposed by `Stack()`.
@@ -909,6 +979,31 @@ class TypeOfParameterizedEntityName : public Value {
   Nonnull<const ParameterizedEntityName*> name_;
 };
 
+// The type of a member name expression.
+//
+// This is used for member names that don't denote a specific object or value
+// until used on the right-hand side of a `.`, such as an instance method or
+// field name, or any member function in an interface.
+//
+// Such expressions can appear only as the target of an `alias` declaration or
+// as the member name in a compound member access.
+class TypeOfMemberName : public Value {
+ public:
+  explicit TypeOfMemberName(Member member)
+      : Value(Kind::TypeOfMemberName), member_(member) {}
+
+  static auto classof(const Value* value) -> bool {
+    return value->kind() == Kind::TypeOfMemberName;
+  }
+
+  // TODO: consider removing this or moving it elsewhere in the AST,
+  // since it's arguably part of the expression value rather than its type.
+  auto member() const -> Member { return member_; }
+
+ private:
+  Member member_;
+};
+
 // The type of a statically-sized array.
 //
 // Note that values of this type are represented as tuples.

+ 5 - 0
explorer/syntax/parser.ypp

@@ -326,6 +326,11 @@ postfix_expression:
   primary_expression
 | postfix_expression designator
     { $$ = arena->New<FieldAccessExpression>(context.source_loc(), $1, $2); }
+| postfix_expression PERIOD LEFT_PARENTHESIS expression RIGHT_PARENTHESIS
+    {
+      $$ = arena->New<CompoundFieldAccessExpression>(context.source_loc(), $1,
+                                                     $4);
+    }
 | postfix_expression LEFT_SQUARE_BRACKET expression RIGHT_SQUARE_BRACKET
     { $$ = arena->New<IndexExpression>(context.source_loc(), $1, $3); }
 | intrinsic_identifier tuple

+ 25 - 0
explorer/testdata/class/fail_method_deduced.carbon

@@ -0,0 +1,25 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{not} %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+
+package ExplorerTest api;
+
+class C {
+  fn F() {}
+  fn G[me: Self]() {}
+}
+
+fn H[T:! Type](x: T) {}
+
+fn Main() -> i32 {
+  H(C.F);
+  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_method_deduced.carbon:[[@LINE+1]]: Expected a type, but got member name G
+  H(C.G);
+  return 0;
+}

+ 1 - 1
explorer/testdata/class/fail_method_from_class.carbon

@@ -24,6 +24,6 @@ class Point {
 }
 
 fn Main() -> i32 {
-  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_method_from_class.carbon:[[@LINE+1]]: GetX is not a class function
+  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_method_from_class.carbon:[[@LINE+1]]: in call `Point.GetX()`, expected callee to be a function, found `member name GetX`
   return Point.GetX();
 }

+ 24 - 0
explorer/testdata/class/fail_method_in_var.carbon

@@ -0,0 +1,24 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{not} %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+
+package ExplorerTest api;
+
+class C {
+  fn F() {}
+  fn G[me: Self]() {}
+}
+
+fn Main() -> i32 {
+  var f: auto = C.F;
+  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_method_in_var.carbon:[[@LINE+1]]: Expected a type, but got member name G
+  var g: auto = C.G;
+
+  return 0;
+}

+ 24 - 0
explorer/testdata/class/fail_return_method.carbon

@@ -0,0 +1,24 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{not} %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+
+package ExplorerTest api;
+
+class C {
+  fn F() {}
+  fn G[me: Self]() {}
+}
+
+fn ReturnF() -> auto { return C.F; }
+// CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/class/fail_return_method.carbon:[[@LINE+1]]: Expected a type, but got member name G
+fn ReturnG() -> auto { return C.G; }
+
+fn Main() -> i32 {
+  return 0;
+}

+ 19 - 0
explorer/testdata/member_access/fail_qualified_non_member.carbon

@@ -0,0 +1,19 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{not} %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+
+package Foo api;
+fn F[me: i32]() {}
+fn Main() -> i32 {
+  // TODO: It's unclear whether this is valid per the current rules. See
+  // https://github.com/carbon-language/carbon-lang/pull/1122
+  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/member_access/fail_qualified_non_member.carbon:[[@LINE+1]]: expected name of instance member or interface member in compound member access, found fn () -> ()
+  42.(F)();
+  return 0;
+}

+ 22 - 0
explorer/testdata/member_access/fail_vacuous_access.carbon

@@ -0,0 +1,22 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{not} %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+
+package Foo api;
+interface A { fn F() -> i32; }
+class X {
+  impl as A {
+    fn F() -> i32 { return 1; }
+  }
+}
+fn Main() -> i32 {
+  var a: X = {};
+  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/member_access/fail_vacuous_access.carbon:[[@LINE+1]]: expected name of instance member or interface member in compound member access, found fn () -> i32
+  return a.(X.(A.F))();
+}

+ 25 - 0
explorer/testdata/member_access/fail_vacuous_access_via_type_param.carbon

@@ -0,0 +1,25 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{not} %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{not} %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+
+package Foo api;
+interface A { fn F() -> i32; }
+class X {
+  impl as A {
+    fn F() -> i32 { return 1; }
+  }
+}
+fn F[T:! A](a: T) -> i32 {
+  // CHECK: COMPILATION ERROR: {{.*}}/explorer/testdata/member_access/fail_vacuous_access_via_type_param.carbon:[[@LINE+1]]: expected name of instance member or interface member in compound member access, found fn () -> i32
+  return a.(T.F)();
+}
+fn Main() -> i32 {
+  var a: X = {};
+  return F(a);
+}

+ 22 - 0
explorer/testdata/member_access/nearly_vacuous_access_with_impl_lookup.carbon

@@ -0,0 +1,22 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 1
+
+package Foo api;
+interface A { fn F() -> i32; }
+class X {
+  impl as A {
+    fn F() -> i32 { return 1; }
+  }
+}
+fn Main() -> i32 {
+  var a: X = {};
+  return a.(A.F)();
+}

+ 22 - 0
explorer/testdata/member_access/nearly_vacuous_access_with_instance_binding.carbon

@@ -0,0 +1,22 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 1
+
+package Foo api;
+interface A { fn F[me: Self]() -> i32; }
+class X {
+  impl as A {
+    fn F[me: Self]() -> i32 { return 1; }
+  }
+}
+fn Main() -> i32 {
+  var a: X = {};
+  return a.(X.(A.F))();
+}

+ 29 - 0
explorer/testdata/member_access/param_qualified_interface_member.carbon

@@ -0,0 +1,29 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 3
+
+package Foo api;
+interface A {
+  fn F[me: Self](o: Self) -> Self;
+}
+class X {
+  impl as A {
+    fn F[me: Self](o: Self) -> Self { return {.n = me.n + o.n}; }
+  }
+  var n: i32;
+}
+fn F[T:! A](v: T, w: T) -> T {
+  return v.(T.(A.F))(w);
+}
+fn Main() -> i32 {
+  var v: X = {.n = 1};
+  var w: X = {.n = 2};
+  return F(v, w).n;
+}

+ 21 - 0
explorer/testdata/member_access/qualified_class_member.carbon

@@ -0,0 +1,21 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 3
+
+package Foo api;
+class X {
+  fn F[me: Self](o: Self) -> Self { return {.n = me.n + o.n}; }
+  var n: i32;
+}
+fn Main() -> i32 {
+  var v: X = {.n = 1};
+  var w: X = {.n = 2};
+  return v.(X.F)(w).(X.n);
+}

+ 26 - 0
explorer/testdata/member_access/qualified_interface_member.carbon

@@ -0,0 +1,26 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 3
+
+package Foo api;
+interface A {
+  fn F[me: Self](o: Self) -> Self;
+}
+class X {
+  impl as A {
+    fn F[me: Self](o: Self) -> Self { return {.n = me.n + o.n}; }
+  }
+  var n: i32;
+}
+fn Main() -> i32 {
+  var v: X = {.n = 1};
+  var w: X = {.n = 2};
+  return v.(A.F)(w).n;
+}

+ 29 - 0
explorer/testdata/member_access/qualified_param_member.carbon

@@ -0,0 +1,29 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 3
+
+package Foo api;
+interface A {
+  fn F[me: Self](o: Self) -> Self;
+}
+class X {
+  impl as A {
+    fn F[me: Self](o: Self) -> Self { return {.n = me.n + o.n}; }
+  }
+  var n: i32;
+}
+fn F[T:! A](v: T, w: T) -> T {
+  return v.(T.F)(w);
+}
+fn Main() -> i32 {
+  var v: X = {.n = 1};
+  var w: X = {.n = 2};
+  return F(v, w).n;
+}

+ 15 - 0
explorer/testdata/member_access/qualified_struct_member.carbon

@@ -0,0 +1,15 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 2
+
+package Foo api;
+fn Main() -> i32 {
+  return {.m = 1, .n = 2}.({.n: i32, .m: i32}.n);
+}

+ 21 - 0
explorer/testdata/member_access/type_qualified_interface_member.carbon

@@ -0,0 +1,21 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// RUN: %{explorer} %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes=false %s
+// RUN: %{explorer} --parser_debug --trace_file=- %s 2>&1 | \
+// RUN:   %{FileCheck} --match-full-lines --allow-unused-prefixes %s
+// AUTOUPDATE: %{explorer} %s
+// CHECK: result: 42
+
+package Foo api;
+interface HasDefault {
+  fn Default() -> Self;
+}
+impl i32 as HasDefault {
+  fn Default() -> i32 { return 42; }
+}
+fn Main() -> i32 {
+  return i32.(HasDefault.Default)();
+}