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

Support initializing a class from a struct. (#3358)

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

+ 1 - 0
toolchain/check/context.cpp

@@ -666,6 +666,7 @@ class TypeCompleter {
       case SemIR::Call::Kind:
       case SemIR::ClassDeclaration::Kind:
       case SemIR::ClassFieldAccess::Kind:
+      case SemIR::ClassInit::Kind:
       case SemIR::Dereference::Kind:
       case SemIR::Field::Kind:
       case SemIR::FunctionDeclaration::Kind:

+ 93 - 22
toolchain/check/convert.cpp

@@ -29,6 +29,7 @@ static auto FindReturnSlotForInitializer(SemIR::File& sem_ir,
     default:
       CARBON_FATAL() << "Initialization from unexpected inst " << init;
 
+    case SemIR::ClassInit::Kind:
     case SemIR::StructInit::Kind:
     case SemIR::TupleInit::Kind:
       // TODO: Track a return slot for these initializers.
@@ -228,8 +229,9 @@ class CopyOnWriteBlock {
 };
 }  // namespace
 
-// Performs a conversion from a tuple to an array type. Does not perform a
-// final conversion to the requested expression category.
+// Performs a conversion from a tuple to an array type. This function only
+// converts the type, and does not perform a final conversion to the requested
+// expression category.
 static auto ConvertTupleToArray(Context& context, SemIR::TupleType tuple_type,
                                 SemIR::ArrayType array_type,
                                 SemIR::InstId value_id, ConversionTarget target)
@@ -310,8 +312,9 @@ static auto ConvertTupleToArray(Context& context, SemIR::TupleType tuple_type,
                                           sem_ir.inst_blocks().Add(inits)});
 }
 
-// Performs a conversion from a tuple to a tuple type. Does not perform a
-// final conversion to the requested expression category.
+// Performs a conversion from a tuple to a tuple type. This function only
+// converts the type, and does not perform a final conversion to the requested
+// expression category.
 static auto ConvertTupleToTuple(Context& context, SemIR::TupleType src_type,
                                 SemIR::TupleType dest_type,
                                 SemIR::InstId value_id, ConversionTarget target)
@@ -383,12 +386,14 @@ static auto ConvertTupleToTuple(Context& context, SemIR::TupleType src_type,
                                                      new_block.id()});
 }
 
-// Performs a conversion from a struct to a struct type. Does not perform a
-// final conversion to the requested expression category.
-static auto ConvertStructToStruct(Context& context, SemIR::StructType src_type,
-                                  SemIR::StructType dest_type,
-                                  SemIR::InstId value_id,
-                                  ConversionTarget target) -> SemIR::InstId {
+// Common implementation for ConvertStructToStruct and ConvertStructToClass.
+template <typename TargetAccessInstT>
+static auto ConvertStructToStructOrClass(Context& context,
+                                         SemIR::StructType src_type,
+                                         SemIR::StructType dest_type,
+                                         SemIR::InstId value_id,
+                                         ConversionTarget target, bool is_class)
+    -> SemIR::InstId {
   auto& sem_ir = context.sem_ir();
   auto src_elem_fields = sem_ir.inst_blocks().Get(src_type.fields_id);
   auto dest_elem_fields = sem_ir.inst_blocks().Get(dest_type.fields_id);
@@ -412,11 +417,13 @@ static auto ConvertStructToStruct(Context& context, SemIR::StructType src_type,
   // exist in the destination or vice versa in the diagnostic.
   if (src_elem_fields.size() != dest_elem_fields.size()) {
     CARBON_DIAGNOSTIC(StructInitElementCountMismatch, Error,
-                      "Cannot initialize struct of {0} element(s) from struct "
-                      "with {1} element(s).",
-                      size_t, size_t);
-    context.emitter().Emit(value.parse_node(), StructInitElementCountMismatch,
-                           dest_elem_fields.size(), src_elem_fields.size());
+                      "Cannot initialize {0} with {1} field(s) from struct "
+                      "with {2} field(s).",
+                      llvm::StringLiteral, size_t, size_t);
+    context.emitter().Emit(
+        value.parse_node(), StructInitElementCountMismatch,
+        is_class ? llvm::StringLiteral("class") : llvm::StringLiteral("struct"),
+        dest_elem_fields.size(), src_elem_fields.size());
     return SemIR::InstId::BuiltinError;
   }
 
@@ -484,7 +491,7 @@ static auto ConvertStructToStruct(Context& context, SemIR::StructType src_type,
     // TODO: This call recurses back into conversion. Switch to an iterative
     // approach.
     auto init_id =
-        ConvertAggregateElement<SemIR::StructAccess, SemIR::StructAccess>(
+        ConvertAggregateElement<SemIR::StructAccess, TargetAccessInstT>(
             context, value.parse_node(), value_id, src_field.field_type_id,
             literal_elems, inner_kind, target.init_id, dest_field.field_type_id,
             target.init_block, src_field_index);
@@ -494,12 +501,63 @@ static auto ConvertStructToStruct(Context& context, SemIR::StructType src_type,
     new_block.Set(i, init_id);
   }
 
-  return is_init ? context.AddInst(SemIR::StructInit{value.parse_node(),
-                                                     target.type_id, value_id,
-                                                     new_block.id()})
-                 : context.AddInst(SemIR::StructValue{value.parse_node(),
-                                                      target.type_id, value_id,
-                                                      new_block.id()});
+  if (is_class) {
+    CARBON_CHECK(is_init)
+        << "Converting directly to a class value is not supported";
+    return context.AddInst(SemIR::ClassInit{value.parse_node(), target.type_id,
+                                            value_id, new_block.id()});
+  } else if (is_init) {
+    return context.AddInst(SemIR::StructInit{value.parse_node(), target.type_id,
+                                             value_id, new_block.id()});
+  } else {
+    return context.AddInst(SemIR::StructValue{
+        value.parse_node(), target.type_id, value_id, new_block.id()});
+  }
+}
+
+// Performs a conversion from a struct to a struct type. This function only
+// converts the type, and does not perform a final conversion to the requested
+// expression category.
+static auto ConvertStructToStruct(Context& context, SemIR::StructType src_type,
+                                  SemIR::StructType dest_type,
+                                  SemIR::InstId value_id,
+                                  ConversionTarget target) -> SemIR::InstId {
+  return ConvertStructToStructOrClass<SemIR::StructAccess>(
+      context, src_type, dest_type, value_id, target, /*is_class=*/false);
+}
+
+// Performs a conversion from a struct to a class type. This function only
+// converts the type, and does not perform a final conversion to the requested
+// expression category.
+static auto ConvertStructToClass(Context& context, SemIR::StructType src_type,
+                                 SemIR::ClassType dest_type,
+                                 SemIR::InstId value_id,
+                                 ConversionTarget target) -> SemIR::InstId {
+  PendingBlock target_block(context);
+  auto dest_struct_type = context.insts().GetAs<SemIR::StructType>(
+      context.sem_ir().GetTypeAllowBuiltinTypes(
+          context.classes().Get(dest_type.class_id).object_representation_id));
+
+  // If we're trying to create a class value, form a temporary for the value to
+  // point to.
+  bool need_temporary = !target.is_initializer();
+  if (need_temporary) {
+    target.kind = ConversionTarget::Initializer;
+    target.init_block = &target_block;
+    target.init_id = target_block.AddInst(SemIR::TemporaryStorage{
+        context.insts().Get(value_id).parse_node(), target.type_id});
+  }
+
+  auto result_id = ConvertStructToStructOrClass<SemIR::ClassFieldAccess>(
+      context, src_type, dest_struct_type, value_id, target, /*is_class=*/true);
+
+  if (need_temporary) {
+    target_block.InsertHere();
+    result_id = context.AddInst(
+        SemIR::Temporary{context.insts().Get(value_id).parse_node(),
+                         target.type_id, target.init_id, result_id});
+  }
+  return result_id;
 }
 
 // Returns whether `category` is a valid expression category to produce as a
@@ -622,6 +680,19 @@ static auto PerformBuiltinConversion(Context& context, Parse::Node parse_node,
     }
   }
 
+  // A struct {.f_1: T_1, .f_2: T_2, ..., .f_n: T_n} converts to a class type
+  // if it converts to the struct type that is the class's representation type
+  // (a struct with the same fields as the class, plus a base field where
+  // relevant).
+  if (auto target_class_type = target_type_inst.TryAs<SemIR::ClassType>()) {
+    auto value_type_inst =
+        sem_ir.insts().Get(sem_ir.GetTypeAllowBuiltinTypes(value_type_id));
+    if (auto src_struct_type = value_type_inst.TryAs<SemIR::StructType>()) {
+      return ConvertStructToClass(context, *src_struct_type, *target_class_type,
+                                  value_id, target);
+    }
+  }
+
   if (target.type_id == SemIR::TypeId::TypeType) {
     // A tuple of types converts to type `type`.
     // TODO: This should apply even for non-literal tuples.

+ 1 - 1
toolchain/check/convert.h

@@ -41,7 +41,7 @@ struct ConversionTarget {
   // For an initializer, the object being initialized.
   SemIR::InstId init_id = SemIR::InstId::Invalid;
   // For an initializer, a block of pending instructions that are needed to
-  // form the value of `target_id`, and that can be discarded if no
+  // form the value of `init_id`, and that can be discarded if no
   // initialization is needed.
   PendingBlock* init_block = nullptr;
 

+ 74 - 0
toolchain/check/testdata/class/fail_init.carbon

@@ -0,0 +1,74 @@
+// 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 a: i32;
+  var b: i32;
+}
+
+fn F() {
+  // CHECK:STDERR: fail_init.carbon:[[@LINE+3]]:10: ERROR: Cannot initialize class with 2 field(s) from struct with 1 field(s).
+  // CHECK:STDERR:   {.a = 1} as Class;
+  // CHECK:STDERR:          ^
+  {.a = 1} as Class;
+  // CHECK:STDERR: fail_init.carbon:[[@LINE+3]]:18: ERROR: Missing value for field `b` in struct initialization.
+  // CHECK:STDERR:   {.a = 1, .c = 2} as Class;
+  // CHECK:STDERR:                  ^
+  {.a = 1, .c = 2} as Class;
+  // CHECK:STDERR: fail_init.carbon:[[@LINE+3]]:26: ERROR: Cannot initialize class with 2 field(s) from struct with 3 field(s).
+  // CHECK:STDERR:   {.a = 1, .b = 2, .c = 3} as Class;
+  // CHECK:STDERR:                          ^
+  {.a = 1, .b = 2, .c = 3} as Class;
+}
+
+// CHECK:STDOUT: file "fail_init.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc10: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %F: <function> = fn_decl @F
+// 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 "a", member0
+// CHECK:STDOUT:   %a: <unbound field of class Class> = bind_name "a", %.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 "b", member1
+// CHECK:STDOUT:   %b: <unbound field of class Class> = bind_name "b", %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .a = %a
+// CHECK:STDOUT:   .b = %b
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc16_9: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc16_10.1: type = struct_type {.a: i32}
+// CHECK:STDOUT:   %.loc16_10.2: {.a: i32} = struct_literal (%.loc16_9)
+// CHECK:STDOUT:   %Class.ref.loc16: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc10: type = ptr_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %.loc16_10.3: ref Class = temporary_storage
+// CHECK:STDOUT:   %.loc16_10.4: ref Class = temporary %.loc16_10.3, <error>
+// CHECK:STDOUT:   %.loc20_9: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc20_17: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc20_18.1: type = struct_type {.a: i32, .c: i32}
+// CHECK:STDOUT:   %.loc20_18.2: {.a: i32, .c: i32} = struct_literal (%.loc20_9, %.loc20_17)
+// CHECK:STDOUT:   %Class.ref.loc20: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc20_18.3: ref Class = temporary_storage
+// CHECK:STDOUT:   %.loc20_18.4: ref i32 = class_field_access %.loc20_18.3, member0
+// CHECK:STDOUT:   %.loc20_18.5: init i32 = initialize_from %.loc20_9 to %.loc20_18.4
+// CHECK:STDOUT:   %.loc20_18.6: ref Class = temporary %.loc20_18.3, <error>
+// CHECK:STDOUT:   %.loc24_9: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc24_17: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc24_25: i32 = int_literal 3
+// CHECK:STDOUT:   %.loc24_26.1: type = struct_type {.a: i32, .b: i32, .c: i32}
+// CHECK:STDOUT:   %.loc24_26.2: {.a: i32, .b: i32, .c: i32} = struct_literal (%.loc24_9, %.loc24_17, %.loc24_25)
+// CHECK:STDOUT:   %Class.ref.loc24: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc24_26.3: ref Class = temporary_storage
+// CHECK:STDOUT:   %.loc24_26.4: ref Class = temporary %.loc24_26.3, <error>
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 73 - 0
toolchain/check/testdata/class/fail_init_as_inplace.carbon

@@ -0,0 +1,73 @@
+// 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 a: i32;
+  var b: i32;
+}
+
+fn G(p: Class*);
+
+fn F() {
+  // TODO: This case should presumably work: `{...} as Class` should be an
+  // initializing expression, not a value expression.
+  //
+  // CHECK:STDERR: fail_init_as_inplace.carbon:[[@LINE+3]]:33: ERROR: Cannot copy value of type `Class`.
+  // CHECK:STDERR:   var c: Class = {.a = 1, .b = 2} as Class;
+  // CHECK:STDERR:                                 ^
+  var c: Class = {.a = 1, .b = 2} as Class;
+  G(&c);
+}
+
+// CHECK:STDOUT: file "fail_init_as_inplace.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc10: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %G: <function> = fn_decl @G
+// CHECK:STDOUT:   %F: <function> = fn_decl @F
+// 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 "a", member0
+// CHECK:STDOUT:   %a: <unbound field of class Class> = bind_name "a", %.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 "b", member1
+// CHECK:STDOUT:   %b: <unbound field of class Class> = bind_name "b", %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .a = %a
+// CHECK:STDOUT:   .b = %b
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G(%p: Class*);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %Class.ref.loc21_10: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc10: type = ptr_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %c.var: ref Class = var "c"
+// CHECK:STDOUT:   %c: ref Class = bind_name "c", %c.var
+// CHECK:STDOUT:   %.loc21_24: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc21_32: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc21_33.1: {.a: i32, .b: i32} = struct_literal (%.loc21_24, %.loc21_32)
+// CHECK:STDOUT:   %Class.ref.loc21_38: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc21_33.2: ref Class = temporary_storage
+// CHECK:STDOUT:   %.loc21_33.3: ref i32 = class_field_access %.loc21_33.2, member0
+// CHECK:STDOUT:   %.loc21_33.4: init i32 = initialize_from %.loc21_24 to %.loc21_33.3
+// CHECK:STDOUT:   %.loc21_33.5: ref i32 = class_field_access %.loc21_33.2, member1
+// CHECK:STDOUT:   %.loc21_33.6: init i32 = initialize_from %.loc21_32 to %.loc21_33.5
+// CHECK:STDOUT:   %.loc21_33.7: init Class = class_init %.loc21_33.1, (%.loc21_33.4, %.loc21_33.6)
+// CHECK:STDOUT:   %.loc21_33.8: ref Class = temporary %.loc21_33.2, %.loc21_33.7
+// CHECK:STDOUT:   %.loc21_33.9: Class = bind_value %.loc21_33.8
+// CHECK:STDOUT:   assign %c.var, <error>
+// CHECK:STDOUT:   %G.ref: <function> = name_reference "G", file.%G
+// CHECK:STDOUT:   %c.ref: ref Class = name_reference "c", %c
+// CHECK:STDOUT:   %.loc22_5: Class* = address_of %c.ref
+// CHECK:STDOUT:   %.loc22_4.1: type = tuple_type ()
+// CHECK:STDOUT:   %.loc22_4.2: init () = call %G.ref(%.loc22_5)
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 69 - 0
toolchain/check/testdata/class/init.carbon

@@ -0,0 +1,69 @@
+// 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;
+  var next: Class*;
+}
+
+fn Make(n: i32, next: Class*) -> Class {
+  return {.n = n, .next = next};
+}
+
+fn MakeReorder(n: i32, next: Class*) -> Class {
+  return {.next = next, .n = n};
+}
+
+// CHECK:STDOUT: file "init.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc10_1.1: type = struct_type {.n: i32, .next: Class*}
+// CHECK:STDOUT:   %.loc10_1.2: type = ptr_type {.n: i32, .next: Class*}
+// CHECK:STDOUT:   %Make: <function> = fn_decl @Make
+// CHECK:STDOUT:   %MakeReorder: <function> = fn_decl @MakeReorder
+// 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:   %Class.ref: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc9_18: type = ptr_type Class
+// CHECK:STDOUT:   %.loc9_11.1: type = unbound_field_type Class, Class*
+// CHECK:STDOUT:   %.loc9_11.2: <unbound field of class Class> = field "next", member1
+// CHECK:STDOUT:   %next: <unbound field of class Class> = bind_name "next", %.loc9_11.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .n = %n
+// CHECK:STDOUT:   .next = %next
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Make(%n: i32, %next: Class*) -> %return: Class {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %n.ref: i32 = name_reference "n", %n
+// CHECK:STDOUT:   %next.ref: Class* = name_reference "next", %next
+// CHECK:STDOUT:   %.loc13_31.1: {.n: i32, .next: Class*} = struct_literal (%n.ref, %next.ref)
+// CHECK:STDOUT:   %.loc13_31.2: ref i32 = class_field_access %return, member0
+// CHECK:STDOUT:   %.loc13_31.3: init i32 = initialize_from %n.ref to %.loc13_31.2
+// CHECK:STDOUT:   %.loc13_31.4: ref Class* = class_field_access %return, member1
+// CHECK:STDOUT:   %.loc13_31.5: init Class* = initialize_from %next.ref to %.loc13_31.4
+// CHECK:STDOUT:   %.loc13_31.6: init Class = class_init %.loc13_31.1, (%.loc13_31.3, %.loc13_31.5)
+// CHECK:STDOUT:   return %.loc13_31.6
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @MakeReorder(%n: i32, %next: Class*) -> %return: Class {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %next.ref: Class* = name_reference "next", %next
+// CHECK:STDOUT:   %n.ref: i32 = name_reference "n", %n
+// CHECK:STDOUT:   %.loc17_31.1: type = struct_type {.next: Class*, .n: i32}
+// CHECK:STDOUT:   %.loc17_31.2: {.next: Class*, .n: i32} = struct_literal (%next.ref, %n.ref)
+// CHECK:STDOUT:   %.loc17_31.3: ref i32 = class_field_access %return, member1
+// CHECK:STDOUT:   %.loc17_31.4: init i32 = initialize_from %n.ref to %.loc17_31.3
+// CHECK:STDOUT:   %.loc17_31.5: ref Class* = class_field_access %return, member0
+// CHECK:STDOUT:   %.loc17_31.6: init Class* = initialize_from %next.ref to %.loc17_31.5
+// CHECK:STDOUT:   %.loc17_31.7: init Class = class_init %.loc17_31.2, (%.loc17_31.4, %.loc17_31.6)
+// CHECK:STDOUT:   return %.loc17_31.7
+// CHECK:STDOUT: }

+ 53 - 0
toolchain/check/testdata/class/init_as.carbon

@@ -0,0 +1,53 @@
+// 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 a: i32;
+  var b: i32;
+}
+
+fn F() -> i32 {
+  return ({.a = 1, .b = 2} as Class).a;
+}
+
+// CHECK:STDOUT: file "init_as.carbon" {
+// CHECK:STDOUT:   class_declaration @Class, ()
+// CHECK:STDOUT:   %Class: type = class_type @Class
+// CHECK:STDOUT:   %.loc10: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %F: <function> = fn_decl @F
+// 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 "a", member0
+// CHECK:STDOUT:   %a: <unbound field of class Class> = bind_name "a", %.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 "b", member1
+// CHECK:STDOUT:   %b: <unbound field of class Class> = bind_name "b", %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .a = %a
+// CHECK:STDOUT:   .b = %b
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc13_17: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc13_25: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc13_26.1: {.a: i32, .b: i32} = struct_literal (%.loc13_17, %.loc13_25)
+// CHECK:STDOUT:   %Class.ref: type = name_reference "Class", file.%Class
+// CHECK:STDOUT:   %.loc10: type = ptr_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %.loc13_26.2: ref Class = temporary_storage
+// CHECK:STDOUT:   %.loc13_26.3: ref i32 = class_field_access %.loc13_26.2, member0
+// CHECK:STDOUT:   %.loc13_26.4: init i32 = initialize_from %.loc13_17 to %.loc13_26.3
+// CHECK:STDOUT:   %.loc13_26.5: ref i32 = class_field_access %.loc13_26.2, member1
+// CHECK:STDOUT:   %.loc13_26.6: init i32 = initialize_from %.loc13_25 to %.loc13_26.5
+// CHECK:STDOUT:   %.loc13_26.7: init Class = class_init %.loc13_26.1, (%.loc13_26.4, %.loc13_26.6)
+// CHECK:STDOUT:   %.loc13_26.8: ref Class = temporary %.loc13_26.2, %.loc13_26.7
+// CHECK:STDOUT:   %.loc13_37.1: ref i32 = class_field_access %.loc13_26.8, member0
+// CHECK:STDOUT:   %.loc13_37.2: i32 = bind_value %.loc13_37.1
+// CHECK:STDOUT:   return %.loc13_37.2
+// CHECK:STDOUT: }

+ 79 - 0
toolchain/check/testdata/class/init_nested.carbon

@@ -0,0 +1,79 @@
+// 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 Inner {
+  var a: i32;
+  var b: i32;
+}
+
+fn MakeInner() -> Inner;
+
+class Outer {
+  var c: Inner;
+  var d: Inner;
+}
+
+fn MakeOuter() -> Outer {
+  return {.c = MakeInner(), .d = MakeInner()};
+}
+
+// CHECK:STDOUT: file "init_nested.carbon" {
+// CHECK:STDOUT:   class_declaration @Inner, ()
+// CHECK:STDOUT:   %Inner: type = class_type @Inner
+// CHECK:STDOUT:   %.loc10_1.1: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %.loc10_1.2: type = ptr_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %MakeInner: <function> = fn_decl @MakeInner
+// CHECK:STDOUT:   class_declaration @Outer, ()
+// CHECK:STDOUT:   %Outer: type = class_type @Outer
+// CHECK:STDOUT:   %.loc17_1.1: type = struct_type {.c: Inner, .d: Inner}
+// CHECK:STDOUT:   %.loc17_1.2: type = struct_type {.c: {.a: i32, .b: i32}*, .d: {.a: i32, .b: i32}*}
+// CHECK:STDOUT:   %.loc17_1.3: type = ptr_type {.c: {.a: i32, .b: i32}*, .d: {.a: i32, .b: i32}*}
+// CHECK:STDOUT:   %.loc14: type = ptr_type {.c: Inner, .d: Inner}
+// CHECK:STDOUT:   %MakeOuter: <function> = fn_decl @MakeOuter
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Inner {
+// CHECK:STDOUT:   %.loc8_8.1: type = unbound_field_type Inner, i32
+// CHECK:STDOUT:   %.loc8_8.2: <unbound field of class Inner> = field "a", member0
+// CHECK:STDOUT:   %a: <unbound field of class Inner> = bind_name "a", %.loc8_8.2
+// CHECK:STDOUT:   %.loc9_8.1: type = unbound_field_type Inner, i32
+// CHECK:STDOUT:   %.loc9_8.2: <unbound field of class Inner> = field "b", member1
+// CHECK:STDOUT:   %b: <unbound field of class Inner> = bind_name "b", %.loc9_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .a = %a
+// CHECK:STDOUT:   .b = %b
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: class @Outer {
+// CHECK:STDOUT:   %Inner.ref.loc15: type = name_reference "Inner", file.%Inner
+// CHECK:STDOUT:   %.loc15_8.1: type = unbound_field_type Outer, Inner
+// CHECK:STDOUT:   %.loc15_8.2: <unbound field of class Outer> = field "c", member0
+// CHECK:STDOUT:   %c: <unbound field of class Outer> = bind_name "c", %.loc15_8.2
+// CHECK:STDOUT:   %Inner.ref.loc16: type = name_reference "Inner", file.%Inner
+// CHECK:STDOUT:   %.loc16_8.1: type = unbound_field_type Outer, Inner
+// CHECK:STDOUT:   %.loc16_8.2: <unbound field of class Outer> = field "d", member1
+// CHECK:STDOUT:   %d: <unbound field of class Outer> = bind_name "d", %.loc16_8.2
+// CHECK:STDOUT:
+// CHECK:STDOUT: !members:
+// CHECK:STDOUT:   .c = %c
+// CHECK:STDOUT:   .d = %d
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @MakeInner() -> %return: Inner;
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @MakeOuter() -> %return: Outer {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %MakeInner.ref.loc20_16: <function> = name_reference "MakeInner", file.%MakeInner
+// CHECK:STDOUT:   %.loc20_45.1: ref Inner = class_field_access %return, member0
+// CHECK:STDOUT:   %.loc20_25: init Inner = call %MakeInner.ref.loc20_16() to %.loc20_45.1
+// CHECK:STDOUT:   %MakeInner.ref.loc20_34: <function> = name_reference "MakeInner", file.%MakeInner
+// CHECK:STDOUT:   %.loc20_45.2: ref Inner = class_field_access %return, member1
+// CHECK:STDOUT:   %.loc20_43: init Inner = call %MakeInner.ref.loc20_34() to %.loc20_45.2
+// CHECK:STDOUT:   %.loc20_45.3: {.c: Inner, .d: Inner} = struct_literal (%.loc20_25, %.loc20_43)
+// CHECK:STDOUT:   %.loc20_45.4: init Outer = class_init %.loc20_45.3, (%.loc20_25, %.loc20_43)
+// CHECK:STDOUT:   return %.loc20_45.4
+// CHECK:STDOUT: }

+ 1 - 1
toolchain/check/testdata/struct/fail_assign_empty.carbon

@@ -4,7 +4,7 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_empty.carbon:[[@LINE+3]]:21: ERROR: Cannot initialize struct of 1 element(s) from struct with 0 element(s).
+// CHECK:STDERR: fail_assign_empty.carbon:[[@LINE+3]]:21: ERROR: Cannot initialize struct with 1 field(s) from struct with 0 field(s).
 // CHECK:STDERR: var x: {.a: i32} = {};
 // CHECK:STDERR:                     ^
 var x: {.a: i32} = {};

+ 1 - 1
toolchain/check/testdata/struct/fail_assign_to_empty.carbon

@@ -4,7 +4,7 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_to_empty.carbon:[[@LINE+3]]:20: ERROR: Cannot initialize struct of 0 element(s) from struct with 1 element(s).
+// CHECK:STDERR: fail_assign_to_empty.carbon:[[@LINE+3]]:20: ERROR: Cannot initialize struct with 0 field(s) from struct with 1 field(s).
 // CHECK:STDERR: var x: {} = {.a = 1};
 // CHECK:STDERR:                    ^
 var x: {} = {.a = 1};

+ 1 - 1
toolchain/check/testdata/struct/fail_too_few_values.carbon

@@ -4,7 +4,7 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_too_few_values.carbon:[[@LINE+3]]:36: ERROR: Cannot initialize struct of 2 element(s) from struct with 1 element(s).
+// CHECK:STDERR: fail_too_few_values.carbon:[[@LINE+3]]:36: ERROR: Cannot initialize struct with 2 field(s) from struct with 1 field(s).
 // CHECK:STDERR: var x: {.a: i32, .b: i32} = {.a = 1};
 // CHECK:STDERR:                                    ^
 var x: {.a: i32, .b: i32} = {.a = 1};

+ 48 - 43
toolchain/lower/handle.cpp

@@ -280,6 +280,39 @@ auto HandleClassFieldAccess(FunctionContext& context, SemIR::InstId inst_id,
                              inst.index)));
 }
 
+static auto EmitAggregateInitializer(FunctionContext& context,
+                                     SemIR::TypeId type_id,
+                                     SemIR::InstBlockId refs_id,
+                                     llvm::Twine name) -> llvm::Value* {
+  auto* llvm_type = context.GetType(type_id);
+
+  switch (
+      SemIR::GetInitializingRepresentation(context.sem_ir(), type_id).kind) {
+    case SemIR::InitializingRepresentation::None:
+    case SemIR::InitializingRepresentation::InPlace:
+      // TODO: Add a helper to poison a value slot.
+      return llvm::PoisonValue::get(llvm_type);
+
+    case SemIR::InitializingRepresentation::ByCopy: {
+      auto refs = context.sem_ir().inst_blocks().Get(refs_id);
+      CARBON_CHECK(refs.size() == 1)
+          << "Unexpected size for aggregate with by-copy value representation";
+      // TODO: Remove the LLVM StructType wrapper in this case, so we don't
+      // need this `insert_value` wrapping.
+      return context.builder().CreateInsertValue(
+          llvm::PoisonValue::get(llvm_type), context.GetLocal(refs[0]), {0},
+          name);
+    }
+  }
+}
+
+auto HandleClassInit(FunctionContext& context, SemIR::InstId inst_id,
+                     SemIR::ClassInit inst) -> void {
+  context.SetLocal(
+      inst_id, EmitAggregateInitializer(context, inst.type_id, inst.elements_id,
+                                        "class.init"));
+}
+
 auto HandleDereference(FunctionContext& context, SemIR::InstId inst_id,
                        SemIR::Dereference inst) -> void {
   context.SetLocal(inst_id, context.GetLocal(inst.pointer_id));
@@ -413,10 +446,10 @@ auto HandleStructLiteral(FunctionContext& /*context*/,
 
 // Emits the value representation for a struct or tuple whose elements are the
 // contents of `refs_id`.
-auto EmitStructOrTupleValueRepresentation(FunctionContext& context,
-                                          SemIR::TypeId type_id,
-                                          SemIR::InstBlockId refs_id,
-                                          llvm::Twine name) -> llvm::Value* {
+auto EmitAggregateValueRepresentation(FunctionContext& context,
+                                      SemIR::TypeId type_id,
+                                      SemIR::InstBlockId refs_id,
+                                      llvm::Twine name) -> llvm::Value* {
   auto value_rep = SemIR::GetValueRepresentation(context.sem_ir(), type_id);
   switch (value_rep.kind) {
     case SemIR::ValueRepresentation::Unknown:
@@ -463,30 +496,16 @@ auto EmitStructOrTupleValueRepresentation(FunctionContext& context,
 
 auto HandleStructInit(FunctionContext& context, SemIR::InstId inst_id,
                       SemIR::StructInit inst) -> void {
-  auto* llvm_type = context.GetType(inst.type_id);
-
-  switch (SemIR::GetInitializingRepresentation(context.sem_ir(), inst.type_id)
-              .kind) {
-    case SemIR::InitializingRepresentation::None:
-    case SemIR::InitializingRepresentation::InPlace:
-      // TODO: Add a helper to poison a value slot.
-      context.SetLocal(inst_id, llvm::PoisonValue::get(llvm_type));
-      break;
-
-    case SemIR::InitializingRepresentation::ByCopy: {
-      context.SetLocal(
-          inst_id, EmitStructOrTupleValueRepresentation(
-                       context, inst.type_id, inst.elements_id, "struct.init"));
-      break;
-    }
-  }
+  context.SetLocal(
+      inst_id, EmitAggregateInitializer(context, inst.type_id, inst.elements_id,
+                                        "struct.init"));
 }
 
 auto HandleStructValue(FunctionContext& context, SemIR::InstId inst_id,
                        SemIR::StructValue inst) -> void {
-  context.SetLocal(inst_id,
-                   EmitStructOrTupleValueRepresentation(
-                       context, inst.type_id, inst.elements_id, "struct"));
+  context.SetLocal(
+      inst_id, EmitAggregateValueRepresentation(context, inst.type_id,
+                                                inst.elements_id, "struct"));
 }
 
 auto HandleStructTypeField(FunctionContext& /*context*/,
@@ -521,29 +540,15 @@ auto HandleTupleLiteral(FunctionContext& /*context*/, SemIR::InstId /*inst_id*/,
 
 auto HandleTupleInit(FunctionContext& context, SemIR::InstId inst_id,
                      SemIR::TupleInit inst) -> void {
-  auto* llvm_type = context.GetType(inst.type_id);
-
-  switch (SemIR::GetInitializingRepresentation(context.sem_ir(), inst.type_id)
-              .kind) {
-    case SemIR::InitializingRepresentation::None:
-    case SemIR::InitializingRepresentation::InPlace:
-      // TODO: Add a helper to poison a value slot.
-      context.SetLocal(inst_id, llvm::PoisonValue::get(llvm_type));
-      break;
-
-    case SemIR::InitializingRepresentation::ByCopy: {
-      context.SetLocal(
-          inst_id, EmitStructOrTupleValueRepresentation(
-                       context, inst.type_id, inst.elements_id, "tuple.init"));
-      break;
-    }
-  }
+  context.SetLocal(
+      inst_id, EmitAggregateInitializer(context, inst.type_id, inst.elements_id,
+                                        "tuple.init"));
 }
 
 auto HandleTupleValue(FunctionContext& context, SemIR::InstId inst_id,
                       SemIR::TupleValue inst) -> void {
-  context.SetLocal(
-      inst_id, EmitStructOrTupleValueRepresentation(context, inst.type_id,
+  context.SetLocal(inst_id,
+                   EmitAggregateValueRepresentation(context, inst.type_id,
                                                     inst.elements_id, "tuple"));
 }
 

+ 2 - 2
toolchain/lower/testdata/struct/one_entry.carbon

@@ -19,7 +19,7 @@ fn Run() -> i32 {
 // CHECK:STDOUT:   %y = alloca { i32 }, align 8
 // CHECK:STDOUT:   %a = getelementptr inbounds { i32 }, ptr %x, i32 0, i32 0
 // CHECK:STDOUT:   %1 = load i32, ptr %a, align 4
-// CHECK:STDOUT:   %2 = insertvalue { i32 } poison, i32 %1, 0
-// CHECK:STDOUT:   store { i32 } %2, ptr %y, align 4
+// CHECK:STDOUT:   %struct.init = insertvalue { i32 } poison, i32 %1, 0
+// CHECK:STDOUT:   store { i32 } %struct.init, ptr %y, align 4
 // CHECK:STDOUT:   ret i32 0
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/lower/testdata/tuple/one_entry.carbon

@@ -19,7 +19,7 @@ fn Run() -> i32 {
 // CHECK:STDOUT:   %y = alloca { i32 }, align 8
 // CHECK:STDOUT:   %tuple.elem = getelementptr inbounds { i32 }, ptr %x, i32 0, i32 0
 // CHECK:STDOUT:   %1 = load i32, ptr %tuple.elem, align 4
-// CHECK:STDOUT:   %2 = insertvalue { i32 } poison, i32 %1, 0
-// CHECK:STDOUT:   store { i32 } %2, ptr %y, align 4
+// CHECK:STDOUT:   %tuple.init = insertvalue { i32 } poison, i32 %1, 0
+// CHECK:STDOUT:   store { i32 } %tuple.init, ptr %y, align 4
 // CHECK:STDOUT:   ret i32 0
 // CHECK:STDOUT: }

+ 3 - 0
toolchain/sem_ir/file.cpp

@@ -198,6 +198,7 @@ static auto GetTypePrecedence(InstKind kind) -> int {
     case Call::Kind:
     case ClassDeclaration::Kind:
     case ClassFieldAccess::Kind:
+    case ClassInit::Kind:
     case Dereference::Kind:
     case Field::Kind:
     case FunctionDeclaration::Kind:
@@ -399,6 +400,7 @@ auto File::StringifyTypeExpression(InstId outer_inst_id,
       case Call::Kind:
       case ClassDeclaration::Kind:
       case ClassFieldAccess::Kind:
+      case ClassInit::Kind:
       case CrossReference::Kind:
       case Dereference::Kind:
       case Field::Kind:
@@ -567,6 +569,7 @@ auto GetExpressionCategory(const File& file, InstId inst_id)
       case ArrayInit::Kind:
       case Call::Kind:
       case InitializeFrom::Kind:
+      case ClassInit::Kind:
       case StructInit::Kind:
       case TupleInit::Kind:
         return ExpressionCategory::Initializing;

+ 1 - 0
toolchain/sem_ir/inst_kind.def

@@ -35,6 +35,7 @@ CARBON_SEM_IR_INST_KIND(Builtin)
 CARBON_SEM_IR_INST_KIND(Call)
 CARBON_SEM_IR_INST_KIND(ClassDeclaration)
 CARBON_SEM_IR_INST_KIND(ClassFieldAccess)
+CARBON_SEM_IR_INST_KIND(ClassInit)
 CARBON_SEM_IR_INST_KIND(ClassType)
 CARBON_SEM_IR_INST_KIND(ConstType)
 CARBON_SEM_IR_INST_KIND(CrossReference)

+ 9 - 0
toolchain/sem_ir/typed_insts.h

@@ -218,6 +218,15 @@ struct ClassFieldAccess {
   MemberIndex index;
 };
 
+struct ClassInit {
+  static constexpr auto Kind = InstKind::ClassInit.Define("class_init");
+
+  Parse::Node parse_node;
+  TypeId type_id;
+  InstId src_id;
+  InstBlockId elements_id;
+};
+
 struct ClassType {
   static constexpr auto Kind = InstKind::ClassType.Define("class_type");