Просмотр исходного кода

Support for member access into classes. (#3335)

Add support for member access into classes, for both non-instance
members and for fields.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Richard Smith 2 лет назад
Родитель
Сommit
ab575cf32a

+ 74 - 28
toolchain/check/context.cpp

@@ -170,6 +170,30 @@ auto Context::FollowNameReferences(SemIR::NodeId node_id) -> SemIR::NodeId {
   return node_id;
 }
 
+auto Context::GetConstantValue(SemIR::NodeId node_id) -> SemIR::NodeId {
+  // TODO: The constant value of a node should be computed as we build the
+  // node, or at least cached once computed.
+  while (true) {
+    auto node = nodes().Get(node_id);
+    switch (node.kind()) {
+      case SemIR::NameReference::Kind:
+        node_id = node.As<SemIR::NameReference>().value_id;
+        break;
+
+      case SemIR::BindName::Kind:
+        node_id = node.As<SemIR::BindName>().value_id;
+        break;
+
+      case SemIR::Field::Kind:
+        return node_id;
+
+      default:
+        // TODO: Handle the remaining cases.
+        return SemIR::NodeId::Invalid;
+    }
+  }
+}
+
 template <typename BranchNode, typename... Args>
 static auto AddDominatedBlockAndBranchImpl(Context& context,
                                            Parse::Node parse_node, Args... args)
@@ -454,18 +478,26 @@ class TypeCompleter {
 
   // Makes a value representation that uses pass-by-copy, copying the given
   // type.
-  auto MakeCopyRepresentation(SemIR::TypeId rep_id) const
+  auto MakeCopyRepresentation(
+      SemIR::TypeId rep_id,
+      SemIR::ValueRepresentation::AggregateKind aggregate_kind =
+          SemIR::ValueRepresentation::NotAggregate) const
       -> SemIR::ValueRepresentation {
-    return {.kind = SemIR::ValueRepresentation::Copy, .type_id = rep_id};
+    return {.kind = SemIR::ValueRepresentation::Copy,
+            .aggregate_kind = aggregate_kind,
+            .type_id = rep_id};
   }
 
   // Makes a value representation that uses pass-by-address with the given
   // pointee type.
-  auto MakePointerRepresentation(Parse::Node parse_node,
-                                 SemIR::TypeId pointee_id) const
+  auto MakePointerRepresentation(
+      Parse::Node parse_node, SemIR::TypeId pointee_id,
+      SemIR::ValueRepresentation::AggregateKind aggregate_kind =
+          SemIR::ValueRepresentation::NotAggregate) const
       -> SemIR::ValueRepresentation {
     // TODO: Should we add `const` qualification to `pointee_id`?
     return {.kind = SemIR::ValueRepresentation::Pointer,
+            .aggregate_kind = aggregate_kind,
             .type_id = context_.GetPointerType(parse_node, pointee_id)};
   }
 
@@ -517,10 +549,33 @@ class TypeCompleter {
     llvm_unreachable("All builtin kinds were handled above");
   }
 
+  auto BuildStructOrTupleValueRepresentation(Parse::Node parse_node,
+                                             std::size_t num_elements,
+                                             SemIR::TypeId elementwise_rep,
+                                             bool same_as_object_rep) const
+      -> SemIR::ValueRepresentation {
+    SemIR::ValueRepresentation::AggregateKind aggregate_kind =
+        same_as_object_rep ? SemIR::ValueRepresentation::ValueAndObjectAggregate
+                           : SemIR::ValueRepresentation::ValueAggregate;
+
+    if (num_elements == 1) {
+      // The value representation for a struct or tuple with a single element
+      // is a struct or tuple containing the value representation of the
+      // element.
+      // TODO: Consider doing the same whenever `elementwise_rep` is
+      // sufficiently small.
+      return MakeCopyRepresentation(elementwise_rep, aggregate_kind);
+    }
+    // For a struct or tuple with multiple fields, we use a pointer
+    // to the elementwise value representation.
+    return MakePointerRepresentation(parse_node, elementwise_rep,
+                                     aggregate_kind);
+  }
+
   auto BuildStructTypeValueRepresentation(SemIR::TypeId type_id,
                                           SemIR::StructType struct_type) const
       -> SemIR::ValueRepresentation {
-    // TODO: Share code with tuples.
+    // TODO: Share more code with tuples.
     auto fields = context_.node_blocks().Get(struct_type.fields_id);
     if (fields.empty()) {
       return MakeEmptyRepresentation(struct_type.parse_node);
@@ -547,21 +602,14 @@ class TypeCompleter {
                          : context_.CanonicalizeStructType(
                                struct_type.parse_node,
                                context_.node_blocks().Add(value_rep_fields));
-    if (fields.size() == 1) {
-      // The value representation for a struct with a single field is a
-      // struct containing the value representation of the field.
-      // TODO: Consider doing the same for structs with multiple small
-      // fields.
-      return MakeCopyRepresentation(value_rep);
-    }
-    // For a struct with multiple fields, we use a pointer representation.
-    return MakePointerRepresentation(struct_type.parse_node, value_rep);
+    return BuildStructOrTupleValueRepresentation(
+        struct_type.parse_node, fields.size(), value_rep, same_as_object_rep);
   }
 
   auto BuildTupleTypeValueRepresentation(SemIR::TypeId type_id,
                                          SemIR::TupleType tuple_type) const
       -> SemIR::ValueRepresentation {
-    // TODO: Share code with structs.
+    // TODO: Share more code with structs.
     auto elements = context_.type_blocks().Get(tuple_type.elements_id);
     if (elements.empty()) {
       return MakeEmptyRepresentation(tuple_type.parse_node);
@@ -584,15 +632,8 @@ class TypeCompleter {
                          ? type_id
                          : context_.CanonicalizeTupleType(tuple_type.parse_node,
                                                           value_rep_elements);
-    if (elements.size() == 1) {
-      // The value representation for a tuple with a single element is a
-      // tuple containing the value representation of that element.
-      // TODO: Consider doing the same for tuples with multiple small
-      // elements.
-      return MakeCopyRepresentation(value_rep);
-    }
-    // For a tuple with multiple elements, we use a pointer representation.
-    return MakePointerRepresentation(tuple_type.parse_node, value_rep);
+    return BuildStructOrTupleValueRepresentation(
+        tuple_type.parse_node, elements.size(), value_rep, same_as_object_rep);
   }
 
   // Builds and returns the value representation for the given type. All nested
@@ -621,6 +662,7 @@ class TypeCompleter {
       case SemIR::BranchWithArg::Kind:
       case SemIR::Call::Kind:
       case SemIR::ClassDeclaration::Kind:
+      case SemIR::ClassFieldAccess::Kind:
       case SemIR::Dereference::Kind:
       case SemIR::Field::Kind:
       case SemIR::FunctionDeclaration::Kind:
@@ -660,7 +702,9 @@ class TypeCompleter {
         // For arrays, it's convenient to always use a pointer representation,
         // even when the array has zero or one element, in order to support
         // indexing.
-        return MakePointerRepresentation(node.parse_node(), type_id);
+        return MakePointerRepresentation(
+            node.parse_node(), type_id,
+            SemIR::ValueRepresentation::ObjectAggregate);
       }
 
       case SemIR::StructType::Kind:
@@ -677,9 +721,11 @@ class TypeCompleter {
         // TODO: Support customized value representations for classes.
         // TODO: Pick a better value representation when possible.
         return MakePointerRepresentation(
-            node.parse_node(), context_.classes()
-                                   .Get(node.As<SemIR::ClassType>().class_id)
-                                   .object_representation_id);
+            node.parse_node(),
+            context_.classes()
+                .Get(node.As<SemIR::ClassType>().class_id)
+                .object_representation_id,
+            SemIR::ValueRepresentation::ObjectAggregate);
 
       case SemIR::Builtin::Kind:
         CARBON_FATAL() << "Builtins should be named as cross-references";

+ 3 - 0
toolchain/check/context.h

@@ -96,6 +96,9 @@ class Context {
   // Follows NameReference nodes to find the value named by a given node.
   auto FollowNameReferences(SemIR::NodeId node_id) -> SemIR::NodeId;
 
+  // Gets the constant value of the given node, if it has one.
+  auto GetConstantValue(SemIR::NodeId node_id) -> SemIR::NodeId;
+
   // Adds a `Branch` node branching to a new node block, and returns the ID of
   // the new block. All paths to the branch target must go through the current
   // block, though not necessarily through this branch.

+ 62 - 1
toolchain/check/handle_name.cpp

@@ -55,14 +55,72 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
     return true;
   }
 
+  // If the base isn't a scope, it must have a complete type.
+  auto base_type_id = context.nodes().Get(base_id).type_id();
+  if (!context.TryToCompleteType(base_type_id, [&] {
+        CARBON_DIAGNOSTIC(IncompleteTypeInMemberAccess, Error,
+                          "Member access into object of incomplete type `{0}`.",
+                          std::string);
+        return context.emitter().Build(
+            context.nodes().Get(base_id).parse_node(),
+            IncompleteTypeInMemberAccess,
+            context.sem_ir().StringifyType(base_type_id, true));
+      })) {
+    context.node_stack().Push(parse_node, SemIR::NodeId::BuiltinError);
+    return true;
+  }
+
   // Materialize a temporary for the base expression if necessary.
   base_id = ConvertToValueOrReferenceExpression(context, base_id);
-  auto base_type_id = context.nodes().Get(base_id).type_id();
+  base_type_id = context.nodes().Get(base_id).type_id();
 
   auto base_type = context.nodes().Get(
       context.sem_ir().GetTypeAllowBuiltinTypes(base_type_id));
 
   switch (base_type.kind()) {
+    case SemIR::ClassType::Kind: {
+      // Perform lookup for the name in the class scope.
+      auto class_scope_id = context.classes()
+                                .Get(base_type.As<SemIR::ClassType>().class_id)
+                                .scope_id;
+      auto member_id = context.LookupName(parse_node, name_id, class_scope_id,
+                                          /*print_diagnostics=*/true);
+      if (!member_id.is_valid()) {
+        break;
+      }
+
+      // Perform instance binding if we found an instance member.
+      auto member_type_id = context.nodes().Get(member_id).type_id();
+      auto member_type_node = context.nodes().Get(
+          context.sem_ir().GetTypeAllowBuiltinTypes(member_type_id));
+      if (auto unbound_field_type =
+              member_type_node.TryAs<SemIR::UnboundFieldType>()) {
+        // TODO: Check that the unbound field type describes a member of this
+        // class. Perform a conversion of the base if necessary.
+
+        // Find the named field and build a field access expression.
+        auto field_id = context.GetConstantValue(member_id);
+        CARBON_CHECK(field_id.is_valid())
+            << "Non-constant value " << context.nodes().Get(member_id)
+            << " of unbound field type";
+        auto field = context.nodes().Get(field_id).TryAs<SemIR::Field>();
+        CARBON_CHECK(field)
+            << "Unexpected value " << context.nodes().Get(field_id)
+            << " for field name expression";
+        context.AddNodeAndPush(
+            parse_node, SemIR::ClassFieldAccess{
+                            parse_node, unbound_field_type->field_type_id,
+                            base_id, field->index});
+        return true;
+      }
+
+      // For a non-instance member, the result is that member.
+      // TODO: Track that this was named within `base_id`.
+      context.AddNodeAndPush(
+          parse_node,
+          SemIR::NameReference{parse_node, member_type_id, name_id, member_id});
+      return true;
+    }
     case SemIR::StructType::Kind: {
       auto refs = context.node_blocks().Get(
           base_type.As<SemIR::StructType>().fields_id);
@@ -84,6 +142,9 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
                              context.strings().Get(name_id));
       break;
     }
+    // TODO: `ConstType` should support member access just like the
+    // corresponding non-const type, except that the result should have `const`
+    // type if it creates a reference expression performing field access.
     default: {
       if (base_type_id != SemIR::TypeId::Error) {
         CARBON_DIAGNOSTIC(QualifiedExpressionUnsupported, Error,

+ 5 - 2
toolchain/check/handle_pattern_binding.cpp

@@ -55,8 +55,11 @@ auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
             parse_node, context.GetBuiltinType(SemIR::BuiltinKind::TypeType),
             class_info.self_type_id, cast_type_id});
         value_type_id = context.CanonicalizeType(field_type_node_id);
-        value_id =
-            context.AddNode(SemIR::Field{parse_node, value_type_id, name_id});
+        value_id = context.AddNode(
+            SemIR::Field{parse_node, value_type_id, name_id,
+                         SemIR::MemberIndex(context.args_type_info_stack()
+                                                .PeekCurrentBlockContents()
+                                                .size())});
 
         // Add a corresponding field to the object representation of the class.
         context.args_type_info_stack().AddNode(

+ 1 - 1
toolchain/check/testdata/class/basic.carbon

@@ -34,7 +34,7 @@ fn Run() -> i32 {
 // CHECK:STDOUT:   %F: <function> = fn_decl @F
 // CHECK:STDOUT:   %G: <function> = fn_decl @G
 // CHECK:STDOUT:   %.loc14_8.1: type = unbound_field_type Class, i32
-// CHECK:STDOUT:   %.loc14_8.2: <unbound field of class Class> = field "k"
+// CHECK:STDOUT:   %.loc14_8.2: <unbound field of class Class> = field "k", member0
 // CHECK:STDOUT:   %k: <unbound field of class Class> = bind_name "k", %.loc14_8.2
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:

+ 1 - 1
toolchain/check/testdata/class/fail_incomplete.carbon

@@ -49,7 +49,7 @@ fn ConvertFromStruct() -> Class { return {}; }
 // TODO: }
 
 fn MemberAccess(p: Class*) -> i32 {
-  // CHECK:STDERR: fail_incomplete.carbon:[[@LINE+6]]:11: ERROR: Invalid use of incomplete type `Class`.
+  // CHECK:STDERR: fail_incomplete.carbon:[[@LINE+6]]:11: ERROR: Member access into object of incomplete type `Class`.
   // CHECK:STDERR:   return (*p).n;
   // CHECK:STDERR:           ^
   // CHECK:STDERR: fail_incomplete.carbon:[[@LINE-48]]:1: Class was forward declared here.

+ 1 - 1
toolchain/check/testdata/class/fail_unbound_field.carbon

@@ -31,7 +31,7 @@ fn G() -> i32 {
 // CHECK:STDOUT:
 // CHECK:STDOUT: class @Class {
 // CHECK:STDOUT:   %.loc8_12.1: type = unbound_field_type Class, i32
-// CHECK:STDOUT:   %.loc8_12.2: <unbound field of class Class> = field "field"
+// CHECK:STDOUT:   %.loc8_12.2: <unbound field of class Class> = field "field", member0
 // CHECK:STDOUT:   %field: <unbound field of class Class> = bind_name "field", %.loc8_12.2
 // CHECK:STDOUT:   %F: <function> = fn_decl @F
 // CHECK:STDOUT:

+ 41 - 0
toolchain/check/testdata/class/fail_unknown_member.carbon

@@ -0,0 +1,41 @@
+// 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 Class {
+  var n: i32;
+}
+
+fn G(c: Class) -> i32 {
+  // TODO: Mention the scope in which we looked for the name.
+  // CHECK:STDERR: fail_unknown_member.carbon:[[@LINE+3]]:11: ERROR: Name `something` not found.
+  // CHECK:STDERR:   return c.something;
+  // CHECK:STDERR:           ^
+  return c.something;
+}
+
+// CHECK:STDOUT: file "fail_unknown_member.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc9: type = struct_type {.n: i32}
+// CHECK:STDOUT:   %G: <function> = fn_decl @G
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Class {
+// CHECK:STDOUT:   %.loc8_8.1: type = unbound_field_type Class, i32
+// CHECK:STDOUT:   %.loc8_8.2: <unbound field of class Class> = field "n", member0
+// CHECK:STDOUT:   %n: <unbound field of class Class> = bind_name "n", %.loc8_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .n = %n
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G(%c: Class) -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc7: type = ptr_type {.n: i32}
+// CHECK:STDOUT:   %c.ref: Class = name_reference "c", %c
+// CHECK:STDOUT:   %something.ref: <error> = name_reference "something", <error>
+// CHECK:STDOUT:   return <error>
+// CHECK:STDOUT: }

+ 61 - 0
toolchain/check/testdata/class/field_access.carbon

@@ -0,0 +1,61 @@
+// 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 Class {
+  var j: i32;
+  var k: i32;
+}
+
+fn Run() -> i32 {
+  var c: Class;
+  c.j = 1;
+  c.k = 2;
+  return c.j + c.k;
+}
+
+// CHECK:STDOUT: file "field_access.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc10: type = struct_type {.j: i32, .k: i32}
+// CHECK:STDOUT:   %Run: <function> = fn_decl @Run
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Class {
+// CHECK:STDOUT:   %.loc8_8.1: type = unbound_field_type Class, i32
+// CHECK:STDOUT:   %.loc8_8.2: <unbound field of class Class> = field "j", member0
+// CHECK:STDOUT:   %j: <unbound field of class Class> = bind_name "j", %.loc8_8.2
+// CHECK:STDOUT:   %.loc9_8.1: type = unbound_field_type Class, i32
+// CHECK:STDOUT:   %.loc9_8.2: <unbound field of class Class> = field "k", member1
+// CHECK:STDOUT:   %k: <unbound field of class Class> = bind_name "k", %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .j = %j
+// CHECK:STDOUT:   .k = %k
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Run() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %Class.ref: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc10: type = ptr_type {.j: i32, .k: i32}
+// CHECK:STDOUT:   %c.var: ref Class = var "c"
+// CHECK:STDOUT:   %c: ref Class = bind_name "c", %c.var
+// CHECK:STDOUT:   %c.ref.loc14: ref Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc14_4: ref i32 = class_field_access %c.ref.loc14, member0
+// CHECK:STDOUT:   %.loc14_9: i32 = int_literal 1
+// CHECK:STDOUT:   assign %.loc14_4, %.loc14_9
+// CHECK:STDOUT:   %c.ref.loc15: ref Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc15_4: ref i32 = class_field_access %c.ref.loc15, member1
+// CHECK:STDOUT:   %.loc15_9: i32 = int_literal 2
+// CHECK:STDOUT:   assign %.loc15_4, %.loc15_9
+// CHECK:STDOUT:   %c.ref.loc16_10: ref Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc16_11.1: ref i32 = class_field_access %c.ref.loc16_10, member0
+// CHECK:STDOUT:   %c.ref.loc16_16: ref Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc16_17.1: ref i32 = class_field_access %c.ref.loc16_16, member1
+// CHECK:STDOUT:   %.loc16_11.2: i32 = bind_value %.loc16_11.1
+// CHECK:STDOUT:   %.loc16_17.2: i32 = bind_value %.loc16_17.1
+// CHECK:STDOUT:   %.loc16_14: i32 = add %.loc16_11.2, %.loc16_17.2
+// CHECK:STDOUT:   return %.loc16_14
+// CHECK:STDOUT: }

+ 66 - 0
toolchain/check/testdata/class/field_access_in_value.carbon

@@ -0,0 +1,66 @@
+// 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 Class {
+  var j: i32;
+  var k: i32;
+}
+
+fn Run() -> i32 {
+  var cv: Class;
+  cv.j = 1;
+  cv.k = 2;
+  let c: Class = cv;
+  return c.j + c.k;
+}
+
+// CHECK:STDOUT: file "field_access_in_value.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc10: type = struct_type {.j: i32, .k: i32}
+// CHECK:STDOUT:   %Run: <function> = fn_decl @Run
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Class {
+// CHECK:STDOUT:   %.loc8_8.1: type = unbound_field_type Class, i32
+// CHECK:STDOUT:   %.loc8_8.2: <unbound field of class Class> = field "j", member0
+// CHECK:STDOUT:   %j: <unbound field of class Class> = bind_name "j", %.loc8_8.2
+// CHECK:STDOUT:   %.loc9_8.1: type = unbound_field_type Class, i32
+// CHECK:STDOUT:   %.loc9_8.2: <unbound field of class Class> = field "k", member1
+// CHECK:STDOUT:   %k: <unbound field of class Class> = bind_name "k", %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .j = %j
+// CHECK:STDOUT:   .k = %k
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Run() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %Class.ref.loc13: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc10: type = ptr_type {.j: i32, .k: i32}
+// CHECK:STDOUT:   %cv.var: ref Class = var "cv"
+// CHECK:STDOUT:   %cv: ref Class = bind_name "cv", %cv.var
+// CHECK:STDOUT:   %cv.ref.loc14: ref Class = name_reference "cv", %cv
+// CHECK:STDOUT:   %.loc14_5: ref i32 = class_field_access %cv.ref.loc14, member0
+// CHECK:STDOUT:   %.loc14_10: i32 = int_literal 1
+// CHECK:STDOUT:   assign %.loc14_5, %.loc14_10
+// CHECK:STDOUT:   %cv.ref.loc15: ref Class = name_reference "cv", %cv
+// CHECK:STDOUT:   %.loc15_5: ref i32 = class_field_access %cv.ref.loc15, member1
+// CHECK:STDOUT:   %.loc15_10: i32 = int_literal 2
+// CHECK:STDOUT:   assign %.loc15_5, %.loc15_10
+// CHECK:STDOUT:   %Class.ref.loc16: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %cv.ref.loc16: ref Class = name_reference "cv", %cv
+// CHECK:STDOUT:   %.loc16: Class = bind_value %cv.ref.loc16
+// CHECK:STDOUT:   %c: Class = bind_name "c", %.loc16
+// CHECK:STDOUT:   %c.ref.loc17_10: Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc17_11.1: ref i32 = class_field_access %c.ref.loc17_10, member0
+// CHECK:STDOUT:   %c.ref.loc17_16: Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc17_17.1: ref i32 = class_field_access %c.ref.loc17_16, member1
+// CHECK:STDOUT:   %.loc17_11.2: i32 = bind_value %.loc17_11.1
+// CHECK:STDOUT:   %.loc17_17.2: i32 = bind_value %.loc17_17.1
+// CHECK:STDOUT:   %.loc17_14: i32 = add %.loc17_11.2, %.loc17_17.2
+// CHECK:STDOUT:   return %.loc17_14
+// CHECK:STDOUT: }

+ 46 - 0
toolchain/check/testdata/class/static_method.carbon

@@ -0,0 +1,46 @@
+// 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 Class {
+  fn F() -> i32;
+}
+
+fn Run() -> i32 {
+  var c: Class;
+  return c.F();
+}
+
+// CHECK:STDOUT: file "static_method.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc9: type = struct_type {}
+// CHECK:STDOUT:   %Run: <function> = fn_decl @Run
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Class {
+// CHECK:STDOUT:   %F: <function> = fn_decl @F
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .F = %F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Run() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %Class.ref: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc9: type = tuple_type ()
+// CHECK:STDOUT:   %.loc7: type = ptr_type {}
+// CHECK:STDOUT:   %c.var: ref Class = var "c"
+// CHECK:STDOUT:   %c: ref Class = bind_name "c", %c.var
+// CHECK:STDOUT:   %c.ref: ref Class = name_reference "c", %c
+// CHECK:STDOUT:   %F.ref: <function> = name_reference "F", @Class.%F
+// CHECK:STDOUT:   %.loc13_13.1: init i32 = call %F.ref()
+// CHECK:STDOUT:   %.loc13_13.2: ref i32 = temporary_storage
+// CHECK:STDOUT:   %.loc13_13.3: ref i32 = temporary %.loc13_13.2, %.loc13_13.1
+// CHECK:STDOUT:   %.loc13_13.4: i32 = bind_value %.loc13_13.3
+// CHECK:STDOUT:   return %.loc13_13.4
+// CHECK:STDOUT: }

+ 1 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -137,6 +137,7 @@ CARBON_DIAGNOSTIC_KIND(IncompleteTypeInFunctionParam)
 CARBON_DIAGNOSTIC_KIND(IncompleteTypeInFunctionReturnType)
 CARBON_DIAGNOSTIC_KIND(IncompleteTypeInInitialization)
 CARBON_DIAGNOSTIC_KIND(IncompleteTypeInLetDeclaration)
+CARBON_DIAGNOSTIC_KIND(IncompleteTypeInMemberAccess)
 CARBON_DIAGNOSTIC_KIND(IncompleteTypeInValueConversion)
 CARBON_DIAGNOSTIC_KIND(IncompleteTypeInVarDeclaration)
 CARBON_DIAGNOSTIC_KIND(InvalidArrayExpression)

+ 116 - 78
toolchain/lower/handle.cpp

@@ -166,6 +166,113 @@ auto HandleClassDeclaration(FunctionContext& /*context*/,
   // No action to perform.
 }
 
+// Extracts an element of an aggregate, such as a struct, tuple, or class, by
+// index. Depending on the expression category and value representation of the
+// aggregate input, this will either produce a value or a reference.
+static auto GetAggregateElement(FunctionContext& context,
+                                SemIR::NodeId aggr_node_id,
+                                SemIR::MemberIndex idx,
+                                SemIR::TypeId result_type_id, llvm::Twine name)
+    -> llvm::Value* {
+  auto aggr_node = context.sem_ir().nodes().Get(aggr_node_id);
+  auto* aggr_value = context.GetLocal(aggr_node_id);
+
+  switch (SemIR::GetExpressionCategory(context.sem_ir(), aggr_node_id)) {
+    case SemIR::ExpressionCategory::Error:
+    case SemIR::ExpressionCategory::NotExpression:
+    case SemIR::ExpressionCategory::Initializing:
+    case SemIR::ExpressionCategory::Mixed:
+      CARBON_FATAL() << "Unexpected expression category for aggregate access";
+
+    case SemIR::ExpressionCategory::Value: {
+      auto value_rep =
+          SemIR::GetValueRepresentation(context.sem_ir(), aggr_node.type_id());
+      CARBON_CHECK(value_rep.aggregate_kind !=
+                   SemIR::ValueRepresentation::NotAggregate)
+          << "aggregate type should have aggregate value representation";
+      switch (value_rep.kind) {
+        case SemIR::ValueRepresentation::Unknown:
+          CARBON_FATAL() << "Lowering access to incomplete aggregate type";
+        case SemIR::ValueRepresentation::None:
+          return aggr_value;
+        case SemIR::ValueRepresentation::Copy:
+          // We are holding the values of the aggregate directly, elementwise.
+          return context.builder().CreateExtractValue(aggr_value, idx.index,
+                                                      name);
+        case SemIR::ValueRepresentation::Pointer: {
+          // The value representation is a pointer to an aggregate that we want
+          // to index into.
+          auto pointee_type_id =
+              context.sem_ir().GetPointeeType(value_rep.type_id);
+          auto* value_type = context.GetType(pointee_type_id);
+          auto* elem_ptr = context.builder().CreateStructGEP(
+              value_type, aggr_value, idx.index, name);
+
+          if (!value_rep.elements_are_values()) {
+            // `elem_ptr` points to an object representation, which is our
+            // result.
+            return elem_ptr;
+          }
+
+          // `elem_ptr` points to a value representation. Load it.
+          auto result_value_type_id =
+              SemIR::GetValueRepresentation(context.sem_ir(), result_type_id)
+                  .type_id;
+          return context.builder().CreateLoad(
+              context.GetType(result_value_type_id), elem_ptr, name + ".load");
+        }
+        case SemIR::ValueRepresentation::Custom:
+          CARBON_FATAL()
+              << "Aggregate should never have custom value representation";
+      }
+    }
+
+    case SemIR::ExpressionCategory::DurableReference:
+    case SemIR::ExpressionCategory::EphemeralReference: {
+      // Just locate the aggregate element.
+      auto* aggr_type = context.GetType(aggr_node.type_id());
+      return context.builder().CreateStructGEP(aggr_type, aggr_value, idx.index,
+                                               name);
+    }
+  }
+}
+
+static auto GetStructFieldName(FunctionContext& context,
+                               SemIR::TypeId struct_type_id,
+                               SemIR::MemberIndex index) -> llvm::StringRef {
+  auto fields = context.sem_ir().node_blocks().Get(
+      context.sem_ir()
+          .nodes()
+          .GetAs<SemIR::StructType>(
+              context.sem_ir().types().Get(struct_type_id).node_id)
+          .fields_id);
+  auto field = context.sem_ir().nodes().GetAs<SemIR::StructTypeField>(
+      fields[index.index]);
+  return context.sem_ir().strings().Get(field.name_id);
+}
+
+auto HandleClassFieldAccess(FunctionContext& context, SemIR::NodeId node_id,
+                            SemIR::ClassFieldAccess node) -> void {
+  // Find the class that we're performing access into.
+  auto class_type_id = context.sem_ir().nodes().Get(node.base_id).type_id();
+  auto class_id =
+      context.sem_ir()
+          .nodes()
+          .GetAs<SemIR::ClassType>(
+              context.sem_ir().GetTypeAllowBuiltinTypes(class_type_id))
+          .class_id;
+  auto& class_info = context.sem_ir().classes().Get(class_id);
+
+  // Translate the class field access into a struct access on the object
+  // representation.
+  context.SetLocal(
+      node_id,
+      GetAggregateElement(
+          context, node.base_id, node.index, node.type_id,
+          GetStructFieldName(context, class_info.object_representation_id,
+                             node.index)));
+}
+
 auto HandleDereference(FunctionContext& context, SemIR::NodeId node_id,
                        SemIR::Dereference node) -> void {
   context.SetLocal(node_id, context.GetLocal(node.pointer_id));
@@ -283,82 +390,13 @@ auto HandleStringLiteral(FunctionContext& /*context*/,
   CARBON_FATAL() << "TODO: Add support: " << node;
 }
 
-// Extracts an element of either a struct or a tuple by index. Depending on the
-// expression category of the aggregate input, this will either produce a value
-// or a reference.
-static auto GetStructOrTupleElement(FunctionContext& context,
-                                    SemIR::NodeId aggr_node_id, unsigned idx,
-                                    SemIR::TypeId result_type_id,
-                                    llvm::Twine name) -> llvm::Value* {
-  auto aggr_node = context.sem_ir().nodes().Get(aggr_node_id);
-  auto* aggr_value = context.GetLocal(aggr_node_id);
-
-  switch (SemIR::GetExpressionCategory(context.sem_ir(), aggr_node_id)) {
-    case SemIR::ExpressionCategory::Error:
-    case SemIR::ExpressionCategory::NotExpression:
-    case SemIR::ExpressionCategory::Initializing:
-    case SemIR::ExpressionCategory::Mixed:
-      CARBON_FATAL() << "Unexpected expression category for aggregate access";
-
-    case SemIR::ExpressionCategory::Value: {
-      auto value_rep =
-          SemIR::GetValueRepresentation(context.sem_ir(), aggr_node.type_id());
-      switch (value_rep.kind) {
-        case SemIR::ValueRepresentation::Unknown:
-          CARBON_FATAL() << "Lowering access to incomplete aggregate type";
-        case SemIR::ValueRepresentation::None:
-          return aggr_value;
-        case SemIR::ValueRepresentation::Copy:
-          // We are holding the values of the aggregate directly, elementwise.
-          return context.builder().CreateExtractValue(aggr_value, idx, name);
-        case SemIR::ValueRepresentation::Pointer: {
-          // The value representation is a pointer to an aggregate that we want
-          // to index into.
-          auto pointee_type_id =
-              context.sem_ir().GetPointeeType(value_rep.type_id);
-          auto* value_type = context.GetType(pointee_type_id);
-          auto* elem_ptr = context.builder().CreateStructGEP(
-              value_type, aggr_value, idx, name);
-          auto result_value_type_id =
-              SemIR::GetValueRepresentation(context.sem_ir(), result_type_id)
-                  .type_id;
-          return context.builder().CreateLoad(
-              context.GetType(result_value_type_id), elem_ptr, name + ".load");
-        }
-        case SemIR::ValueRepresentation::Custom:
-          CARBON_FATAL()
-              << "Aggregate should never have custom value representation";
-      }
-    }
-
-    case SemIR::ExpressionCategory::DurableReference:
-    case SemIR::ExpressionCategory::EphemeralReference: {
-      // Just locate the aggregate element.
-      auto* aggr_type = context.GetType(aggr_node.type_id());
-      return context.builder().CreateStructGEP(aggr_type, aggr_value, idx,
-                                               name);
-    }
-  }
-}
-
 auto HandleStructAccess(FunctionContext& context, SemIR::NodeId node_id,
                         SemIR::StructAccess node) -> void {
   auto struct_type_id = context.sem_ir().nodes().Get(node.struct_id).type_id();
-
-  // Get type information for member names.
-  auto fields = context.sem_ir().node_blocks().Get(
-      context.sem_ir()
-          .nodes()
-          .GetAs<SemIR::StructType>(
-              context.sem_ir().types().Get(struct_type_id).node_id)
-          .fields_id);
-  auto field = context.sem_ir().nodes().GetAs<SemIR::StructTypeField>(
-      fields[node.index.index]);
-  auto member_name = context.sem_ir().strings().Get(field.name_id);
-
-  context.SetLocal(node_id, GetStructOrTupleElement(context, node.struct_id,
-                                                    node.index.index,
-                                                    node.type_id, member_name));
+  context.SetLocal(
+      node_id, GetAggregateElement(
+                   context, node.struct_id, node.index, node.type_id,
+                   GetStructFieldName(context, struct_type_id, node.index)));
 }
 
 auto HandleStructLiteral(FunctionContext& /*context*/,
@@ -454,8 +492,8 @@ auto HandleStructTypeField(FunctionContext& /*context*/,
 
 auto HandleTupleAccess(FunctionContext& context, SemIR::NodeId node_id,
                        SemIR::TupleAccess node) -> void {
-  context.SetLocal(
-      node_id, GetStructOrTupleElement(context, node.tuple_id, node.index.index,
+  context.SetLocal(node_id,
+                   GetAggregateElement(context, node.tuple_id, node.index,
                                        node.type_id, "tuple.elem"));
 }
 
@@ -465,9 +503,9 @@ auto HandleTupleIndex(FunctionContext& context, SemIR::NodeId node_id,
       context.sem_ir().nodes().GetAs<SemIR::IntegerLiteral>(node.index_id);
   auto index =
       context.sem_ir().integers().Get(index_node.integer_id).getZExtValue();
-  context.SetLocal(node_id,
-                   GetStructOrTupleElement(context, node.tuple_id, index,
-                                           node.type_id, "tuple.index"));
+  context.SetLocal(node_id, GetAggregateElement(context, node.tuple_id,
+                                                SemIR::MemberIndex(index),
+                                                node.type_id, "tuple.index"));
 }
 
 auto HandleTupleLiteral(FunctionContext& /*context*/, SemIR::NodeId /*node_id*/,

+ 45 - 0
toolchain/lower/testdata/class/field.carbon

@@ -0,0 +1,45 @@
+// 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 C {
+  var a: i32;
+  var b: C*;
+}
+
+fn F(c: C) -> i32 {
+  return (*c.b).a;
+}
+
+fn Run() -> i32 {
+  var c: C;
+  c.a = 1;
+  c.b = &c;
+  return F(c);
+}
+
+// CHECK:STDOUT: ; ModuleID = 'field.carbon'
+// CHECK:STDOUT: source_filename = "field.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @F(ptr %c) {
+// CHECK:STDOUT:   %b = getelementptr inbounds { i32, ptr }, ptr %c, i32 0, i32 1
+// CHECK:STDOUT:   %1 = load ptr, ptr %b, align 8
+// CHECK:STDOUT:   %a = getelementptr inbounds { i32, ptr }, ptr %1, i32 0, i32 0
+// CHECK:STDOUT:   %2 = load i32, ptr %a, align 4
+// CHECK:STDOUT:   ret i32 %2
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @main() {
+// CHECK:STDOUT:   %c = alloca { i32, ptr }, align 8
+// CHECK:STDOUT:   %a = getelementptr inbounds { i32, ptr }, ptr %c, i32 0, i32 0
+// CHECK:STDOUT:   store i32 1, ptr %a, align 4
+// CHECK:STDOUT:   %b = getelementptr inbounds { i32, ptr }, ptr %c, i32 0, i32 1
+// CHECK:STDOUT:   store ptr %c, ptr %b, align 8
+// CHECK:STDOUT:   %F = call i32 @F(ptr %c)
+// CHECK:STDOUT:   %temp = alloca i32, align 4
+// CHECK:STDOUT:   store i32 %F, ptr %temp, align 4
+// CHECK:STDOUT:   %1 = load i32, ptr %temp, align 4
+// CHECK:STDOUT:   ret i32 %1
+// CHECK:STDOUT: }

+ 23 - 0
toolchain/lower/testdata/class/value_access.carbon

@@ -0,0 +1,23 @@
+// 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 C {
+  var a: (i32, i32, i32);
+}
+
+fn F(c: C) -> i32 {
+  return c.a[1];
+}
+
+// CHECK:STDOUT: ; ModuleID = 'value_access.carbon'
+// CHECK:STDOUT: source_filename = "value_access.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @F(ptr %c) {
+// CHECK:STDOUT:   %a = getelementptr inbounds { { i32, i32, i32 } }, ptr %c, i32 0, i32 0
+// CHECK:STDOUT:   %tuple.index = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 1
+// CHECK:STDOUT:   %1 = load i32, ptr %tuple.index, align 4
+// CHECK:STDOUT:   ret i32 %1
+// CHECK:STDOUT: }

+ 17 - 2
toolchain/sem_ir/file.cpp

@@ -192,6 +192,7 @@ static auto GetTypePrecedence(NodeKind kind) -> int {
     case BranchWithArg::Kind:
     case Call::Kind:
     case ClassDeclaration::Kind:
+    case ClassFieldAccess::Kind:
     case Dereference::Kind:
     case Field::Kind:
     case FunctionDeclaration::Kind:
@@ -389,6 +390,7 @@ auto File::StringifyTypeExpression(NodeId outer_node_id,
       case Builtin::Kind:
       case Call::Kind:
       case ClassDeclaration::Kind:
+      case ClassFieldAccess::Kind:
       case CrossReference::Kind:
       case Dereference::Kind:
       case Field::Kind:
@@ -442,6 +444,10 @@ auto File::StringifyTypeExpression(NodeId outer_node_id,
 auto GetExpressionCategory(const File& file, NodeId node_id)
     -> ExpressionCategory {
   const File* ir = &file;
+
+  // The overall expression category if the current node is a value expression.
+  ExpressionCategory value_category = ExpressionCategory::Value;
+
   while (true) {
     auto node = ir->nodes().Get(node_id);
     // clang warns on unhandled enum values; clang-tidy is incorrect here.
@@ -492,13 +498,13 @@ auto GetExpressionCategory(const File& file, NodeId node_id)
       case TupleType::Kind:
       case UnaryOperatorNot::Kind:
       case UnboundFieldType::Kind:
-        return ExpressionCategory::Value;
+        return value_category;
 
       case Builtin::Kind: {
         if (node.As<Builtin>().builtin_kind == BuiltinKind::Error) {
           return ExpressionCategory::Error;
         }
-        return ExpressionCategory::Value;
+        return value_category;
       }
 
       case BindName::Kind: {
@@ -511,6 +517,15 @@ auto GetExpressionCategory(const File& file, NodeId node_id)
         continue;
       }
 
+      case ClassFieldAccess::Kind: {
+        node_id = node.As<ClassFieldAccess>().base_id;
+        // A value of class type is a pointer to an object representation.
+        // Therefore, if the base is a value, the result is an ephemeral
+        // reference.
+        value_category = ExpressionCategory::EphemeralReference;
+        continue;
+      }
+
       case StructAccess::Kind: {
         node_id = node.As<StructAccess>().struct_id;
         continue;

+ 24 - 1
toolchain/sem_ir/file.h

@@ -107,7 +107,7 @@ struct ValueRepresentation : public Printable<ValueRepresentation> {
     // The value representation is a copy of the value. On call boundaries, the
     // value itself will be passed. `type` is the value type.
     Copy,
-    // The value representation is a pointer to an object. When used as a
+    // The value representation is a pointer to the value. When used as a
     // parameter, the argument is a reference expression. `type` is the pointee
     // type.
     Pointer,
@@ -116,8 +116,31 @@ struct ValueRepresentation : public Printable<ValueRepresentation> {
     // TODO: This is not implemented or used yet.
     Custom,
   };
+
+  enum AggregateKind : int8_t {
+    // This type is not an aggregation of other types.
+    NotAggregate,
+    // This type is an aggregate that holds the value representations of its
+    // elements.
+    ValueAggregate,
+    // This type is an aggregate that holds the object representations of its
+    // elements.
+    ObjectAggregate,
+    // This type is an aggregate for which the value and object representation
+    // of all elements are the same, so it effectively holds both.
+    ValueAndObjectAggregate,
+  };
+
+  // Returns whether this is an aggregate that holds its elements by value.
+  auto elements_are_values() const {
+    return aggregate_kind == ValueAggregate ||
+           aggregate_kind == ValueAndObjectAggregate;
+  }
+
   // The kind of value representation used by this type.
   Kind kind = Unknown;
+  // The kind of aggregate representation used by this type.
+  AggregateKind aggregate_kind = AggregateKind::NotAggregate;
   // The type used to model the value representation.
   TypeId type_id = TypeId::Invalid;
 };

+ 1 - 0
toolchain/sem_ir/node_kind.def

@@ -33,6 +33,7 @@ CARBON_SEM_IR_NODE_KIND(BranchWithArg)
 CARBON_SEM_IR_NODE_KIND(Builtin)
 CARBON_SEM_IR_NODE_KIND(Call)
 CARBON_SEM_IR_NODE_KIND(ClassDeclaration)
+CARBON_SEM_IR_NODE_KIND(ClassFieldAccess)
 CARBON_SEM_IR_NODE_KIND(ClassType)
 CARBON_SEM_IR_NODE_KIND(ConstType)
 CARBON_SEM_IR_NODE_KIND(CrossReference)

+ 11 - 0
toolchain/sem_ir/typed_nodes.h

@@ -190,6 +190,16 @@ struct ClassDeclaration {
   NodeBlockId decl_block_id;
 };
 
+struct ClassFieldAccess {
+  static constexpr auto Kind =
+      NodeKind::ClassFieldAccess.Define("class_field_access");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  NodeId base_id;
+  MemberIndex index;
+};
+
 struct ClassType {
   static constexpr auto Kind = NodeKind::ClassType.Define("class_type");
 
@@ -235,6 +245,7 @@ struct Field {
   Parse::Node parse_node;
   TypeId type_id;
   StringId name_id;
+  MemberIndex index;
 };
 
 struct FunctionDeclaration {