Explorar o código

Parse and check support for compound member access. (#3790)

On the parsing side, we treat `a.(b)` as a member access whose second
operand is a `ParenExpr` rather than a `MemberName`. A new node category
is added for the union of `MemberName` and `ParenExpr` to support this.

Checking is mostly reusing the same pieces we already have for simple
member access. Compound member access is in most ways a simplified form
of simple member access because it doesn't need to do any lookup.
Richard Smith %!s(int64=2) %!d(string=hai) anos
pai
achega
3884d3c27e

+ 13 - 4
toolchain/check/handle_name.cpp

@@ -5,6 +5,7 @@
 #include "toolchain/check/context.h"
 #include "toolchain/check/member_access.h"
 #include "toolchain/lex/token_kind.h"
+#include "toolchain/parse/typed_nodes.h"
 #include "toolchain/sem_ir/inst.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
@@ -12,10 +13,18 @@ namespace Carbon::Check {
 
 auto HandleMemberAccessExpr(Context& context, Parse::MemberAccessExprId node_id)
     -> bool {
-  SemIR::NameId name_id = context.node_stack().PopName();
-  auto base_id = context.node_stack().PopExpr();
-  auto member_id = PerformMemberAccess(context, node_id, base_id, name_id);
-  context.node_stack().Push(node_id, member_id);
+  if (context.node_stack().PeekIs<Parse::NodeKind::ParenExpr>()) {
+    auto member_expr_id = context.node_stack().PopExpr();
+    auto base_id = context.node_stack().PopExpr();
+    auto member_id =
+        PerformCompoundMemberAccess(context, node_id, base_id, member_expr_id);
+    context.node_stack().Push(node_id, member_id);
+  } else {
+    SemIR::NameId name_id = context.node_stack().PopName();
+    auto base_id = context.node_stack().PopExpr();
+    auto member_id = PerformMemberAccess(context, node_id, base_id, name_id);
+    context.node_stack().Push(node_id, member_id);
+  }
   return true;
 }
 

+ 42 - 0
toolchain/check/member_access.cpp

@@ -269,6 +269,10 @@ static auto PerformInstanceBinding(Context& context,
       context.GetBuiltinType(SemIR::BuiltinKind::FunctionType)) {
     // Find the named function and check whether it's an instance method.
     auto function_name_id = context.constant_values().Get(member_id);
+    if (function_name_id == SemIR::ConstantId::Error) {
+      return SemIR::InstId::BuiltinError;
+    }
+
     CARBON_CHECK(function_name_id.is_constant())
         << "Non-constant value " << context.insts().Get(member_id)
         << " of function type";
@@ -366,4 +370,42 @@ auto PerformMemberAccess(Context& context, Parse::MemberAccessExprId node_id,
   return member_id;
 }
 
+auto PerformCompoundMemberAccess(Context& context,
+                                 Parse::MemberAccessExprId node_id,
+                                 SemIR::InstId base_id,
+                                 SemIR::InstId member_expr_id)
+    -> SemIR::InstId {
+  // Materialize a temporary for the base expression if necessary.
+  base_id = ConvertToValueOrRefExpr(context, base_id);
+  auto base_type_id = context.insts().Get(base_id).type_id();
+  auto base_type_const_id = context.types().GetConstantId(base_type_id);
+
+  auto member_id = member_expr_id;
+  auto member = context.insts().Get(member_id);
+
+  // If the member expression names an associated entity, impl lookup is always
+  // performed using the type of the base expression.
+  if (auto assoc_type = context.types().TryGetAs<SemIR::AssociatedEntityType>(
+          member.type_id())) {
+    member_id =
+        PerformImplLookup(context, base_type_const_id, *assoc_type, member_id);
+  }
+
+  // Perform instance binding if we found an instance member.
+  member_id = PerformInstanceBinding(context, node_id, base_id, member_id);
+
+  // If we didn't perform impl lookup or instance binding, that's an error
+  // because the base expression is not used for anything.
+  if (member_id == member_expr_id) {
+    CARBON_DIAGNOSTIC(CompoundMemberAccessDoesNotUseBase, Error,
+                      "Member name of type `{0}` in compound member access is "
+                      "not an instance member or an interface member.",
+                      SemIR::TypeId);
+    context.emitter().Emit(node_id, CompoundMemberAccessDoesNotUseBase,
+                           member.type_id());
+  }
+
+  return member_id;
+}
+
 }  // namespace Carbon::Check

+ 8 - 0
toolchain/check/member_access.h

@@ -16,6 +16,14 @@ auto PerformMemberAccess(Context& context, Parse::MemberAccessExprId node_id,
                          SemIR::InstId base_id, SemIR::NameId name_id)
     -> SemIR::InstId;
 
+// Creates SemIR to perform a compound member access with base expression
+// `base_id` and member name expression `member_expr_id`. Returns the result of
+// the access.
+auto PerformCompoundMemberAccess(Context& context,
+                                 Parse::MemberAccessExprId node_id,
+                                 SemIR::InstId base_id,
+                                 SemIR::InstId member_expr_id) -> SemIR::InstId;
+
 }  // namespace Carbon::Check
 
 #endif  // CARBON_TOOLCHAIN_CHECK_MEMBER_ACCESS_H_

+ 114 - 0
toolchain/check/testdata/class/compound_field.carbon

@@ -0,0 +1,114 @@
+// 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
+//
+// AUTOUPDATE
+
+base class Base {
+  var a: i32;
+  var b: i32;
+  var c: i32;
+}
+
+class Derived {
+  extend base: Base;
+
+  var d: i32;
+  var e: i32;
+}
+
+fn AccessDerived(d: Derived) -> i32 {
+  return d.(Derived.d);
+}
+
+fn AccessBase(d: Derived) -> i32 {
+  return d.(Base.b);
+}
+
+// CHECK:STDOUT: --- compound_field.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %Base: type = class_type @Base [template]
+// CHECK:STDOUT:   %.1: type = unbound_element_type Base, i32 [template]
+// CHECK:STDOUT:   %.2: type = struct_type {.a: i32, .b: i32, .c: i32} [template]
+// CHECK:STDOUT:   %Derived: type = class_type @Derived [template]
+// CHECK:STDOUT:   %.3: type = ptr_type {.a: i32, .b: i32, .c: i32} [template]
+// CHECK:STDOUT:   %.4: type = unbound_element_type Derived, Base [template]
+// CHECK:STDOUT:   %.5: type = unbound_element_type Derived, i32 [template]
+// CHECK:STDOUT:   %.6: type = struct_type {.base: Base, .d: i32, .e: i32} [template]
+// CHECK:STDOUT:   %.7: type = struct_type {.base: {.a: i32, .b: i32, .c: i32}*, .d: i32, .e: i32} [template]
+// CHECK:STDOUT:   %.8: type = ptr_type {.base: {.a: i32, .b: i32, .c: i32}*, .d: i32, .e: i32} [template]
+// CHECK:STDOUT:   %.9: type = ptr_type {.base: Base, .d: i32, .e: i32} [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [template] {
+// CHECK:STDOUT:     .Base = %Base.decl
+// CHECK:STDOUT:     .Derived = %Derived.decl
+// CHECK:STDOUT:     .AccessDerived = %AccessDerived
+// CHECK:STDOUT:     .AccessBase = %AccessBase
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %Base.decl: type = class_decl @Base [template = constants.%Base] {}
+// CHECK:STDOUT:   %Derived.decl: type = class_decl @Derived [template = constants.%Derived] {}
+// CHECK:STDOUT:   %AccessDerived: <function> = fn_decl @AccessDerived [template] {
+// CHECK:STDOUT:     %Derived.ref.loc20: type = name_ref Derived, %Derived.decl [template = constants.%Derived]
+// CHECK:STDOUT:     %d.loc20_18.1: Derived = param d
+// CHECK:STDOUT:     @AccessDerived.%d: Derived = bind_name d, %d.loc20_18.1
+// CHECK:STDOUT:     %return.var.loc20: ref i32 = var <return slot>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %AccessBase: <function> = fn_decl @AccessBase [template] {
+// CHECK:STDOUT:     %Derived.ref.loc24: type = name_ref Derived, %Derived.decl [template = constants.%Derived]
+// CHECK:STDOUT:     %d.loc24_15.1: Derived = param d
+// CHECK:STDOUT:     @AccessBase.%d: Derived = bind_name d, %d.loc24_15.1
+// CHECK:STDOUT:     %return.var.loc24: ref i32 = var <return slot>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Base {
+// CHECK:STDOUT:   %.loc8: <unbound element of class Base> = field_decl a, element0 [template]
+// CHECK:STDOUT:   %.loc9: <unbound element of class Base> = field_decl b, element1 [template]
+// CHECK:STDOUT:   %.loc10: <unbound element of class Base> = field_decl c, element2 [template]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%Base
+// CHECK:STDOUT:   .a = %.loc8
+// CHECK:STDOUT:   .b = %.loc9
+// CHECK:STDOUT:   .c = %.loc10
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Derived {
+// CHECK:STDOUT:   %Base.ref: type = name_ref Base, file.%Base.decl [template = constants.%Base]
+// CHECK:STDOUT:   %.loc14: <unbound element of class Derived> = base_decl Base, element0 [template]
+// CHECK:STDOUT:   %.loc16: <unbound element of class Derived> = field_decl d, element1 [template]
+// CHECK:STDOUT:   %.loc17: <unbound element of class Derived> = field_decl e, element2 [template]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%Derived
+// CHECK:STDOUT:   .base = %.loc14
+// CHECK:STDOUT:   .d = %.loc16
+// CHECK:STDOUT:   .e = %.loc17
+// CHECK:STDOUT:   extend name_scope1
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @AccessDerived(%d: Derived) -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %d.ref.loc21_10: Derived = name_ref d, %d
+// CHECK:STDOUT:   %Derived.ref: type = name_ref Derived, file.%Derived.decl [template = constants.%Derived]
+// CHECK:STDOUT:   %d.ref.loc21_20: <unbound element of class Derived> = name_ref d, @Derived.%.loc16 [template = @Derived.%.loc16]
+// CHECK:STDOUT:   %.loc21_11.1: ref i32 = class_element_access %d.ref.loc21_10, element1
+// CHECK:STDOUT:   %.loc21_11.2: i32 = bind_value %.loc21_11.1
+// CHECK:STDOUT:   return %.loc21_11.2
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @AccessBase(%d: Derived) -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %d.ref: Derived = name_ref d, %d
+// CHECK:STDOUT:   %Base.ref: type = name_ref Base, file.%Base.decl [template = constants.%Base]
+// CHECK:STDOUT:   %b.ref: <unbound element of class Base> = name_ref b, @Base.%.loc9 [template = @Base.%.loc9]
+// CHECK:STDOUT:   %.loc25_11.1: ref Base = class_element_access %d.ref, element0
+// CHECK:STDOUT:   %.loc25_10: ref Base = converted %d.ref, %.loc25_11.1
+// CHECK:STDOUT:   %.loc25_11.2: ref i32 = class_element_access %.loc25_10, element1
+// CHECK:STDOUT:   %.loc25_11.3: i32 = bind_value %.loc25_11.2
+// CHECK:STDOUT:   return %.loc25_11.3
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 75 - 0
toolchain/check/testdata/class/fail_compound_type_mismatch.carbon

@@ -0,0 +1,75 @@
+// 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
+//
+// AUTOUPDATE
+
+class A {
+  var a: i32;
+}
+
+class B {
+  var b: i32;
+}
+
+fn AccessBInA(a: A) -> i32 {
+  // CHECK:STDERR: fail_compound_type_mismatch.carbon:[[@LINE+3]]:10: ERROR: Cannot implicitly convert from `A` to `B`.
+  // CHECK:STDERR:   return a.(B.b);
+  // CHECK:STDERR:          ^~~~~~~
+  return a.(B.b);
+}
+
+// CHECK:STDOUT: --- fail_compound_type_mismatch.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %A: type = class_type @A [template]
+// CHECK:STDOUT:   %.1: type = unbound_element_type A, i32 [template]
+// CHECK:STDOUT:   %.2: type = struct_type {.a: i32} [template]
+// CHECK:STDOUT:   %B: type = class_type @B [template]
+// CHECK:STDOUT:   %.3: type = unbound_element_type B, i32 [template]
+// CHECK:STDOUT:   %.4: type = struct_type {.b: i32} [template]
+// CHECK:STDOUT:   %.5: type = ptr_type {.a: i32} [template]
+// CHECK:STDOUT:   %.6: type = ptr_type {.b: i32} [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [template] {
+// CHECK:STDOUT:     .A = %A.decl
+// CHECK:STDOUT:     .B = %B.decl
+// CHECK:STDOUT:     .AccessBInA = %AccessBInA
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %A.decl: type = class_decl @A [template = constants.%A] {}
+// CHECK:STDOUT:   %B.decl: type = class_decl @B [template = constants.%B] {}
+// CHECK:STDOUT:   %AccessBInA: <function> = fn_decl @AccessBInA [template] {
+// CHECK:STDOUT:     %A.ref: type = name_ref A, %A.decl [template = constants.%A]
+// CHECK:STDOUT:     %a.loc15_15.1: A = param a
+// CHECK:STDOUT:     @AccessBInA.%a: A = bind_name a, %a.loc15_15.1
+// CHECK:STDOUT:     %return.var: ref i32 = var <return slot>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @A {
+// CHECK:STDOUT:   %.loc8: <unbound element of class A> = field_decl a, element0 [template]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%A
+// CHECK:STDOUT:   .a = %.loc8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @B {
+// CHECK:STDOUT:   %.loc12: <unbound element of class B> = field_decl b, element0 [template]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = constants.%B
+// CHECK:STDOUT:   .b = %.loc12
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @AccessBInA(%a: A) -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %a.ref: A = name_ref a, %a
+// CHECK:STDOUT:   %B.ref: type = name_ref B, file.%B.decl [template = constants.%B]
+// CHECK:STDOUT:   %b.ref: <unbound element of class B> = name_ref b, @B.%.loc12 [template = @B.%.loc12]
+// CHECK:STDOUT:   %.loc19: i32 = class_element_access <error>, element0 [template = <error>]
+// CHECK:STDOUT:   return <error>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 118 - 0
toolchain/check/testdata/impl/compound.carbon

@@ -0,0 +1,118 @@
+// 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
+//
+// AUTOUPDATE
+
+interface Simple {
+  fn F();
+  fn G[self: Self]();
+}
+
+impl i32 as Simple {
+  fn F();
+  fn G[self: i32]();
+}
+
+fn NonInstanceCall(n: i32) {
+  n.(Simple.F)();
+}
+
+fn InstanceCall(n: i32) {
+  n.(Simple.G)();
+}
+
+// CHECK:STDOUT: --- compound.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.1: type = interface_type @Simple [template]
+// CHECK:STDOUT:   %.2: type = assoc_entity_type @Simple, <function> [template]
+// CHECK:STDOUT:   %.3: <associated <function> in Simple> = assoc_entity element0, @Simple.%F [template]
+// CHECK:STDOUT:   %.4: <associated <function> in Simple> = assoc_entity element1, @Simple.%G [template]
+// CHECK:STDOUT:   %.5: <witness> = interface_witness (@impl.%F, @impl.%G) [template]
+// CHECK:STDOUT:   %.6: type = tuple_type () [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [template] {
+// CHECK:STDOUT:     .Simple = %Simple.decl
+// CHECK:STDOUT:     .NonInstanceCall = %NonInstanceCall
+// CHECK:STDOUT:     .InstanceCall = %InstanceCall
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %Simple.decl: type = interface_decl @Simple [template = constants.%.1] {}
+// CHECK:STDOUT:   impl_decl @impl {
+// CHECK:STDOUT:     %Simple.ref: type = name_ref Simple, %Simple.decl [template = constants.%.1]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %NonInstanceCall: <function> = fn_decl @NonInstanceCall [template] {
+// CHECK:STDOUT:     %n.loc17_20.1: i32 = param n
+// CHECK:STDOUT:     @NonInstanceCall.%n: i32 = bind_name n, %n.loc17_20.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %InstanceCall: <function> = fn_decl @InstanceCall [template] {
+// CHECK:STDOUT:     %n.loc21_17.1: i32 = param n
+// CHECK:STDOUT:     @InstanceCall.%n: i32 = bind_name n, %n.loc21_17.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: interface @Simple {
+// CHECK:STDOUT:   %Self: Simple = bind_symbolic_name Self [symbolic]
+// CHECK:STDOUT:   %F: <function> = fn_decl @F.1 [template] {}
+// CHECK:STDOUT:   %.loc8: <associated <function> in Simple> = assoc_entity element0, %F [template = constants.%.3]
+// CHECK:STDOUT:   %G: <function> = fn_decl @G.1 [template] {
+// CHECK:STDOUT:     %Self.ref: Simple = name_ref Self, %Self [symbolic = %Self]
+// CHECK:STDOUT:     %.loc9_14.1: type = facet_type_access %Self.ref [symbolic = %Self]
+// CHECK:STDOUT:     %.loc9_14.2: type = converted %Self.ref, %.loc9_14.1 [symbolic = %Self]
+// CHECK:STDOUT:     %self.loc9_8.1: Self = param self
+// CHECK:STDOUT:     %self.loc9_8.2: Self = bind_name self, %self.loc9_8.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %.loc9_21: <associated <function> in Simple> = assoc_entity element1, %G [template = constants.%.4]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = %Self
+// CHECK:STDOUT:   .F = %.loc8
+// CHECK:STDOUT:   .G = %.loc9_21
+// CHECK:STDOUT:   witness = (%F, %G)
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: impl @impl: i32 as Simple {
+// CHECK:STDOUT:   %F: <function> = fn_decl @F.2 [template] {}
+// CHECK:STDOUT:   %G: <function> = fn_decl @G.2 [template] {
+// CHECK:STDOUT:     %self.loc14_8.1: i32 = param self
+// CHECK:STDOUT:     %self.loc14_8.2: i32 = bind_name self, %self.loc14_8.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %.1: <witness> = interface_witness (%F, %G) [template = constants.%.5]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .F = %F
+// CHECK:STDOUT:   .G = %G
+// CHECK:STDOUT:   witness = %.1
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F.1();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G.1[@Simple.%self.loc9_8.2: Self]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F.2();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G.2[@impl.%self.loc14_8.2: i32]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @NonInstanceCall(%n: i32) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %n.ref: i32 = name_ref n, %n
+// CHECK:STDOUT:   %Simple.ref: type = name_ref Simple, file.%Simple.decl [template = constants.%.1]
+// CHECK:STDOUT:   %F.ref: <associated <function> in Simple> = name_ref F, @Simple.%.loc8 [template = constants.%.3]
+// CHECK:STDOUT:   %.1: <function> = interface_witness_access @impl.%.1, element0 [template = @impl.%F]
+// CHECK:STDOUT:   %.loc18: init () = call %.1()
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @InstanceCall(%n: i32) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %n.ref: i32 = name_ref n, %n
+// CHECK:STDOUT:   %Simple.ref: type = name_ref Simple, file.%Simple.decl [template = constants.%.1]
+// CHECK:STDOUT:   %G.ref: <associated <function> in Simple> = name_ref G, @Simple.%.loc9_21 [template = constants.%.4]
+// CHECK:STDOUT:   %.1: <function> = interface_witness_access @impl.%.1, element1 [template = @impl.%G]
+// CHECK:STDOUT:   %.loc22_4: <bound method> = bound_method %n.ref, %.1
+// CHECK:STDOUT:   %.loc22_15: init () = call %.loc22_4(%n.ref)
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 87 - 0
toolchain/check/testdata/impl/fail_call_invalid.carbon

@@ -0,0 +1,87 @@
+// 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
+//
+// AUTOUPDATE
+
+interface Simple {
+  fn G[self: Self]();
+}
+
+impl i32 as Simple {
+  // CHECK:STDERR: fail_call_invalid.carbon:[[@LINE+3]]:14: ERROR: Name `Undeclared` not found.
+  // CHECK:STDERR:   fn G[self: Undeclared]();
+  // CHECK:STDERR:              ^~~~~~~~~~
+  fn G[self: Undeclared]();
+}
+
+fn InstanceCall(n: i32) {
+  n.(Simple.G)();
+}
+
+// CHECK:STDOUT: --- fail_call_invalid.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.1: type = interface_type @Simple [template]
+// CHECK:STDOUT:   %.2: type = assoc_entity_type @Simple, <function> [template]
+// CHECK:STDOUT:   %.3: <associated <function> in Simple> = assoc_entity element0, @Simple.%G [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [template] {
+// CHECK:STDOUT:     .Simple = %Simple.decl
+// CHECK:STDOUT:     .InstanceCall = %InstanceCall
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %Simple.decl: type = interface_decl @Simple [template = constants.%.1] {}
+// CHECK:STDOUT:   impl_decl @impl {
+// CHECK:STDOUT:     %Simple.ref: type = name_ref Simple, %Simple.decl [template = constants.%.1]
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %InstanceCall: <function> = fn_decl @InstanceCall [template] {
+// CHECK:STDOUT:     %n.loc18_17.1: i32 = param n
+// CHECK:STDOUT:     @InstanceCall.%n: i32 = bind_name n, %n.loc18_17.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: interface @Simple {
+// CHECK:STDOUT:   %Self: Simple = bind_symbolic_name Self [symbolic]
+// CHECK:STDOUT:   %G: <function> = fn_decl @G.1 [template] {
+// CHECK:STDOUT:     %Self.ref: Simple = name_ref Self, %Self [symbolic = %Self]
+// CHECK:STDOUT:     %.loc8_14.1: type = facet_type_access %Self.ref [symbolic = %Self]
+// CHECK:STDOUT:     %.loc8_14.2: type = converted %Self.ref, %.loc8_14.1 [symbolic = %Self]
+// CHECK:STDOUT:     %self.loc8_8.1: Self = param self
+// CHECK:STDOUT:     %self.loc8_8.2: Self = bind_name self, %self.loc8_8.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %.loc8_21: <associated <function> in Simple> = assoc_entity element0, %G [template = constants.%.3]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .Self = %Self
+// CHECK:STDOUT:   .G = %.loc8_21
+// CHECK:STDOUT:   witness = (%G)
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: impl @impl: i32 as Simple {
+// CHECK:STDOUT:   %G: <function> = fn_decl @G.2 [template] {
+// CHECK:STDOUT:     %Undeclared.ref: <error> = name_ref Undeclared, <error> [template = <error>]
+// CHECK:STDOUT:     %self.loc15_8.1: <error> = param self
+// CHECK:STDOUT:     %self.loc15_8.2: <error> = bind_name self, %self.loc15_8.1
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %.1: <witness> = interface_witness (<error>) [template = <error>]
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .G = %G
+// CHECK:STDOUT:   witness = %.1
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G.1[@Simple.%self.loc8_8.2: Self]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G.2[@impl.%self.loc15_8.2: <error>]();
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @InstanceCall(%n: i32) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %n.ref: i32 = name_ref n, %n
+// CHECK:STDOUT:   %Simple.ref: type = name_ref Simple, file.%Simple.decl [template = constants.%.1]
+// CHECK:STDOUT:   %G.ref: <associated <function> in Simple> = name_ref G, @Simple.%.loc8_21 [template = constants.%.3]
+// CHECK:STDOUT:   %.1: <function> = interface_witness_access @impl.%.1, element0 [template = <error>]
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 63 - 0
toolchain/check/testdata/operators/fail_redundant_compound_access.carbon

@@ -0,0 +1,63 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() -> i32 { return 0; }
+
+fn Main() {
+  var a: i32 = 3;
+  // CHECK:STDERR: fail_redundant_compound_access.carbon:[[@LINE+3]]:7: ERROR: Member name of type `i32` in compound member access is not an instance member or an interface member.
+  // CHECK:STDERR:   a = a.(a);
+  // CHECK:STDERR:       ^~~~~
+  a = a.(a);
+  // CHECK:STDERR: fail_redundant_compound_access.carbon:[[@LINE+3]]:7: ERROR: Member name of type `<function>` in compound member access is not an instance member or an interface member.
+  // CHECK:STDERR:   a = a.(F)();
+  // CHECK:STDERR:       ^~~~~
+  a = a.(F)();
+}
+
+// CHECK:STDOUT: --- fail_redundant_compound_access.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %.1: i32 = int_literal 0 [template]
+// CHECK:STDOUT:   %.2: i32 = int_literal 3 [template]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   package: <namespace> = namespace [template] {
+// CHECK:STDOUT:     .F = %F
+// CHECK:STDOUT:     .Main = %Main
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %F: <function> = fn_decl @F [template] {
+// CHECK:STDOUT:     %return.var: ref i32 = var <return slot>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %Main: <function> = fn_decl @Main [template] {}
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc7: i32 = int_literal 0 [template = constants.%.1]
+// CHECK:STDOUT:   return %.loc7
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Main() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %a.var: ref i32 = var a
+// CHECK:STDOUT:   %a: ref i32 = bind_name a, %a.var
+// CHECK:STDOUT:   %.loc10: i32 = int_literal 3 [template = constants.%.2]
+// CHECK:STDOUT:   assign %a.var, %.loc10
+// CHECK:STDOUT:   %a.ref.loc14_3: ref i32 = name_ref a, %a
+// CHECK:STDOUT:   %a.ref.loc14_7: ref i32 = name_ref a, %a
+// CHECK:STDOUT:   %a.ref.loc14_10: ref i32 = name_ref a, %a
+// CHECK:STDOUT:   %.loc14: i32 = bind_value %a.ref.loc14_10
+// CHECK:STDOUT:   assign %a.ref.loc14_3, %.loc14
+// CHECK:STDOUT:   %a.ref.loc18_3: ref i32 = name_ref a, %a
+// CHECK:STDOUT:   %a.ref.loc18_7: ref i32 = name_ref a, %a
+// CHECK:STDOUT:   %F.ref: <function> = name_ref F, file.%F [template = file.%F]
+// CHECK:STDOUT:   %.loc18: init i32 = call %F.ref()
+// CHECK:STDOUT:   assign %a.ref.loc18_3, %.loc18
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 1 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -234,6 +234,7 @@ CARBON_DIAGNOSTIC_KIND(CopyOfUncopyableType)
 CARBON_DIAGNOSTIC_KIND(DerefOfNonPointer)
 CARBON_DIAGNOSTIC_KIND(DerefOfType)
 CARBON_DIAGNOSTIC_KIND(CompileTimeBindingInVarDecl)
+CARBON_DIAGNOSTIC_KIND(CompoundMemberAccessDoesNotUseBase)
 CARBON_DIAGNOSTIC_KIND(NameAmbiguousDueToExtend)
 CARBON_DIAGNOSTIC_KIND(NameNotFound)
 CARBON_DIAGNOSTIC_KIND(NameDeclDuplicate)

+ 37 - 0
toolchain/lower/testdata/impl/impl.carbon

@@ -0,0 +1,37 @@
+// 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
+//
+// AUTOUPDATE
+
+interface I {
+  fn F[self: Self]() -> i32;
+}
+
+class A {
+  var n: i32;
+}
+
+impl A as I {
+  fn F[self: A]() -> i32 {
+    return self.n;
+  }
+}
+
+fn Call(a: A) -> i32 {
+  return a.(I.F)();
+}
+
+// CHECK:STDOUT: ; ModuleID = 'impl.carbon'
+// CHECK:STDOUT: source_filename = "impl.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @F(ptr %self) {
+// CHECK:STDOUT:   %n = getelementptr inbounds { i32 }, ptr %self, i32 0, i32 0
+// CHECK:STDOUT:   %1 = load i32, ptr %n, align 4
+// CHECK:STDOUT:   ret i32 %1
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @Call(ptr %a) {
+// CHECK:STDOUT:   %F = call i32 @F(ptr %a)
+// CHECK:STDOUT:   ret i32 %F
+// CHECK:STDOUT: }

+ 23 - 3
toolchain/parse/handle_period.cpp

@@ -10,7 +10,7 @@ namespace Carbon::Parse {
 // TODO: This currently only supports identifiers on the rhs, but will in the
 // future need to handle things like `object.(Interface.member)` for qualifiers.
 static auto HandlePeriodOrArrow(Context& context, NodeKind node_kind,
-                                bool is_arrow) -> void {
+                                State paren_state, bool is_arrow) -> void {
   auto state = context.PopState();
 
   // We're handling `.something` or `->something`.
@@ -25,6 +25,12 @@ static auto HandlePeriodOrArrow(Context& context, NodeKind node_kind,
                                              NodeKind::BaseName)) {
     // OK, `.base`. This is allowed in any name context other than declaring a
     // new qualified name: `fn Namespace.base() {}`
+  } else if (paren_state != State::Invalid &&
+             context.PositionIs(Lex::TokenKind::OpenParen)) {
+    state.state = paren_state;
+    context.PushState(state);
+    context.PushState(State::ParenExpr);
+    return;
   } else {
     CARBON_DIAGNOSTIC(ExpectedIdentifierAfterDotOrArrow, Error,
                       "Expected identifier after `{0}`.", llvm::StringLiteral);
@@ -49,23 +55,37 @@ static auto HandlePeriodOrArrow(Context& context, NodeKind node_kind,
 }
 
 auto HandlePeriodAsDecl(Context& context) -> void {
-  HandlePeriodOrArrow(context, NodeKind::QualifiedName,
+  HandlePeriodOrArrow(context, NodeKind::QualifiedName, State::Invalid,
                       /*is_arrow=*/false);
 }
 
 auto HandlePeriodAsExpr(Context& context) -> void {
   HandlePeriodOrArrow(context, NodeKind::MemberAccessExpr,
+                      State::CompoundMemberAccess,
                       /*is_arrow=*/false);
 }
 
 auto HandlePeriodAsStruct(Context& context) -> void {
-  HandlePeriodOrArrow(context, NodeKind::StructFieldDesignator,
+  HandlePeriodOrArrow(context, NodeKind::StructFieldDesignator, State::Invalid,
                       /*is_arrow=*/false);
 }
 
 auto HandleArrowExpr(Context& context) -> void {
   HandlePeriodOrArrow(context, NodeKind::PointerMemberAccessExpr,
+                      State::CompoundPointerMemberAccess,
                       /*is_arrow=*/true);
 }
 
+auto HandleCompoundMemberAccess(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddNode(NodeKind::MemberAccessExpr, state.token, state.subtree_start,
+                  state.has_error);
+}
+
+auto HandleCompoundPointerMemberAccess(Context& context) -> void {
+  auto state = context.PopState();
+  context.AddNode(NodeKind::PointerMemberAccessExpr, state.token,
+                  state.subtree_start, state.has_error);
+}
+
 }  // namespace Carbon::Parse

+ 2 - 0
toolchain/parse/node_ids.h

@@ -66,6 +66,8 @@ using AnyDeclId = NodeIdInCategory<NodeCategory::Decl>;
 using AnyExprId = NodeIdInCategory<NodeCategory::Expr>;
 using AnyImplAsId = NodeIdInCategory<NodeCategory::ImplAs>;
 using AnyMemberNameId = NodeIdInCategory<NodeCategory::MemberName>;
+using AnyMemberNameOrMemberExprId =
+    NodeIdInCategory<NodeCategory::MemberName | NodeCategory::MemberExpr>;
 using AnyModifierId = NodeIdInCategory<NodeCategory::Modifier>;
 using AnyNameComponentId = NodeIdInCategory<NodeCategory::NameComponent>;
 using AnyPatternId = NodeIdInCategory<NodeCategory::Pattern>;

+ 6 - 5
toolchain/parse/node_kind.h

@@ -23,11 +23,12 @@ enum class NodeCategory : uint32_t {
   Decl = 1 << 0,
   Expr = 1 << 1,
   ImplAs = 1 << 2,
-  MemberName = 1 << 3,
-  Modifier = 1 << 4,
-  NameComponent = 1 << 5,
-  Pattern = 1 << 6,
-  Statement = 1 << 7,
+  MemberExpr = 1 << 3,
+  MemberName = 1 << 4,
+  Modifier = 1 << 5,
+  NameComponent = 1 << 6,
+  Pattern = 1 << 7,
+  Statement = 1 << 8,
   None = 0,
 
   LLVM_MARK_AS_BITMASK_ENUM(/*LargestValue=*/Statement)

+ 31 - 3
toolchain/parse/state.def

@@ -385,13 +385,41 @@ CARBON_PARSE_STATE(DeclScopeLoop)
 //
 // . name
 // ^~~~~~
-//   (state done)
-//
-// . ???    (??? consumed if it is a keyword)
+// -> name
+// ^~~~~~~
+// . base    (variant is not Decl)
+// ^~~~~~
+// -> base   (variant is not Decl)
+// ^~~~~~~
+// . ???     (??? consumed if it is a keyword)
 // ^
+// -> ???    (??? consumed if it is a keyword)
+// ^~
 //   (state done)
+//
+// expr . ( ... )
+//      ^
+//   1. ParenExpr
+//   2. CompoundMemberAccess
+//
+// expr -> ( ... )
+//      ^~
+//   1. ParenExpr
+//   2. CompoundPointerMemberAccess
 CARBON_PARSE_STATE_VARIANTS3(Period, Decl, Expr, Struct)
 
+// Handles a compound member access after we parse the name expression.
+//
+// expr . ( expr )
+//                ^
+CARBON_PARSE_STATE(CompoundMemberAccess)
+
+// Handles a compound pointer member access after we parse the name expression.
+//
+// expr -> ( expr )
+//                 ^
+CARBON_PARSE_STATE(CompoundPointerMemberAccess)
+
 // Handles `->name` expressions. Identical to PeriodAsExpr except for the
 // leading token.
 //

+ 93 - 0
toolchain/parse/testdata/generics/impl/fail_out_of_line_member.carbon

@@ -0,0 +1,93 @@
+// 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
+//
+// AUTOUPDATE
+
+// TODO: Decide if we want to support this syntax. See #3763.
+
+interface Interface {
+  fn F();
+}
+
+impl bool as Interface {
+  fn F();
+}
+
+// CHECK:STDERR: fail_out_of_line_member.carbon:[[@LINE+3]]:4: ERROR: `fn` introducer should be followed by a name.
+// CHECK:STDERR: fn (bool as Interface).F() {}
+// CHECK:STDERR:    ^
+fn (bool as Interface).F() {}
+
+class C {
+  impl Self as Interface {
+    fn F();
+  }
+}
+
+// TODO: The error recovery here is not very good. The `(` is treated as
+// starting the function parameter list.
+// CHECK:STDERR: fail_out_of_line_member.carbon:[[@LINE+9]]:6: ERROR: Expected identifier after `.`.
+// CHECK:STDERR: fn C.(Self as Interface).F() {}
+// CHECK:STDERR:      ^
+// CHECK:STDERR: fail_out_of_line_member.carbon:[[@LINE+6]]:7: ERROR: Expected binding pattern.
+// CHECK:STDERR: fn C.(Self as Interface).F() {}
+// CHECK:STDERR:       ^~~~
+// CHECK:STDERR: fail_out_of_line_member.carbon:[[@LINE+3]]:25: ERROR: `fn` declarations must either end with a `;` or have a `{ ... }` block for a definition.
+// CHECK:STDERR: fn C.(Self as Interface).F() {}
+// CHECK:STDERR:                         ^
+fn C.(Self as Interface).F() {}
+
+// CHECK:STDOUT: - filename: fail_out_of_line_member.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'InterfaceIntroducer', text: 'interface'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'Interface'},
+// CHECK:STDOUT:       {kind: 'InterfaceDefinitionStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'InterfaceDefinition', text: '}', subtree_size: 9},
+// CHECK:STDOUT:         {kind: 'ImplIntroducer', text: 'impl'},
+// CHECK:STDOUT:           {kind: 'BoolTypeLiteral', text: 'bool'},
+// CHECK:STDOUT:         {kind: 'TypeImplAs', text: 'as', subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'IdentifierNameExpr', text: 'Interface'},
+// CHECK:STDOUT:       {kind: 'ImplDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'ImplDefinition', text: '}', subtree_size: 11},
+// CHECK:STDOUT:       {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:       {kind: 'InvalidParse', text: '(', has_error: yes},
+// CHECK:STDOUT:     {kind: 'FunctionDecl', text: '}', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'ClassIntroducer', text: 'class'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'C'},
+// CHECK:STDOUT:       {kind: 'ClassDefinitionStart', text: '{', subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'ImplIntroducer', text: 'impl'},
+// CHECK:STDOUT:             {kind: 'SelfTypeNameExpr', text: 'Self'},
+// CHECK:STDOUT:           {kind: 'TypeImplAs', text: 'as', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'Interface'},
+// CHECK:STDOUT:         {kind: 'ImplDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:             {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:           {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:         {kind: 'FunctionDecl', text: ';', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ImplDefinition', text: '}', subtree_size: 11},
+// CHECK:STDOUT:     {kind: 'ClassDefinition', text: '}', subtree_size: 15},
+// CHECK:STDOUT:       {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'C'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: '(', has_error: yes},
+// CHECK:STDOUT:       {kind: 'QualifiedName', text: '.', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'Self', has_error: yes},
+// CHECK:STDOUT:           {kind: 'InvalidParse', text: 'Self', has_error: yes},
+// CHECK:STDOUT:         {kind: 'BindingPattern', text: 'Self', has_error: yes, subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'TuplePattern', text: ')', has_error: yes, subtree_size: 5},
+// CHECK:STDOUT:     {kind: 'FunctionDecl', text: '}', has_error: yes, subtree_size: 10},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 34 - 0
toolchain/parse/testdata/member_access/compound.carbon

@@ -0,0 +1,34 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() {
+  a.(b);
+  a->(b);
+}
+
+// CHECK:STDOUT: - filename: compound.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:             {kind: 'ExprOpenParen', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'b'},
+// CHECK:STDOUT:           {kind: 'ParenExpr', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:             {kind: 'ExprOpenParen', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'b'},
+// CHECK:STDOUT:           {kind: 'ParenExpr', text: ')', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 18},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 68 - 0
toolchain/parse/testdata/member_access/fail_keyword.carbon

@@ -0,0 +1,68 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() {
+  // CHECK:STDERR: fail_keyword.carbon:[[@LINE+3]]:5: ERROR: Expected identifier after `.`.
+  // CHECK:STDERR:   a.self;
+  // CHECK:STDERR:     ^~~~
+  a.self;
+  // CHECK:STDERR: fail_keyword.carbon:[[@LINE+3]]:5: ERROR: Expected identifier after `.`.
+  // CHECK:STDERR:   a.Self;
+  // CHECK:STDERR:     ^~~~
+  a.Self;
+  // CHECK:STDERR: fail_keyword.carbon:[[@LINE+3]]:5: ERROR: Expected identifier after `.`.
+  // CHECK:STDERR:   a.for;
+  // CHECK:STDERR:     ^~~
+  a.for;
+  // CHECK:STDERR: fail_keyword.carbon:[[@LINE+3]]:6: ERROR: Expected identifier after `->`.
+  // CHECK:STDERR:   p->self;
+  // CHECK:STDERR:      ^~~~
+  p->self;
+  // CHECK:STDERR: fail_keyword.carbon:[[@LINE+3]]:6: ERROR: Expected identifier after `->`.
+  // CHECK:STDERR:   p->Self;
+  // CHECK:STDERR:      ^~~~
+  p->Self;
+  // CHECK:STDERR: fail_keyword.carbon:[[@LINE+3]]:6: ERROR: Expected identifier after `->`.
+  // CHECK:STDERR:   p->while;
+  // CHECK:STDERR:      ^~~~~
+  p->while;
+}
+
+// CHECK:STDOUT: - filename: fail_keyword.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'self', has_error: yes},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'Self', has_error: yes},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'for', has_error: yes},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'p'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'self', has_error: yes},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'p'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'Self', has_error: yes},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'p'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'while', has_error: yes},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 30},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 30 - 0
toolchain/parse/testdata/member_access/keyword.carbon

@@ -0,0 +1,30 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F() {
+  a.base;
+  p->base;
+}
+
+// CHECK:STDOUT: - filename: keyword.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 2},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 5},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:           {kind: 'BaseName', text: 'base'},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'p'},
+// CHECK:STDOUT:           {kind: 'BaseName', text: 'base'},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 14},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 47 - 0
toolchain/parse/testdata/member_access/simple.carbon

@@ -0,0 +1,47 @@
+// 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
+//
+// AUTOUPDATE
+
+fn F(a: T) {
+  a.x;
+  a->x;
+  a.x->y;
+  a->x.y;
+}
+
+// CHECK:STDOUT: - filename: simple.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:         {kind: 'FunctionIntroducer', text: 'fn'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'F'},
+// CHECK:STDOUT:           {kind: 'TuplePatternStart', text: '('},
+// CHECK:STDOUT:             {kind: 'IdentifierName', text: 'a'},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'T'},
+// CHECK:STDOUT:           {kind: 'BindingPattern', text: ':', subtree_size: 3},
+// CHECK:STDOUT:         {kind: 'TuplePattern', text: ')', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'FunctionDefinitionStart', text: '{', subtree_size: 8},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:           {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 3},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 4},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:             {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:           {kind: 'MemberAccessExpr', text: '.', subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'y'},
+// CHECK:STDOUT:         {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:             {kind: 'IdentifierNameExpr', text: 'a'},
+// CHECK:STDOUT:             {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:           {kind: 'PointerMemberAccessExpr', text: '->', subtree_size: 3},
+// CHECK:STDOUT:           {kind: 'IdentifierName', text: 'y'},
+// CHECK:STDOUT:         {kind: 'MemberAccessExpr', text: '.', subtree_size: 5},
+// CHECK:STDOUT:       {kind: 'ExprStatement', text: ';', subtree_size: 6},
+// CHECK:STDOUT:     {kind: 'FunctionDefinition', text: '}', subtree_size: 29},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 26 - 0
toolchain/parse/testdata/struct/fail_dot_paren.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
+//
+// AUTOUPDATE
+
+// CHECK:STDERR: fail_dot_paren.carbon:[[@LINE+3]]:10: ERROR: Expected identifier after `.`.
+// CHECK:STDERR: var x: {.(a) = 1};
+// CHECK:STDERR:          ^
+var x: {.(a) = 1};
+
+// CHECK:STDOUT: - filename: fail_dot_paren.carbon
+// CHECK:STDOUT:   parse_tree: [
+// CHECK:STDOUT:     {kind: 'FileStart', text: ''},
+// CHECK:STDOUT:       {kind: 'VariableIntroducer', text: 'var'},
+// CHECK:STDOUT:         {kind: 'IdentifierName', text: 'x'},
+// CHECK:STDOUT:           {kind: 'StructLiteralOrStructTypeLiteralStart', text: '{'},
+// CHECK:STDOUT:             {kind: 'IdentifierName', text: '(', has_error: yes},
+// CHECK:STDOUT:           {kind: 'StructFieldDesignator', text: '.', subtree_size: 2},
+// CHECK:STDOUT:           {kind: 'IntLiteral', text: '1'},
+// CHECK:STDOUT:           {kind: 'InvalidParse', text: '=', has_error: yes},
+// CHECK:STDOUT:         {kind: 'StructLiteral', text: '}', has_error: yes, subtree_size: 6},
+// CHECK:STDOUT:       {kind: 'BindingPattern', text: ':', subtree_size: 8},
+// CHECK:STDOUT:     {kind: 'VariableDecl', text: ';', subtree_size: 10},
+// CHECK:STDOUT:     {kind: 'FileEnd', text: ''},
+// CHECK:STDOUT:   ]

+ 6 - 5
toolchain/parse/typed_nodes.h

@@ -620,7 +620,8 @@ using ExprOpenParen = LeafNode<NodeKind::ExprOpenParen>;
 
 // A parenthesized expression: `(a)`.
 struct ParenExpr {
-  static constexpr auto Kind = NodeKind::ParenExpr.Define(NodeCategory::Expr);
+  static constexpr auto Kind =
+      NodeKind::ParenExpr.Define(NodeCategory::Expr | NodeCategory::MemberExpr);
 
   ExprOpenParenId left_paren;
   AnyExprId expr;
@@ -656,22 +657,22 @@ struct CallExpr {
   CommaSeparatedList<AnyExprId, CallExprCommaId> arguments;
 };
 
-// A simple member access expression: `a.b`.
+// A member access expression: `a.b` or `a.(b)`.
 struct MemberAccessExpr {
   static constexpr auto Kind =
       NodeKind::MemberAccessExpr.Define(NodeCategory::Expr);
 
   AnyExprId lhs;
-  AnyMemberNameId rhs;
+  AnyMemberNameOrMemberExprId rhs;
 };
 
-// A simple indirect member access expression: `a->b`.
+// An indirect member access expression: `a->b` or `a->(b)`.
 struct PointerMemberAccessExpr {
   static constexpr auto Kind =
       NodeKind::PointerMemberAccessExpr.Define(NodeCategory::Expr);
 
   AnyExprId lhs;
-  AnyMemberNameId rhs;
+  AnyMemberNameOrMemberExprId rhs;
 };
 
 // A prefix operator expression.