Bläddra i källkod

Compute type layouts in SemIR / Check (#7066)

Instead of allowing lower to pick whatever type layout it desires,
compute the layouts of types as part of completing the type, and make
lower build types that match that representation.

For now we assume that all pointers are 64-bit, since we don't have
access to target information. We allow tail padding reuse for structs
and tuple types (and by extension, for classes, since they use structs
as their object representation), but not for arrays.

In order to build matching LLVM types, we create LLVM packed structs
where necessary, and we insert inter-field padding on the end of the
previous field so that GEP indexes still always match Carbon's
ElementIndexes.

We don't yet use the computed alignment much in LLVM IR generation -- in
particular, `alloca`s, `load`s, and `store`s should probably use the
computed type alignment, but don't.

Assisted-by: Gemini via Antigravity
Richard Smith 2 veckor sedan
förälder
incheckning
a6061d975c
33 ändrade filer med 1456 tillägg och 190 borttagningar
  1. 1 1
      toolchain/check/convert.cpp
  2. 8 6
      toolchain/check/cpp/import.cpp
  3. 1 1
      toolchain/check/custom_witness.cpp
  4. 12 0
      toolchain/check/testdata/basics/raw_sem_ir/builtins.carbon
  5. 51 0
      toolchain/check/testdata/basics/raw_sem_ir/cpp_interop.carbon
  6. 39 0
      toolchain/check/testdata/basics/raw_sem_ir/multifile.carbon
  7. 39 0
      toolchain/check/testdata/basics/raw_sem_ir/multifile_with_textual_ir.carbon
  8. 63 0
      toolchain/check/testdata/basics/raw_sem_ir/one_file.carbon
  9. 24 0
      toolchain/check/testdata/basics/raw_sem_ir/one_file_with_textual_ir.carbon
  10. 16 16
      toolchain/check/testdata/interop/cpp/function/overloads.carbon
  11. 131 42
      toolchain/check/type_completion.cpp
  12. 12 0
      toolchain/driver/testdata/stdin.carbon
  13. 5 1
      toolchain/lower/aggregate.cpp
  14. 31 2
      toolchain/lower/constant.cpp
  15. 164 21
      toolchain/lower/file_context.cpp
  16. 1 0
      toolchain/lower/function_context.cpp
  17. 1 1
      toolchain/lower/handle_call.cpp
  18. 11 11
      toolchain/lower/testdata/array/iterate.carbon
  19. 147 0
      toolchain/lower/testdata/array/layout.carbon
  20. 3 3
      toolchain/lower/testdata/class/field.carbon
  21. 9 9
      toolchain/lower/testdata/class/virtual.carbon
  22. 7 7
      toolchain/lower/testdata/for/bindings.carbon
  23. 11 11
      toolchain/lower/testdata/for/break_continue.carbon
  24. 11 11
      toolchain/lower/testdata/for/for.carbon
  25. 30 30
      toolchain/lower/testdata/primitives/optional.carbon
  26. 433 0
      toolchain/lower/testdata/struct/layout.carbon
  27. 6 6
      toolchain/lower/testdata/struct/member_access.carbon
  28. 8 3
      toolchain/sem_ir/file.cpp
  29. 13 6
      toolchain/sem_ir/file.h
  30. 3 1
      toolchain/sem_ir/inst_fingerprinter.cpp
  31. 9 0
      toolchain/sem_ir/type.h
  32. 12 1
      toolchain/sem_ir/type_info.cpp
  33. 144 0
      toolchain/sem_ir/type_info.h

+ 1 - 1
toolchain/check/convert.cpp

@@ -264,7 +264,7 @@ static auto ConvertTupleToArray(Context& context, SemIR::TupleType tuple_type,
 
   // Check that the tuple is the right size.
   std::optional<uint64_t> array_bound =
-      sem_ir.GetArrayBoundValue(array_type.bound_id);
+      sem_ir.GetZExtIntValue(array_type.bound_id);
   if (!array_bound) {
     // TODO: Should this fall back to using `ImplicitAs`?
     if (target.diagnose) {

+ 8 - 6
toolchain/check/cpp/import.cpp

@@ -64,6 +64,7 @@
 #include "toolchain/sem_ir/inst.h"
 #include "toolchain/sem_ir/name_scope.h"
 #include "toolchain/sem_ir/pattern.h"
+#include "toolchain/sem_ir/type_info.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
 namespace Carbon::Check {
@@ -450,14 +451,16 @@ static auto ImportClassObjectRepr(Context& context, SemIR::ClassId class_id,
   const auto& clang_layout =
       context.ast_context().getASTRecordLayout(clang_def);
 
-  llvm::SmallVector<uint64_t> layout;
+  llvm::SmallVector<SemIR::ObjectSize> layout;
   llvm::SmallVector<SemIR::StructTypeField> fields;
 
   static_assert(SemIR::CustomLayoutId::SizeIndex == 0);
-  layout.push_back(clang_layout.getSize().getQuantity());
+  layout.push_back(
+      SemIR::ObjectSize::Bytes(clang_layout.getSize().getQuantity()));
 
   static_assert(SemIR::CustomLayoutId::AlignIndex == 1);
-  layout.push_back(clang_layout.getAlignment().getQuantity());
+  layout.push_back(
+      SemIR::ObjectSize::Bytes(clang_layout.getAlignment().getQuantity()));
 
   static_assert(SemIR::CustomLayoutId::FirstFieldIndex == 2);
 
@@ -529,7 +532,7 @@ static auto ImportClassObjectRepr(Context& context, SemIR::ClassId class_id,
     auto base_offset = base.isVirtual()
                            ? clang_layout.getVBaseClassOffset(base_class)
                            : clang_layout.getBaseClassOffset(base_class);
-    layout.push_back(base_offset.getQuantity());
+    layout.push_back(SemIR::ObjectSize::Bytes(base_offset.getQuantity()));
     fields.push_back(
         {.name_id = SemIR::NameId::Base, .type_inst_id = base_type_inst_id});
   }
@@ -602,8 +605,7 @@ static auto ImportClassObjectRepr(Context& context, SemIR::ClassId class_id,
       offset += inner_layout.getFieldOffset(inner_field->getFieldIndex());
     }
 
-    layout.push_back(
-        context.ast_context().toCharUnitsFromBits(offset).getQuantity());
+    layout.push_back(SemIR::ObjectSize::Bits(offset));
     fields.push_back(
         {.name_id = field_name_id, .type_inst_id = field_type_inst_id});
   }

+ 1 - 1
toolchain/check/custom_witness.cpp

@@ -200,7 +200,7 @@ static auto CanDestroyType(
     case CARBON_KIND(SemIR::ArrayType array_type): {
       // A zero element array is always trivially destructible.
       if (auto int_bound =
-              context.sem_ir().GetArrayBoundValue(array_type.bound_id);
+              context.sem_ir().GetZExtIntValue(array_type.bound_id);
           !int_bound || *int_bound == 0) {
         return DestroyFormat::Trivial;
       }

+ 12 - 0
toolchain/check/testdata/basics/raw_sem_ir/builtins.carbon

@@ -37,12 +37,24 @@
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     'inst(TypeType)':  {kind: TypeType, type: type(TypeType)}

+ 51 - 0
toolchain/check/testdata/basics/raw_sem_ir/cpp_interop.carbon

@@ -96,38 +96,89 @@ fn G(x: Cpp.X) {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(InstType))':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000013)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst5000001E)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000020)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst50000020)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst(WitnessType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(WitnessType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000027)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst50000027)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst50000024)':
 // CHECK:STDOUT:       value_repr:      {kind: pointer, type: type(inst50000027)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst50000015)':
 // CHECK:STDOUT:       value_repr:      {kind: pointer, type: type(inst50000027)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst50000029)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst5000002E)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000031)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst5000003C)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000044)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000013)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     'inst(TypeType)':  {kind: TypeType, type: type(TypeType)}

+ 39 - 0
toolchain/check/testdata/basics/raw_sem_ir/multifile.carbon

@@ -55,16 +55,34 @@ fn B() {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000011)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000012)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000012)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000012)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     instF:           {kind: Namespace, arg0: name_scope0, arg1: inst<none>, type: type(inst(NamespaceType))}
@@ -138,18 +156,39 @@ fn B() {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst70000013)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst70000014)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst70000014)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst70000014)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(InstType))':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst70000014)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     instF:           {kind: Namespace, arg0: name_scope0, arg1: inst<none>, type: type(inst(NamespaceType))}

+ 39 - 0
toolchain/check/testdata/basics/raw_sem_ir/multifile_with_textual_ir.carbon

@@ -55,16 +55,34 @@ fn B() {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000011)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000012)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000012)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000012)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     instF:           {kind: Namespace, arg0: name_scope0, arg1: inst<none>, type: type(inst(NamespaceType))}
@@ -157,18 +175,39 @@ fn B() {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst70000013)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst70000014)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst70000014)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst70000014)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(InstType))':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst70000014)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     instF:           {kind: Namespace, arg0: name_scope0, arg1: inst<none>, type: type(inst(NamespaceType))}

+ 63 - 0
toolchain/check/testdata/basics/raw_sem_ir/one_file.carbon

@@ -417,46 +417,109 @@ fn Foo[T:! type](p: T*) -> (T*, ()) {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst78000026)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst78000026)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst7800002D)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst7800002D)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst78000029)':
 // CHECK:STDOUT:       value_repr:      {kind: pointer, type: type(inst7800002D)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst7800003F)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst78000026)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(symbolic_constant78000003)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(symbolic_constant78000003)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst(WitnessType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(WitnessType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(symbolic_constant78000011)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(symbolic_constant78000011)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(symbolic_constant78000009)':
 // CHECK:STDOUT:       value_repr:      {kind: pointer, type: type(symbolic_constant78000011)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(symbolic_constant78000004)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(symbolic_constant78000004)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(symbolic_constant7800000A)':
 // CHECK:STDOUT:       value_repr:      {kind: pointer, type: type(symbolic_constant78000011)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst(InstType))':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst78000026)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst78000050)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst78000050)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(SpecificFunctionType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(SpecificFunctionType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(RequireSpecificDefinitionType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(RequireSpecificDefinitionType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(symbolic_constant78000145)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst78000026)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(symbolic_constant7800014A)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst78000026)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(BoundMethodType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(BoundMethodType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:
 // CHECK:STDOUT:     facet_type78000000: {}
 // CHECK:STDOUT:     facet_type78000001: {extends interface: interface78000000}

+ 24 - 0
toolchain/check/testdata/basics/raw_sem_ir/one_file_with_textual_ir.carbon

@@ -47,20 +47,44 @@ fn Foo(n: ()) -> ((), ()) {
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst50000010)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000010)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst5000001D)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst5000001D)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            8
+// CHECK:STDOUT:         alignment:       8
 // CHECK:STDOUT:     'type(inst5000001A)':
 // CHECK:STDOUT:       value_repr:      {kind: pointer, type: type(inst5000001D)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst5000002B)':
 // CHECK:STDOUT:       value_repr:      {kind: none, type: type(inst50000010)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     instF:           {kind: Namespace, arg0: name_scope0, arg1: inst<none>, type: type(inst(NamespaceType))}

+ 16 - 16
toolchain/check/testdata/interop/cpp/function/overloads.carbon

@@ -2577,8 +2577,8 @@ fn F() {
 // CHECK:STDOUT:   %PassThreeFields__carbon_thunk.type: type = fn_type @PassThreeFields__carbon_thunk [concrete]
 // CHECK:STDOUT:   %PassThreeFields__carbon_thunk: %PassThreeFields__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %ThreeFields.elem: type = unbound_element_type %ThreeFields.942, %i32 [concrete]
-// CHECK:STDOUT:   %.c77: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete]
-// CHECK:STDOUT:   %complete_type.702: <witness> = complete_type_witness %.c77 [concrete]
+// CHECK:STDOUT:   %.b0d: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete]
+// CHECK:STDOUT:   %complete_type.54f: <witness> = complete_type_witness %.b0d [concrete]
 // CHECK:STDOUT:   %ImplicitAs.type.cc7: type = generic_interface_type @ImplicitAs [concrete]
 // CHECK:STDOUT:   %ImplicitAs.generic: %ImplicitAs.type.cc7 = struct_value () [concrete]
 // CHECK:STDOUT:   %struct_type.b.c.a: type = struct_type {.b: Core.IntLiteral, .c: Core.IntLiteral, .a: Core.IntLiteral} [concrete]
@@ -2669,8 +2669,8 @@ fn F() {
 // CHECK:STDOUT:   %i32.5: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
 // CHECK:STDOUT:   %i32.6: type = type_literal %i32.5 [concrete = constants.%i32]
 // CHECK:STDOUT:   %.3: %ThreeFields.elem = field_decl c, element2 [concrete]
-// CHECK:STDOUT:   %.4: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete = constants.%.c77]
-// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.4 [concrete = constants.%complete_type.702]
+// CHECK:STDOUT:   %.4: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete = constants.%.b0d]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.4 [concrete = constants.%complete_type.54f]
 // CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
@@ -2880,8 +2880,8 @@ fn F() {
 // CHECK:STDOUT:   %PassThreeFields__carbon_thunk.type: type = fn_type @PassThreeFields__carbon_thunk [concrete]
 // CHECK:STDOUT:   %PassThreeFields__carbon_thunk: %PassThreeFields__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %ThreeFields.elem: type = unbound_element_type %ThreeFields.942, %i32 [concrete]
-// CHECK:STDOUT:   %.c77: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete]
-// CHECK:STDOUT:   %complete_type.702: <witness> = complete_type_witness %.c77 [concrete]
+// CHECK:STDOUT:   %.b0d: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete]
+// CHECK:STDOUT:   %complete_type.54f: <witness> = complete_type_witness %.b0d [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -3020,8 +3020,8 @@ fn F() {
 // CHECK:STDOUT:   %i32.5: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
 // CHECK:STDOUT:   %i32.6: type = type_literal %i32.5 [concrete = constants.%i32]
 // CHECK:STDOUT:   %.3: %ThreeFields.elem = field_decl c, element2 [concrete]
-// CHECK:STDOUT:   %.4: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete = constants.%.c77]
-// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.4 [concrete = constants.%complete_type.702]
+// CHECK:STDOUT:   %.4: type = custom_layout_type {size=12, align=4, .a@0: %i32, .b@4: %i32, .c@8: %i32} [concrete = constants.%.b0d]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.4 [concrete = constants.%complete_type.54f]
 // CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
@@ -3344,11 +3344,11 @@ fn F() {
 // CHECK:STDOUT:   %Int.generic: %Int.type = struct_value () [concrete]
 // CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
 // CHECK:STDOUT:   %B.elem.c76: type = unbound_element_type %B, %i32 [concrete]
-// CHECK:STDOUT:   %.e7f: type = custom_layout_type {size=12, align=4, .a@0: %A, .n@8: %i32} [concrete]
-// CHECK:STDOUT:   %complete_type.b39: <witness> = complete_type_witness %.e7f [concrete]
+// CHECK:STDOUT:   %.344: type = custom_layout_type {size=12, align=4, .a@0: %A, .n@8: %i32} [concrete]
+// CHECK:STDOUT:   %complete_type.e04: <witness> = complete_type_witness %.344 [concrete]
 // CHECK:STDOUT:   %A.elem: type = unbound_element_type %A, %i32 [concrete]
-// CHECK:STDOUT:   %.7b6: type = custom_layout_type {size=8, align=4, .x@0: %i32, .y@4: %i32} [concrete]
-// CHECK:STDOUT:   %complete_type.1e0: <witness> = complete_type_witness %.7b6 [concrete]
+// CHECK:STDOUT:   %.171: type = custom_layout_type {size=8, align=4, .x@0: %i32, .y@4: %i32} [concrete]
+// CHECK:STDOUT:   %complete_type.2e6: <witness> = complete_type_witness %.171 [concrete]
 // CHECK:STDOUT:   %ImplicitAs.type.cc7: type = generic_interface_type @ImplicitAs [concrete]
 // CHECK:STDOUT:   %ImplicitAs.generic: %ImplicitAs.type.cc7 = struct_value () [concrete]
 // CHECK:STDOUT: }
@@ -3395,8 +3395,8 @@ fn F() {
 // CHECK:STDOUT:   %i32.1: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
 // CHECK:STDOUT:   %i32.2: type = type_literal %i32.1 [concrete = constants.%i32]
 // CHECK:STDOUT:   %.2: %B.elem.c76 = field_decl n, element1 [concrete]
-// CHECK:STDOUT:   %.3: type = custom_layout_type {size=12, align=4, .a@0: %A, .n@8: %i32} [concrete = constants.%.e7f]
-// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.3 [concrete = constants.%complete_type.b39]
+// CHECK:STDOUT:   %.3: type = custom_layout_type {size=12, align=4, .a@0: %A, .n@8: %i32} [concrete = constants.%.344]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.3 [concrete = constants.%complete_type.e04]
 // CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:
@@ -3412,8 +3412,8 @@ fn F() {
 // CHECK:STDOUT:   %i32.3: type = class_type @Int, @Int(constants.%int_32) [concrete = constants.%i32]
 // CHECK:STDOUT:   %i32.4: type = type_literal %i32.3 [concrete = constants.%i32]
 // CHECK:STDOUT:   %.2: %A.elem = field_decl y, element1 [concrete]
-// CHECK:STDOUT:   %.3: type = custom_layout_type {size=8, align=4, .x@0: %i32, .y@4: %i32} [concrete = constants.%.7b6]
-// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.3 [concrete = constants.%complete_type.1e0]
+// CHECK:STDOUT:   %.3: type = custom_layout_type {size=8, align=4, .x@0: %i32, .y@4: %i32} [concrete = constants.%.171]
+// CHECK:STDOUT:   %complete_type: <witness> = complete_type_witness %.3 [concrete = constants.%complete_type.2e6]
 // CHECK:STDOUT:   complete_type_witness = %complete_type
 // CHECK:STDOUT:
 // CHECK:STDOUT: !members:

+ 131 - 42
toolchain/check/type_completion.cpp

@@ -222,12 +222,14 @@ class TypeCompleter {
   // `type_inst` to be complete to our work list.
   auto AddNestedIncompleteTypes(SemIR::Inst type_inst) -> bool;
 
-  // Makes an empty value representation, which is used for types that have no
-  // state, such as empty structs and tuples.
-  auto MakeEmptyValueRepr() const -> SemIR::ValueRepr;
+  // Makes type info for a type with an empty value representation, which is
+  // used for types that have no state, such as empty structs and tuples.
+  auto MakeEmptyTypeInfo() const -> SemIR::CompleteTypeInfo;
 
-  // Makes a dependent value representation, which is used for symbolic types.
-  auto MakeDependentValueRepr(SemIR::TypeId type_id) const -> SemIR::ValueRepr;
+  // Makes type info for a type with a dependent value representation, which is
+  // used for symbolic types.
+  auto MakeDependentTypeInfo(SemIR::TypeId type_id) const
+      -> SemIR::CompleteTypeInfo;
 
   // Makes a value representation that uses pass-by-copy, copying the given
   // type.
@@ -243,25 +245,34 @@ class TypeCompleter {
                                 SemIR::ValueRepr::NotAggregate) const
       -> SemIR::ValueRepr;
 
-  // Gets the value representation of a nested type, which should already be
-  // complete.
+  // Gets the type info for a nested type, which should already be complete.
   auto GetNestedInfo(SemIR::TypeId nested_type_id) const
       -> SemIR::CompleteTypeInfo;
 
   template <typename InstT>
     requires(InstT::Kind.template IsAnyOf<
-             SemIR::AutoType, SemIR::BoolType, SemIR::BoundMethodType,
-             SemIR::CharLiteralType, SemIR::ErrorInst, SemIR::FacetType,
-             SemIR::FloatLiteralType, SemIR::FloatType, SemIR::FormType,
-             SemIR::IntType, SemIR::IntLiteralType, SemIR::NamespaceType,
-             SemIR::PatternType, SemIR::PointerType,
-             SemIR::RequireSpecificDefinitionType, SemIR::SpecificFunctionType,
-             SemIR::TypeType, SemIR::VtableType, SemIR::WitnessType>())
+             SemIR::AutoType, SemIR::BoundMethodType, SemIR::CharLiteralType,
+             SemIR::ErrorInst, SemIR::FacetType, SemIR::FloatLiteralType,
+             SemIR::FormType, SemIR::IntLiteralType, SemIR::NamespaceType,
+             SemIR::PatternType, SemIR::RequireSpecificDefinitionType,
+             SemIR::SpecificFunctionType, SemIR::TypeType, SemIR::VtableType,
+             SemIR::WitnessType>())
   auto BuildInfoForInst(SemIR::TypeId type_id, InstT /*inst*/) const
       -> SemIR::CompleteTypeInfo {
-    return {.value_repr = MakeCopyValueRepr(type_id)};
+    // These types are empty at runtime but have values to copy at compile time.
+    return {.value_repr = MakeCopyValueRepr(type_id),
+            .object_layout = SemIR::ObjectLayout::Empty()};
   }
 
+  auto BuildInfoForInst(SemIR::TypeId type_id, SemIR::BoolType inst) const
+      -> SemIR::CompleteTypeInfo;
+  auto BuildInfoForInst(SemIR::TypeId type_id, SemIR::PointerType inst) const
+      -> SemIR::CompleteTypeInfo;
+  auto BuildInfoForInst(SemIR::TypeId type_id, SemIR::IntType inst) const
+      -> SemIR::CompleteTypeInfo;
+  auto BuildInfoForInst(SemIR::TypeId type_id, SemIR::FloatType inst) const
+      -> SemIR::CompleteTypeInfo;
+
   auto BuildStructOrTupleValueRepr(size_t num_elements,
                                    SemIR::TypeId elementwise_rep,
                                    bool same_as_object_rep) const
@@ -297,7 +308,7 @@ class TypeCompleter {
     // - For an interface, we could use a witness.
     // - For an associated entity, we could use an index into the witness.
     // - For an unbound element, we could use an index or offset.
-    return {.value_repr = MakeEmptyValueRepr()};
+    return MakeEmptyTypeInfo();
   }
 
   auto BuildInfoForInst(SemIR::TypeId /*type_id*/, SemIR::ConstType inst) const
@@ -330,7 +341,7 @@ class TypeCompleter {
     requires(InstT::Kind.is_symbolic_when_type())
   auto BuildInfoForInst(SemIR::TypeId type_id, InstT /*inst*/) const
       -> SemIR::CompleteTypeInfo {
-    return {.value_repr = MakeDependentValueRepr(type_id)};
+    return MakeDependentTypeInfo(type_id);
   }
 
   // Builds and returns the `CompleteTypeInfo` for the given type. All nested
@@ -501,14 +512,17 @@ auto TypeCompleter::AddNestedIncompleteTypes(SemIR::Inst type_inst) -> bool {
   return true;
 }
 
-auto TypeCompleter::MakeEmptyValueRepr() const -> SemIR::ValueRepr {
-  return {.kind = SemIR::ValueRepr::None,
-          .type_id = GetTupleType(*context_, {})};
+auto TypeCompleter::MakeEmptyTypeInfo() const -> SemIR::CompleteTypeInfo {
+  return {.value_repr = {.kind = SemIR::ValueRepr::None,
+                         .type_id = GetTupleType(*context_, {})},
+          .object_layout = SemIR::ObjectLayout::Empty()};
 }
 
-auto TypeCompleter::MakeDependentValueRepr(SemIR::TypeId type_id) const
-    -> SemIR::ValueRepr {
-  return {.kind = SemIR::ValueRepr::Dependent, .type_id = type_id};
+auto TypeCompleter::MakeDependentTypeInfo(SemIR::TypeId type_id) const
+    -> SemIR::CompleteTypeInfo {
+  return {
+      .value_repr = {.kind = SemIR::ValueRepr::Dependent, .type_id = type_id},
+      .object_layout = SemIR::ObjectLayout()};
 }
 
 auto TypeCompleter::MakeCopyValueRepr(
@@ -565,7 +579,7 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
     -> SemIR::CompleteTypeInfo {
   auto fields = context_->struct_type_fields().Get(struct_type.fields_id);
   if (fields.empty()) {
-    return {.value_repr = MakeEmptyValueRepr()};
+    return MakeEmptyTypeInfo();
   }
 
   // Find the value representation for each field, and construct a struct
@@ -574,6 +588,7 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
   value_rep_fields.reserve(fields.size());
   bool same_as_object_rep = true;
   SemIR::ClassId abstract_class_id = SemIR::ClassId::None;
+  SemIR::ObjectLayout layout = SemIR::ObjectLayout::Empty();
   for (auto field : fields) {
     auto field_type_id =
         context_->types().GetTypeIdForTypeInstId(field.type_inst_id);
@@ -590,6 +605,8 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
         !abstract_class_id.has_value()) {
       abstract_class_id = field_info.abstract_class_id;
     }
+    // Accumulate layout.
+    layout.TryAppendField(field_info.object_layout);
   }
 
   auto value_rep =
@@ -600,6 +617,7 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
                 context_->struct_type_fields().AddCanonical(value_rep_fields));
   return {.value_repr = BuildStructOrTupleValueRepr(fields.size(), value_rep,
                                                     same_as_object_rep),
+          .object_layout = layout,
           .abstract_class_id = abstract_class_id};
 }
 
@@ -609,7 +627,7 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
   // TODO: Share more code with structs.
   auto elements = context_->inst_blocks().Get(tuple_type.type_elements_id);
   if (elements.empty()) {
-    return {.value_repr = MakeEmptyValueRepr()};
+    return MakeEmptyTypeInfo();
   }
 
   // Find the value representation for each element, and construct a tuple
@@ -618,6 +636,7 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
   value_rep_elements.reserve(elements.size());
   bool same_as_object_rep = true;
   SemIR::ClassId abstract_class_id = SemIR::ClassId::None;
+  SemIR::ObjectLayout layout = SemIR::ObjectLayout::Empty();
   for (auto element_type_id : context_->types().GetBlockAsTypeIds(elements)) {
     auto element_info = GetNestedInfo(element_type_id);
     if (!element_info.value_repr.IsCopyOfObjectRepr(context_->sem_ir(),
@@ -631,6 +650,8 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
         !abstract_class_id.has_value()) {
       abstract_class_id = element_info.abstract_class_id;
     }
+    // Accumulate layout.
+    layout.TryAppendField(element_info.object_layout);
   }
 
   auto value_rep = same_as_object_rep
@@ -638,17 +659,80 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
                        : GetTupleType(*context_, value_rep_elements);
   return {.value_repr = BuildStructOrTupleValueRepr(elements.size(), value_rep,
                                                     same_as_object_rep),
+          .object_layout = layout,
           .abstract_class_id = abstract_class_id};
 }
 
 auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
-                                     SemIR::ArrayType /*inst*/) const
+                                     SemIR::ArrayType array_type) const
     -> SemIR::CompleteTypeInfo {
   // 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.
+  auto element_type_id =
+      context_->types().GetTypeIdForTypeInstId(array_type.element_type_inst_id);
+  auto element_info = GetNestedInfo(element_type_id);
+
+  SemIR::ObjectLayout layout;
+  if (element_info.object_layout.has_value()) {
+    if (auto bound = context_->sem_ir().GetZExtIntValue(array_type.bound_id)) {
+      layout =
+          SemIR::ObjectLayout::ForArray(element_info.object_layout, *bound);
+    }
+  }
+
   return {.value_repr =
-              MakePointerValueRepr(type_id, SemIR::ValueRepr::ObjectAggregate)};
+              MakePointerValueRepr(type_id, SemIR::ValueRepr::ObjectAggregate),
+          .object_layout = layout};
+}
+
+auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
+                                     SemIR::BoolType /*inst*/) const
+    -> SemIR::CompleteTypeInfo {
+  // TODO: Decide whether to use a size of `1` here, like `Core.Int(1)`. One
+  // concern is that {.a: bool, .b: bool} would be laid out with the bools at
+  // offsets 0 and 8, which on big-endian systems would imply the MSB stores the
+  // value not the LSB. Consider right-aligning fields on big-endian systems
+  // instead (except when bit-packing, by whatever means we support that).
+  return {.value_repr = MakeCopyValueRepr(type_id),
+          .object_layout = {.size = SemIR::ObjectSize::Bytes(1),
+                            .alignment = SemIR::ObjectSize::Bytes(1)}};
+}
+
+auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
+                                     SemIR::PointerType /*inst*/) const
+    -> SemIR::CompleteTypeInfo {
+  return {.value_repr = MakeCopyValueRepr(type_id),
+          .object_layout = context_->sem_ir().GetPointerLayout()};
+}
+
+auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
+                                     SemIR::IntType inst) const
+    -> SemIR::CompleteTypeInfo {
+  SemIR::ObjectLayout layout;
+  if (auto bit_width = context_->sem_ir().GetZExtIntValue(inst.bit_width_id)) {
+    auto size = SemIR::ObjectSize::Bits(*bit_width);
+    // TODO: The upper bound for alignment here should be target-specific.
+    auto align = SemIR::ObjectSize::Bits(
+        std::clamp<int64_t>(llvm::PowerOf2Ceil(*bit_width), 8, 256));
+    layout = {.size = size, .alignment = align};
+  }
+  return {.value_repr = MakeCopyValueRepr(type_id), .object_layout = layout};
+}
+
+auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
+                                     SemIR::FloatType inst) const
+    -> SemIR::CompleteTypeInfo {
+  SemIR::ObjectLayout layout;
+  if (auto bit_width = context_->sem_ir().GetZExtIntValue(inst.bit_width_id)) {
+    auto size = SemIR::ObjectSize::Bits(*bit_width);
+    // TODO: Pick a suitable alignment here. For some targets, we may want to
+    // use 32-bit alignment for f64 (and f80). For now we round up to a power
+    // of 2.
+    auto align = SemIR::ObjectSize::Bits(llvm::PowerOf2Ceil(*bit_width));
+    layout = {.size = size, .alignment = align};
+  }
+  return {.value_repr = MakeCopyValueRepr(type_id), .object_layout = layout};
 }
 
 auto TypeCompleter::BuildInfoForInst(SemIR::TypeId /*type_id*/,
@@ -660,8 +744,8 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId /*type_id*/,
           ? inst.class_id
           : SemIR::ClassId::None;
 
-  // The value representation of an adapter is the value representation of
-  // its adapted type.
+  // The object and value representation of an adapter are the object and value
+  // representation of its adapted type.
   if (auto adapted_type_id =
           class_info.GetAdaptedType(context_->sem_ir(), inst.specific_id);
       adapted_type_id.has_value()) {
@@ -670,29 +754,35 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId /*type_id*/,
     return info;
   }
   // Otherwise, the value representation for a class is a pointer to the
-  // object representation.
+  // object representation, which was computed when the class was defined.
+  auto object_repr_type_id =
+      class_info.GetObjectRepr(context_->sem_ir(), inst.specific_id);
   // TODO: Support customized value representations for classes.
   // TODO: Pick a better value representation when possible.
-  return {.value_repr = MakePointerValueRepr(
-              class_info.GetObjectRepr(context_->sem_ir(), inst.specific_id),
-              SemIR::ValueRepr::ObjectAggregate),
+  return {.value_repr = MakePointerValueRepr(object_repr_type_id,
+                                             SemIR::ValueRepr::ObjectAggregate),
+          .object_layout = GetNestedInfo(object_repr_type_id).object_layout,
           .abstract_class_id = abstract_class_id};
 }
 
 auto TypeCompleter::BuildInfoForInst(SemIR::TypeId /*type_id*/,
                                      SemIR::ConstType inst) const
     -> SemIR::CompleteTypeInfo {
-  // The value representation of `const T` is the same as that of `T`.
-  // Objects are not modifiable through their value representations.
+  // The object and value representation of `const T` are the same as those of
+  // `T`. Objects are not modifiable through their value representations.
   return GetNestedInfo(context_->types().GetTypeIdForTypeInstId(inst.inner_id));
 }
 
 auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
-                                     SemIR::CustomLayoutType /*inst*/) const
+                                     SemIR::CustomLayoutType inst) const
     -> SemIR::CompleteTypeInfo {
   // TODO: Should we support other value representations for custom layout
   // types?
-  return {.value_repr = MakePointerValueRepr(type_id)};
+  const auto& layout = context_->custom_layouts().Get(inst.layout_id);
+  return {.value_repr = MakePointerValueRepr(type_id),
+          .object_layout = {
+              .size = layout[SemIR::CustomLayoutId::SizeIndex],
+              .alignment = layout[SemIR::CustomLayoutId::AlignIndex]}};
 }
 
 auto TypeCompleter::BuildInfoForInst(SemIR::TypeId type_id,
@@ -726,11 +816,10 @@ auto TypeCompleter::BuildInfoForInst(SemIR::TypeId /*type_id*/,
   // The value representation of `partial T` is the same as that of `T`.
   // Objects are not modifiable through their value representations. However,
   // `partial T` is never abstract.
-  return {
-      .value_repr =
-          GetNestedInfo(context_->types().GetTypeIdForTypeInstId(inst.inner_id))
-              .value_repr,
-      .abstract_class_id = SemIR::ClassId::None};
+  auto inner_type_id = context_->types().GetTypeIdForTypeInstId(inst.inner_id);
+  auto info = GetNestedInfo(inner_type_id);
+  info.abstract_class_id = SemIR::ClassId::None;
+  return info;
 }
 
 auto TypeCompleter::BuildInfoForInst(

+ 12 - 0
toolchain/driver/testdata/stdin.carbon

@@ -49,12 +49,24 @@
 // CHECK:STDOUT:   types:
 // CHECK:STDOUT:     'type(TypeType)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(TypeType)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(FormType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(FormType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(Error)':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(Error)}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:     'type(inst(NamespaceType))':
 // CHECK:STDOUT:       value_repr:      {kind: copy, type: type(inst(NamespaceType))}
+// CHECK:STDOUT:       object_layout:
+// CHECK:STDOUT:         size:            0
+// CHECK:STDOUT:         alignment:       1
 // CHECK:STDOUT:   facet_types:     {}
 // CHECK:STDOUT:   insts:
 // CHECK:STDOUT:     instF:           {kind: Namespace, arg0: name_scope0, arg1: inst<none>, type: type(inst(NamespaceType))}

+ 5 - 1
toolchain/lower/aggregate.cpp

@@ -7,6 +7,7 @@
 #include "llvm/ADT/STLExtras.h"
 #include "toolchain/sem_ir/expr_info.h"
 #include "toolchain/sem_ir/inst.h"
+#include "toolchain/sem_ir/type_info.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
 namespace Carbon::Lower {
@@ -28,9 +29,12 @@ static auto GetElementIndex(FunctionContext::TypeInFile type,
     // For custom layout types, we form an array of i8 as the LLVM type, so the
     // offset in the type is the getelementptr index.
     // TODO: This offset might not fit into an `unsigned int`.
-    return type.file->custom_layouts().Get(
+    auto offset = type.file->custom_layouts().Get(
         custom_layout_type
             ->layout_id)[SemIR::CustomLayoutId::FirstFieldIndex + idx.index];
+    CARBON_CHECK(offset == offset.AlignedTo(SemIR::ObjectSize::Bytes(1)),
+                 "Element offset not byte-aligned: {0}", offset);
+    return offset.bytes();
   }
 
   // For now, struct and tuple types map directly into LLVM struct types with

+ 31 - 2
toolchain/lower/constant.cpp

@@ -110,6 +110,34 @@ class ConstantContext {
   SemIR::InstId current_constant_inst_id_ = SemIR::InstId::None;
 };
 
+// Get the element type at a given index from a struct.
+static auto GetElementType(llvm::StructType* struct_type, int index)
+    -> llvm::Type* {
+  return struct_type->getElementType(index);
+}
+
+// Get the element type from an array.
+static auto GetElementType(llvm::ArrayType* array_type, int /*index*/)
+    -> llvm::Type* {
+  return array_type->getElementType();
+}
+
+// Add padding if necessary to convert the given constant to the specified,
+// possibly-padded LLVM type. The type of the constant will either already be
+// `llvm_type`, or will be a tail-padded type `<{llvm_type, [i8 x N]}>`.
+static auto PadToType(llvm::Constant* constant, llvm::Type* llvm_type)
+    -> llvm::Constant* {
+  if (constant->getType() == llvm_type) {
+    return constant;
+  }
+  auto* padded = cast<llvm::StructType>(llvm_type);
+  CARBON_CHECK(padded->getNumElements() == 2 &&
+                   padded->getElementType(0) == constant->getType(),
+               "Unexpected type {0} for constant {1}", *llvm_type, *constant);
+  return llvm::ConstantStruct::get(
+      padded, constant, llvm::PoisonValue::get(padded->getElementType(1)));
+}
+
 // Emits an aggregate constant of LLVM type `Type` whose elements are the
 // contents of `refs_id`.
 template <typename ConstantType, typename Type>
@@ -119,9 +147,10 @@ static auto EmitAggregateConstant(ConstantContext& context,
   auto refs = context.sem_ir().inst_blocks().Get(refs_id);
   llvm::SmallVector<llvm::Constant*> elements;
   elements.reserve(refs.size());
-  for (auto ref : refs) {
+  for (auto [i, ref] : llvm::enumerate(refs)) {
     if (auto* constant = context.GetConstant(ref)) {
-      elements.push_back(constant);
+      auto* elem_type = GetElementType(llvm_type, i);
+      elements.push_back(PadToType(constant, elem_type));
     } else {
       return nullptr;
     }

+ 164 - 21
toolchain/lower/file_context.cpp

@@ -38,6 +38,7 @@
 #include "toolchain/sem_ir/mangler.h"
 #include "toolchain/sem_ir/pattern.h"
 #include "toolchain/sem_ir/stringify.h"
+#include "toolchain/sem_ir/type_info.h"
 #include "toolchain/sem_ir/typed_insts.h"
 
 namespace Carbon::Lower {
@@ -1191,6 +1192,22 @@ auto FileContext::BuildDISubprogram(const SemIR::Function& function,
   return subprogram;
 }
 
+// Given an LLVM type, build a corresponding type with `padding_bytes` bytes of
+// explicit tail padding.
+static auto BuildTailPaddedType(llvm::Type* subtype, int64_t padding_bytes)
+    -> llvm::Type* {
+  if (padding_bytes == 0) {
+    return subtype;
+  }
+  // Build the type `<{subtype, [i8 x padding_bytes]}>`.
+  llvm::Type* type_with_padding[2] = {
+      subtype,
+      llvm::ArrayType::get(llvm::Type::getInt8Ty(subtype->getContext()),
+                           padding_bytes)};
+  return llvm::StructType::get(subtype->getContext(), type_with_padding,
+                               /*isPacked=*/true);
+}
+
 // BuildTypeForInst is used to construct types for FileContext::BuildType below.
 // Implementations return the LLVM type for the instruction. This first overload
 // is the fallback handler for non-type instructions.
@@ -1211,10 +1228,25 @@ static auto BuildTypeForInst(FileContext& context, InstT /*inst*/)
 
 static auto BuildTypeForInst(FileContext& context, SemIR::ArrayType inst)
     -> FileContext::LoweredTypes {
+  auto elem_type_id = context.sem_ir().types().GetTypeIdForTypeInstId(
+      inst.element_type_inst_id);
+  auto stride = context.sem_ir()
+                    .types()
+                    .GetCompleteTypeInfo(elem_type_id)
+                    .object_layout.ArrayStride();
+
+  auto* elem_type = context.GetType(elem_type_id);
+  auto elem_size = SemIR::ObjectSize::Bytes(
+      context.llvm_module().getDataLayout().getTypeAllocSize(elem_type));
+
+  if (elem_size != stride) {
+    CARBON_CHECK(elem_size < stride, "Array element type too large");
+    elem_type = BuildTailPaddedType(context.GetType(elem_type_id),
+                                    stride.bytes() - elem_size.bytes());
+  }
+
   return {llvm::ArrayType::get(
-              context.GetType(context.sem_ir().types().GetTypeIdForTypeInstId(
-                  inst.element_type_inst_id)),
-              *context.sem_ir().GetArrayBoundValue(inst.bound_id)),
+              elem_type, *context.sem_ir().GetZExtIntValue(inst.bound_id)),
           nullptr};
 }
 
@@ -1246,9 +1278,10 @@ static auto BuildTypeForInst(FileContext& context, InstT inst)
 static auto BuildTypeForInst(FileContext& context, SemIR::CustomLayoutType inst)
     -> FileContext::LoweredTypes {
   auto layout = context.sem_ir().custom_layouts().Get(inst.layout_id);
-  return {llvm::ArrayType::get(llvm::Type::getInt8Ty(context.llvm_context()),
-                               layout[SemIR::CustomLayoutId::SizeIndex]),
-          nullptr};
+  return {
+      llvm::ArrayType::get(llvm::Type::getInt8Ty(context.llvm_context()),
+                           layout[SemIR::CustomLayoutId::SizeIndex].bytes()),
+      nullptr};
 }
 
 static auto BuildTypeForInst(FileContext& context,
@@ -1296,16 +1329,99 @@ static auto BuildTypeForInst(FileContext& /*context*/,
   CARBON_FATAL("Unexpected pattern type in lowering");
 }
 
+// Builds an LLVM packed struct type whose layout matches the Carbon layout for
+// an aggregate with the given field types and field layouts.
+static auto BuildPackedStructType(FileContext& context,
+                                  llvm::MutableArrayRef<llvm::Type*> subtypes,
+                                  llvm::ArrayRef<SemIR::ObjectLayout> layouts)
+    -> llvm::StructType* {
+  const auto& data_layout = context.llvm_module().getDataLayout();
+  auto struct_layout = SemIR::ObjectLayout::Empty();
+  auto size_so_far = SemIR::ObjectSize::Zero();
+
+  llvm::Type** previous_type = nullptr;
+  for (auto [type, layout] : llvm::zip_equal(subtypes, layouts)) {
+    auto offset = struct_layout.FieldOffset(layout);
+    // If this field has padding before it, represent that padding explicitly as
+    // part of the previous field. This allows us to always use GEP indexes that
+    // match the field indexes.
+    if (offset != size_so_far) {
+      CARBON_CHECK(previous_type, "Padding before first field?");
+      CARBON_CHECK(offset > size_so_far, "Extraneous padding after field {0}",
+                   **previous_type);
+      int64_t padding_bytes = offset.bytes() - struct_layout.size.bytes();
+      *previous_type = BuildTailPaddedType(*previous_type, padding_bytes);
+      size_so_far += SemIR::ObjectSize::Bytes(padding_bytes);
+      CARBON_CHECK(offset == size_so_far, "Field at non-byte offset");
+    }
+
+    size_so_far += SemIR::ObjectSize::Bytes(data_layout.getTypeAllocSize(type));
+    struct_layout.AppendField(layout);
+    previous_type = &type;
+  }
+  return llvm::StructType::get(context.llvm_context(), subtypes,
+                               /*isPacked=*/true);
+}
+
+// Returns whether the given LLVM layout matches the expected Carbon layout for
+// an aggregate with the given field layouts.
+static auto StructLayoutMatches(llvm::ArrayRef<SemIR::ObjectLayout> layouts,
+                                const llvm::StructLayout& llvm_layout) -> bool {
+  auto struct_layout = SemIR::ObjectLayout::Empty();
+
+  // Check each field is at the right offset.
+  for (auto [i, layout] : llvm::enumerate(layouts)) {
+    if (static_cast<int64_t>(llvm_layout.getElementOffsetInBits(i)) !=
+        struct_layout.FieldOffset(layout).bits()) {
+      return false;
+    }
+    struct_layout.AppendField(layout);
+  }
+
+  // Treat the LLVM layout as being acceptable if it's the right byte size and
+  // does not require more alignment than the Carbon type. We could ignore the
+  // alignment, but an overaligned LLVM type will prevent the type from being
+  // used in non-packed structs in more situations.
+  return static_cast<int64_t>(llvm_layout.getSizeInBytes()) ==
+             struct_layout.size.bytes() &&
+         llvm_layout.getAlignment() <=
+             llvm::Align(struct_layout.alignment.bytes());
+}
+
+// Builds an LLVM struct type whose layout matches the Carbon layout for an
+// aggregate with the given field types and field layouts.
+static auto BuildStructType(FileContext& context,
+                            llvm::MutableArrayRef<llvm::Type*> subtypes,
+                            llvm::ArrayRef<SemIR::ObjectLayout> layouts)
+    -> FileContext::LoweredTypes {
+  // Opportunistically try building an llvm StructType from the subtypes. If it
+  // has the right layout, we're done. We prefer to use a non-packed struct type
+  // where possible to produce a smaller LLVM IR representation for the type and
+  // for constant values of the type, and to improve the readability of the IR.
+  auto* struct_type = llvm::StructType::get(context.llvm_context(), subtypes);
+  if (!StructLayoutMatches(
+          layouts, *context.llvm_module().getDataLayout().getStructLayout(
+                       struct_type))) {
+    struct_type = BuildPackedStructType(context, subtypes, layouts);
+  }
+  return {struct_type, nullptr};
+}
+
 static auto BuildTypeForInst(FileContext& context, SemIR::StructType inst)
     -> FileContext::LoweredTypes {
   auto fields = context.sem_ir().struct_type_fields().Get(inst.fields_id);
   llvm::SmallVector<llvm::Type*> subtypes;
+  llvm::SmallVector<SemIR::ObjectLayout> layouts;
   subtypes.reserve(fields.size());
+  layouts.reserve(fields.size());
   for (auto field : fields) {
-    subtypes.push_back(context.GetType(
-        context.sem_ir().types().GetTypeIdForTypeInstId(field.type_inst_id)));
+    auto type_id =
+        context.sem_ir().types().GetTypeIdForTypeInstId(field.type_inst_id);
+    subtypes.push_back(context.GetType(type_id));
+    layouts.push_back(
+        context.sem_ir().types().GetCompleteTypeInfo(type_id).object_layout);
   }
-  return {llvm::StructType::get(context.llvm_context(), subtypes), nullptr};
+  return BuildStructType(context, subtypes, layouts);
 }
 
 static auto BuildTypeForInst(FileContext& context, SemIR::TupleType inst)
@@ -1316,11 +1432,15 @@ static auto BuildTypeForInst(FileContext& context, SemIR::TupleType inst)
   // type, so that may require significant special casing.
   auto elements = context.sem_ir().inst_blocks().Get(inst.type_elements_id);
   llvm::SmallVector<llvm::Type*> subtypes;
+  llvm::SmallVector<SemIR::ObjectLayout> layouts;
   subtypes.reserve(elements.size());
+  layouts.reserve(elements.size());
   for (auto type_id : context.sem_ir().types().GetBlockAsTypeIds(elements)) {
     subtypes.push_back(context.GetType(type_id));
+    layouts.push_back(
+        context.sem_ir().types().GetCompleteTypeInfo(type_id).object_layout);
   }
-  return {llvm::StructType::get(context.llvm_context(), subtypes), nullptr};
+  return BuildStructType(context, subtypes, layouts);
 }
 
 static auto BuildTypeForInst(FileContext& context, SemIR::TypeType /*inst*/)
@@ -1338,12 +1458,6 @@ static auto BuildTypeForInst(FileContext& context, SemIR::VtableType /*inst*/)
   return {llvm::Type::getVoidTy(context.llvm_context()), nullptr};
 }
 
-static auto BuildTypeForInst(FileContext& context,
-                             SemIR::SpecificFunctionType /*inst*/)
-    -> FileContext::LoweredTypes {
-  return {llvm::PointerType::get(context.llvm_context(), 0), nullptr};
-}
-
 template <typename InstT>
   requires(InstT::Kind.template IsAnyOf<
            SemIR::AssociatedEntityType, SemIR::AutoType, SemIR::BoundMethodType,
@@ -1353,8 +1467,8 @@ template <typename InstT>
            SemIR::FunctionTypeWithSelfType, SemIR::GenericClassType,
            SemIR::GenericInterfaceType, SemIR::GenericNamedConstraintType,
            SemIR::InstType, SemIR::IntLiteralType, SemIR::NamespaceType,
-           SemIR::RequireSpecificDefinitionType, SemIR::UnboundElementType,
-           SemIR::WhereExpr, SemIR::WitnessType>())
+           SemIR::RequireSpecificDefinitionType, SemIR::SpecificFunctionType,
+           SemIR::UnboundElementType, SemIR::WhereExpr, SemIR::WitnessType>())
 static auto BuildTypeForInst(FileContext& context, InstT /*inst*/)
     -> FileContext::LoweredTypes {
   // Return an empty struct as a placeholder.
@@ -1366,13 +1480,42 @@ static auto BuildTypeForInst(FileContext& context, InstT /*inst*/)
 auto FileContext::BuildType(SemIR::InstId inst_id) -> LoweredTypes {
   // Use overload resolution to select the implementation, producing compile
   // errors when BuildTypeForInst isn't defined for a given instruction.
+  LoweredTypes result;
   CARBON_KIND_SWITCH(sem_ir_->insts().Get(inst_id)) {
-#define CARBON_SEM_IR_INST_KIND(Name)     \
-  case CARBON_KIND(SemIR::Name inst): {   \
-    return BuildTypeForInst(*this, inst); \
+#define CARBON_SEM_IR_INST_KIND(Name)       \
+  case CARBON_KIND(SemIR::Name inst): {     \
+    result = BuildTypeForInst(*this, inst); \
+    break;                                  \
   }
 #include "toolchain/sem_ir/inst_kind.def"
   }
+
+  // In debug builds, check that the type we built has the expected size.
+  CARBON_DCHECK([&] {
+    if (!result.llvm_ir_type) {
+      return true;
+    }
+    const auto& layout = llvm_module().getDataLayout();
+    auto expected_layout =
+        sem_ir()
+            .types()
+            .GetCompleteTypeInfo(
+                sem_ir().types().GetTypeIdForTypeInstId(inst_id))
+            .object_layout;
+    CARBON_CHECK(expected_layout.has_value());
+    auto size =
+        SemIR::ObjectSize::Bits(layout.getTypeSizeInBits(result.llvm_ir_type));
+    // Round up to byte granularity for this check, since LLVM doesn't support
+    // non-byte-sized packed structs.
+    CARBON_CHECK(
+        size.bytes() == expected_layout.size.bytes(),
+        "Lowered type {0} for {1} has unexpected size {2}, expected {3}",
+        *result.llvm_ir_type, sem_ir().insts().Get(inst_id), size,
+        expected_layout.size);
+    return true;
+  }());
+
+  return result;
 }
 
 auto FileContext::BuildGlobalVariableDecl(SemIR::VarStorage var_storage)

+ 1 - 0
toolchain/lower/function_context.cpp

@@ -276,6 +276,7 @@ auto FunctionContext::InitializeStorage(TypeInFile type, SemIR::InstId dest_id,
       if (sem_ir().constant_values().Get(source_id).is_constant()) {
         // When initializing from a constant, emission of the source doesn't
         // initialize the destination. Copy the constant value instead.
+        // TODO: If the type is small, emit a store rather than a memcpy.
         CopyValue(type, source_id, dest_id);
       }
       break;

+ 1 - 1
toolchain/lower/handle_call.cpp

@@ -269,7 +269,7 @@ static auto StoreArrayAsStdInitializerList(FunctionContext& context,
       context.GetTypeIdOfInst(array_inst_id);
   auto array_type = array_type_file->types().GetAs<SemIR::ArrayType>(
       array_type_file->types().GetObjectRepr(array_type_id));
-  auto array_bound = array_type_file->GetArrayBoundValue(array_type.bound_id);
+  auto array_bound = array_type_file->GetZExtIntValue(array_type.bound_id);
   CARBON_CHECK(array_bound, "Array type with non-constant bound");
 
   // Store the array pointer in the first element of the initializer list.

+ 11 - 11
toolchain/lower/testdata/array/iterate.carbon

@@ -32,7 +32,7 @@ fn F() {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %.loc16_43.3.temp = alloca [6 x i32], align 4, !dbg !7
 // CHECK:STDOUT:   %var = alloca i32, align 4, !dbg !8
-// CHECK:STDOUT:   %.loc17_19.1.temp = alloca { i32, i1 }, align 8, !dbg !8
+// CHECK:STDOUT:   %.loc17_19.1.temp = alloca <{ i32, i1 }>, align 8, !dbg !8
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc16_43.3.temp), !dbg !7
 // CHECK:STDOUT:   %.loc16_43.4.array.index = getelementptr inbounds [6 x i32], ptr %.loc16_43.3.temp, i32 0, i64 0, !dbg !7
 // CHECK:STDOUT:   %.loc16_43.7.array.index = getelementptr inbounds [6 x i32], ptr %.loc16_43.3.temp, i32 0, i64 1, !dbg !7
@@ -108,7 +108,7 @@ fn F() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNext.7711e6f0e1563534:Iterate.Core.355bb079bb00aa16"(ptr sret({ i32, i1 }) %return, ptr %self, ptr %cursor) #0 !dbg !49 {
+// CHECK:STDOUT: define linkonce_odr void @"_CNext.7711e6f0e1563534:Iterate.Core.355bb079bb00aa16"(ptr sret(<{ i32, i1 }>) %return, ptr %self, ptr %cursor) #0 !dbg !49 {
 // CHECK:STDOUT:   %1 = load i32, ptr %cursor, align 4, !dbg !55
 // CHECK:STDOUT:   %2 = icmp slt i32 %1, 6, !dbg !55
 // CHECK:STDOUT:   br i1 %2, label %3, label %7, !dbg !56
@@ -146,20 +146,20 @@ fn F() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.b5e6acce74484d77(ptr sret({ i32, i1 }) %return, i32 %value) #0 !dbg !83 {
+// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.b5e6acce74484d77(ptr sret(<{ i32, i1 }>) %return, i32 %value) #0 !dbg !83 {
 // CHECK:STDOUT:   call void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr %return, i32 %value), !dbg !88
 // CHECK:STDOUT:   ret void, !dbg !89
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.b5e6acce74484d77(ptr sret({ i32, i1 }) %return) #0 !dbg !90 {
+// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.b5e6acce74484d77(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !90 {
 // CHECK:STDOUT:   call void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr %return), !dbg !93
 // CHECK:STDOUT:   ret void, !dbg !94
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i1 @"_CHas.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr %value) #0 !dbg !95 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %value, i32 0, i32 1, !dbg !98
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %value, i32 0, i32 1, !dbg !98
 // CHECK:STDOUT:   %1 = load i8, ptr %has_value, align 1, !dbg !98
 // CHECK:STDOUT:   %2 = trunc i8 %1 to i1, !dbg !98
 // CHECK:STDOUT:   ret i1 %2, !dbg !99
@@ -167,7 +167,7 @@ fn F() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i32 @"_CGet.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr %value) #0 !dbg !100 {
-// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw { i32, i1 }, ptr %value, i32 0, i32 0, !dbg !103
+// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw <{ i32, i1 }>, ptr %value, i32 0, i32 0, !dbg !103
 // CHECK:STDOUT:   %1 = load i32, ptr %value1, align 4, !dbg !103
 // CHECK:STDOUT:   ret i32 %1, !dbg !104
 // CHECK:STDOUT: }
@@ -182,17 +182,17 @@ fn F() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !113 {
-// CHECK:STDOUT:   %value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 0, !dbg !116
+// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !113 {
+// CHECK:STDOUT:   %value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 0, !dbg !116
 // CHECK:STDOUT:   store i32 %self, ptr %value, align 4, !dbg !116
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !117
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !117
 // CHECK:STDOUT:   store i8 1, ptr %has_value, align 1, !dbg !117
 // CHECK:STDOUT:   ret void, !dbg !118
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret({ i32, i1 }) %return) #0 !dbg !119 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !120
+// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !119 {
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !120
 // CHECK:STDOUT:   store i8 0, ptr %has_value, align 1, !dbg !120
 // CHECK:STDOUT:   ret void, !dbg !121
 // CHECK:STDOUT: }

+ 147 - 0
toolchain/lower/testdata/array/layout.carbon

@@ -0,0 +1,147 @@
+// 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
+//
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/int.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/array/layout.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/array/layout.carbon
+
+// --- element_tail_padding.carbon
+library "[[@TEST_NAME]]";
+
+fn Check(p: i32*);
+
+fn Test() {
+  // We insert tail padding after each element (including the last) to keep the
+  // array elements properly aligned and to ensure that a past-the-end address
+  // points within the overall object.
+  var v: array({.a: ()*, .b: i32}, 3);
+  Check(&v[0].b);
+  Check(&v[1].b);
+  Check(&v[2].b);
+}
+
+// --- constant_with_padding.carbon
+library "[[@TEST_NAME]]";
+
+var c: array(Core.Int(257), 5) = (1, 2, 3, 4, 5);
+
+// CHECK:STDOUT: ; ModuleID = 'element_tail_padding.carbon'
+// CHECK:STDOUT: source_filename = "element_tail_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CCheck.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CTest.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %v.var = alloca [3 x <{ <{ ptr, i32 }>, [4 x i8] }>], align 8, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %v.var), !dbg !7
+// CHECK:STDOUT:   %.loc10_13.array.index = getelementptr inbounds [3 x <{ <{ ptr, i32 }>, [4 x i8] }>], ptr %v.var, i32 0, i32 0, !dbg !8
+// CHECK:STDOUT:   %.loc10_14.b = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %.loc10_13.array.index, i32 0, i32 1, !dbg !8
+// CHECK:STDOUT:   call void @_CCheck.Main(ptr %.loc10_14.b), !dbg !9
+// CHECK:STDOUT:   %.loc11_13.array.index = getelementptr inbounds [3 x <{ <{ ptr, i32 }>, [4 x i8] }>], ptr %v.var, i32 0, i32 1, !dbg !10
+// CHECK:STDOUT:   %.loc11_14.b = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %.loc11_13.array.index, i32 0, i32 1, !dbg !10
+// CHECK:STDOUT:   call void @_CCheck.Main(ptr %.loc11_14.b), !dbg !11
+// CHECK:STDOUT:   %.loc12_13.array.index = getelementptr inbounds [3 x <{ <{ ptr, i32 }>, [4 x i8] }>], ptr %v.var, i32 0, i32 2, !dbg !12
+// CHECK:STDOUT:   %.loc12_14.b = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %.loc12_13.array.index, i32 0, i32 1, !dbg !12
+// CHECK:STDOUT:   call void @_CCheck.Main(ptr %.loc12_14.b), !dbg !13
+// CHECK:STDOUT:   call void @"_COp.876098026a9fba08:core.Destroy.Core"(ptr %v.var), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %self) #0 !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !21
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.5e9a928496027250:core.Destroy.Core"(ptr %self) #0 !dbg !22 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !28
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.876098026a9fba08:core.Destroy.Core"(ptr %self) #0 !dbg !29 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !32
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "element_tail_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "Test", linkageName: "_CTest.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 9, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 10, column: 10, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 10, column: 3, scope: !4)
+// CHECK:STDOUT: !10 = !DILocation(line: 11, column: 10, scope: !4)
+// CHECK:STDOUT: !11 = !DILocation(line: 11, column: 3, scope: !4)
+// CHECK:STDOUT: !12 = !DILocation(line: 12, column: 10, scope: !4)
+// CHECK:STDOUT: !13 = !DILocation(line: 12, column: 3, scope: !4)
+// CHECK:STDOUT: !14 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "Op", linkageName: "_COp.7e389eab4a7e5487:core.Destroy.Core", scope: null, file: !3, line: 9, type: !16, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !19)
+// CHECK:STDOUT: !16 = !DISubroutineType(types: !17)
+// CHECK:STDOUT: !17 = !{null, !18}
+// CHECK:STDOUT: !18 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !19 = !{!20}
+// CHECK:STDOUT: !20 = !DILocalVariable(arg: 1, scope: !15, type: !18)
+// CHECK:STDOUT: !21 = !DILocation(line: 9, column: 3, scope: !15)
+// CHECK:STDOUT: !22 = distinct !DISubprogram(name: "Op", linkageName: "_COp.5e9a928496027250:core.Destroy.Core", scope: null, file: !3, line: 9, type: !23, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !26)
+// CHECK:STDOUT: !23 = !DISubroutineType(types: !24)
+// CHECK:STDOUT: !24 = !{null, !25}
+// CHECK:STDOUT: !25 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !26 = !{!27}
+// CHECK:STDOUT: !27 = !DILocalVariable(arg: 1, scope: !22, type: !25)
+// CHECK:STDOUT: !28 = !DILocation(line: 9, column: 3, scope: !22)
+// CHECK:STDOUT: !29 = distinct !DISubprogram(name: "Op", linkageName: "_COp.876098026a9fba08:core.Destroy.Core", scope: null, file: !3, line: 9, type: !23, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !30)
+// CHECK:STDOUT: !30 = !{!31}
+// CHECK:STDOUT: !31 = !DILocalVariable(arg: 1, scope: !29, type: !25)
+// CHECK:STDOUT: !32 = !DILocation(line: 9, column: 3, scope: !29)
+// CHECK:STDOUT: ; ModuleID = 'constant_with_padding.carbon'
+// CHECK:STDOUT: source_filename = "constant_with_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: @_Cc.Main = global [5 x <{ i257, [28 x i8] }>] zeroinitializer
+// CHECK:STDOUT: @array.loc3_1 = internal constant [5 x <{ i257, [28 x i8] }>] [<{ i257, [28 x i8] }> <{ i257 1, [28 x i8] poison }>, <{ i257, [28 x i8] }> <{ i257 2, [28 x i8] poison }>, <{ i257, [28 x i8] }> <{ i257 3, [28 x i8] poison }>, <{ i257, [28 x i8] }> <{ i257 4, [28 x i8] poison }>, <{ i257, [28 x i8] }> <{ i257 5, [28 x i8] poison }>]
+// CHECK:STDOUT: @llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 0, ptr @_C__global_init.Main, ptr null }]
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define internal void @_C__global_init.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 @_Cc.Main, ptr align 1 @array.loc3_1, i64 320, i1 false), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !8
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.memcpy.p0.p0.i64(ptr noalias writeonly captures(none), ptr noalias readonly captures(none), i64, i1 immarg) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "constant_with_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "__global_init", linkageName: "_C__global_init.Main", scope: null, file: !3, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 3, column: 1, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 0, scope: !4)

+ 3 - 3
toolchain/lower/testdata/class/field.carbon

@@ -250,12 +250,12 @@ fn Run() {
 // CHECK:STDOUT: define void @main() #0 !dbg !4 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %o.var = alloca { i32 }, align 8, !dbg !7
-// CHECK:STDOUT:   %_.var = alloca { ptr, { i32 } }, align 8, !dbg !8
+// CHECK:STDOUT:   %_.var = alloca <{ ptr, { i32 } }>, align 8, !dbg !8
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %o.var), !dbg !7
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var), !dbg !8
-// CHECK:STDOUT:   %.loc17_40.2.a = getelementptr inbounds nuw { ptr, { i32 } }, ptr %_.var, i32 0, i32 0, !dbg !9
+// CHECK:STDOUT:   %.loc17_40.2.a = getelementptr inbounds nuw <{ ptr, { i32 } }>, ptr %_.var, i32 0, i32 0, !dbg !9
 // CHECK:STDOUT:   store ptr %o.var, ptr %.loc17_40.2.a, align 8, !dbg !9
-// CHECK:STDOUT:   %.loc17_40.4.b = getelementptr inbounds nuw { ptr, { i32 } }, ptr %_.var, i32 0, i32 1, !dbg !9
+// CHECK:STDOUT:   %.loc17_40.4.b = getelementptr inbounds nuw <{ ptr, { i32 } }>, ptr %_.var, i32 0, i32 1, !dbg !9
 // CHECK:STDOUT:   %.loc17_39.3.v = getelementptr inbounds nuw { i32 }, ptr %.loc17_40.4.b, i32 0, i32 0, !dbg !10
 // CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 4 %.loc17_40.4.b, ptr align 4 @Other.val.loc17_40.5, i64 4, i1 false), !dbg !9
 // CHECK:STDOUT:   call void @"_COp.6354d5454c7bf77d:core.Destroy.Core"(ptr %_.var), !dbg !8

+ 9 - 9
toolchain/lower/testdata/class/virtual.carbon

@@ -474,7 +474,7 @@ fn Make() {
 // CHECK:STDOUT: source_filename = "member_init.carbon"
 // CHECK:STDOUT:
 // CHECK:STDOUT: @"_CBase.MemberInit.$vtable" = unnamed_addr constant [1 x i32] [i32 trunc (i64 sub (i64 ptrtoint (ptr @_CFn.Base.MemberInit to i64), i64 ptrtoint (ptr @"_CBase.MemberInit.$vtable" to i64)) to i32)]
-// CHECK:STDOUT: @Base.val.loc13_3 = internal constant { ptr, i32 } { ptr @"_CBase.MemberInit.$vtable", i32 3 }
+// CHECK:STDOUT: @Base.val.loc13_3 = internal constant <{ ptr, i32 }> <{ ptr @"_CBase.MemberInit.$vtable", i32 3 }>
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_CFn.Base.MemberInit(ptr %self) #0 !dbg !4 {
@@ -486,22 +486,22 @@ fn Make() {
 // CHECK:STDOUT: define void @_CFn.MemberInit() #0 !dbg !11 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %i.var = alloca i32, align 4, !dbg !14
-// CHECK:STDOUT:   %v.var = alloca { ptr, i32 }, align 8, !dbg !15
-// CHECK:STDOUT:   %_.var = alloca { ptr, i32 }, align 8, !dbg !16
+// CHECK:STDOUT:   %v.var = alloca <{ ptr, i32 }>, align 8, !dbg !15
+// CHECK:STDOUT:   %_.var = alloca <{ ptr, i32 }>, align 8, !dbg !16
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %i.var), !dbg !14
 // CHECK:STDOUT:   store i32 3, ptr %i.var, align 4, !dbg !14
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %v.var), !dbg !15
-// CHECK:STDOUT:   %.loc11_24.2.vptr = getelementptr inbounds nuw { ptr, i32 }, ptr %v.var, i32 0, i32 0, !dbg !17
+// CHECK:STDOUT:   %.loc11_24.2.vptr = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %v.var, i32 0, i32 0, !dbg !17
 // CHECK:STDOUT:   %.loc11_23 = load i32, ptr %i.var, align 4, !dbg !18
-// CHECK:STDOUT:   %.loc11_24.4.m = getelementptr inbounds nuw { ptr, i32 }, ptr %v.var, i32 0, i32 1, !dbg !17
+// CHECK:STDOUT:   %.loc11_24.4.m = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %v.var, i32 0, i32 1, !dbg !17
 // CHECK:STDOUT:   store i32 %.loc11_23, ptr %.loc11_24.4.m, align 4, !dbg !17
 // CHECK:STDOUT:   store ptr @"_CBase.MemberInit.$vtable", ptr %.loc11_24.2.vptr, align 8, !dbg !17
-// CHECK:STDOUT:   %.loc12_4.m = getelementptr inbounds nuw { ptr, i32 }, ptr %v.var, i32 0, i32 1, !dbg !19
+// CHECK:STDOUT:   %.loc12_4.m = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %v.var, i32 0, i32 1, !dbg !19
 // CHECK:STDOUT:   store i32 5, ptr %.loc12_4.m, align 4, !dbg !19
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var), !dbg !16
-// CHECK:STDOUT:   %.loc13_24.2.vptr = getelementptr inbounds nuw { ptr, i32 }, ptr %_.var, i32 0, i32 0, !dbg !20
-// CHECK:STDOUT:   %.loc13_24.5.m = getelementptr inbounds nuw { ptr, i32 }, ptr %_.var, i32 0, i32 1, !dbg !20
-// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 8 %_.var, ptr align 8 @Base.val.loc13_3, i64 16, i1 false), !dbg !16
+// CHECK:STDOUT:   %.loc13_24.2.vptr = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %_.var, i32 0, i32 0, !dbg !20
+// CHECK:STDOUT:   %.loc13_24.5.m = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %_.var, i32 0, i32 1, !dbg !20
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %_.var, ptr align 1 @Base.val.loc13_3, i64 12, i1 false), !dbg !16
 // CHECK:STDOUT:   call void @"_COp.dbf7676be30a7aa8:core.Destroy.Core"(ptr %_.var), !dbg !16
 // CHECK:STDOUT:   call void @"_COp.dbf7676be30a7aa8:core.Destroy.Core"(ptr %v.var), !dbg !15
 // CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %i.var), !dbg !14

+ 7 - 7
toolchain/lower/testdata/for/bindings.carbon

@@ -43,7 +43,7 @@ fn For() {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %r.var = alloca {}, align 8, !dbg !7
 // CHECK:STDOUT:   %var = alloca {}, align 8, !dbg !8
-// CHECK:STDOUT:   %.loc29_33.1.temp = alloca { { i32, i32 }, i1 }, align 8, !dbg !8
+// CHECK:STDOUT:   %.loc29_33.1.temp = alloca <{ { i32, i32 }, i1 }>, align 8, !dbg !8
 // CHECK:STDOUT:   %n.var = alloca i32, align 4, !dbg !9
 // CHECK:STDOUT:   %.loc29_33.8.temp = alloca { i32, i32 }, align 8, !dbg !8
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %r.var), !dbg !7
@@ -133,7 +133,7 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNext.EmptyRange.1ccc388edee5a786.Main:Iterate.Core.8d745d19e7d6c4bd"(ptr sret({ { i32, i32 }, i1 }) %return, ptr %self, ptr %_) #0 !dbg !51 {
+// CHECK:STDOUT: define linkonce_odr void @"_CNext.EmptyRange.1ccc388edee5a786.Main:Iterate.Core.8d745d19e7d6c4bd"(ptr sret(<{ { i32, i32 }, i1 }>) %return, ptr %self, ptr %_) #0 !dbg !51 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   call void @_CNone.Optional.Core.6b64456bb8003a40(ptr %return), !dbg !57
 // CHECK:STDOUT:   ret void, !dbg !58
@@ -152,14 +152,14 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.6b64456bb8003a40(ptr sret({ { i32, i32 }, i1 }) %return) #0 !dbg !72 {
+// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.6b64456bb8003a40(ptr sret(<{ { i32, i32 }, i1 }>) %return) #0 !dbg !72 {
 // CHECK:STDOUT:   call void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.8d745d19e7d6c4bd"(ptr %return), !dbg !75
 // CHECK:STDOUT:   ret void, !dbg !76
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i1 @"_CHas.3e8267224c5dc9c2:OptionalStorage.Core.8d745d19e7d6c4bd"(ptr %value) #0 !dbg !77 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { { i32, i32 }, i1 }, ptr %value, i32 0, i32 1, !dbg !80
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ { i32, i32 }, i1 }>, ptr %value, i32 0, i32 1, !dbg !80
 // CHECK:STDOUT:   %1 = load i8, ptr %has_value, align 1, !dbg !80
 // CHECK:STDOUT:   %2 = trunc i8 %1 to i1, !dbg !80
 // CHECK:STDOUT:   ret i1 %2, !dbg !81
@@ -167,14 +167,14 @@ fn For() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr void @"_CGet.3e8267224c5dc9c2:OptionalStorage.Core.8d745d19e7d6c4bd"(ptr sret({ i32, i32 }) %return, ptr %value) #0 !dbg !82 {
-// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw { { i32, i32 }, i1 }, ptr %value, i32 0, i32 0, !dbg !85
+// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw <{ { i32, i32 }, i1 }>, ptr %value, i32 0, i32 0, !dbg !85
 // CHECK:STDOUT:   call void @"_COp.e4daa70d026fdea2:Copy.Core.3e36085cb869f630"(ptr %return, ptr %value1), !dbg !85
 // CHECK:STDOUT:   ret void, !dbg !86
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.8d745d19e7d6c4bd"(ptr sret({ { i32, i32 }, i1 }) %return) #0 !dbg !87 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { { i32, i32 }, i1 }, ptr %return, i32 0, i32 1, !dbg !88
+// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.8d745d19e7d6c4bd"(ptr sret(<{ { i32, i32 }, i1 }>) %return) #0 !dbg !87 {
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ { i32, i32 }, i1 }>, ptr %return, i32 0, i32 1, !dbg !88
 // CHECK:STDOUT:   store i8 0, ptr %has_value, align 1, !dbg !88
 // CHECK:STDOUT:   ret void, !dbg !89
 // CHECK:STDOUT: }

+ 11 - 11
toolchain/lower/testdata/for/break_continue.carbon

@@ -38,7 +38,7 @@ fn For() {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %.loc20_32.1.temp = alloca { i32, i32 }, align 8, !dbg !7
 // CHECK:STDOUT:   %var = alloca i32, align 4, !dbg !8
-// CHECK:STDOUT:   %.loc20_33.1.temp = alloca { i32, i1 }, align 8, !dbg !8
+// CHECK:STDOUT:   %.loc20_33.1.temp = alloca <{ i32, i1 }>, align 8, !dbg !8
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc20_32.1.temp), !dbg !7
 // CHECK:STDOUT:   call void @_CRange.Core(ptr %.loc20_32.1.temp, i32 100), !dbg !7
 // CHECK:STDOUT:   %IntRange.as.Iterate.impl.NewCursor.call = call i32 @"_CNewCursor.IntRange.9452f4c51951679b.Core:Iterate.Core.be1e879c1ad406d8"(ptr %.loc20_32.1.temp), !dbg !8
@@ -133,7 +133,7 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNext.IntRange.9452f4c51951679b.Core:Iterate.Core.be1e879c1ad406d8"(ptr sret({ i32, i1 }) %return, ptr %self, ptr %cursor) #0 !dbg !60 {
+// CHECK:STDOUT: define linkonce_odr void @"_CNext.IntRange.9452f4c51951679b.Core:Iterate.Core.be1e879c1ad406d8"(ptr sret(<{ i32, i1 }>) %return, ptr %self, ptr %cursor) #0 !dbg !60 {
 // CHECK:STDOUT:   %1 = alloca i32, align 4, !dbg !66
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %1), !dbg !66
 // CHECK:STDOUT:   %2 = load i32, ptr %cursor, align 4, !dbg !67
@@ -178,20 +178,20 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.a0986cb4211ec3fb(ptr sret({ i32, i1 }) %return, i32 %value) #0 !dbg !96 {
+// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.a0986cb4211ec3fb(ptr sret(<{ i32, i1 }>) %return, i32 %value) #0 !dbg !96 {
 // CHECK:STDOUT:   call void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %return, i32 %value), !dbg !101
 // CHECK:STDOUT:   ret void, !dbg !102
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.a0986cb4211ec3fb(ptr sret({ i32, i1 }) %return) #0 !dbg !103 {
+// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.a0986cb4211ec3fb(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !103 {
 // CHECK:STDOUT:   call void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %return), !dbg !106
 // CHECK:STDOUT:   ret void, !dbg !107
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i1 @"_CHas.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %value) #0 !dbg !108 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %value, i32 0, i32 1, !dbg !111
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %value, i32 0, i32 1, !dbg !111
 // CHECK:STDOUT:   %1 = load i8, ptr %has_value, align 1, !dbg !111
 // CHECK:STDOUT:   %2 = trunc i8 %1 to i1, !dbg !111
 // CHECK:STDOUT:   ret i1 %2, !dbg !112
@@ -199,7 +199,7 @@ fn For() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i32 @"_CGet.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %value) #0 !dbg !113 {
-// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw { i32, i1 }, ptr %value, i32 0, i32 0, !dbg !116
+// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw <{ i32, i1 }>, ptr %value, i32 0, i32 0, !dbg !116
 // CHECK:STDOUT:   %1 = load i32, ptr %value1, align 4, !dbg !116
 // CHECK:STDOUT:   ret i32 %1, !dbg !117
 // CHECK:STDOUT: }
@@ -214,17 +214,17 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !126 {
-// CHECK:STDOUT:   %value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 0, !dbg !129
+// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !126 {
+// CHECK:STDOUT:   %value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 0, !dbg !129
 // CHECK:STDOUT:   store i32 %self, ptr %value, align 4, !dbg !129
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !130
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !130
 // CHECK:STDOUT:   store i8 1, ptr %has_value, align 1, !dbg !130
 // CHECK:STDOUT:   ret void, !dbg !131
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret({ i32, i1 }) %return) #0 !dbg !132 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !133
+// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !132 {
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !133
 // CHECK:STDOUT:   store i8 0, ptr %has_value, align 1, !dbg !133
 // CHECK:STDOUT:   ret void, !dbg !134
 // CHECK:STDOUT: }

+ 11 - 11
toolchain/lower/testdata/for/for.carbon

@@ -38,7 +38,7 @@ fn For() {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %.loc21_32.1.temp = alloca { i32, i32 }, align 8, !dbg !7
 // CHECK:STDOUT:   %var = alloca i32, align 4, !dbg !8
-// CHECK:STDOUT:   %.loc21_33.1.temp = alloca { i32, i1 }, align 8, !dbg !8
+// CHECK:STDOUT:   %.loc21_33.1.temp = alloca <{ i32, i1 }>, align 8, !dbg !8
 // CHECK:STDOUT:   call void @_CF.Main(), !dbg !9
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc21_32.1.temp), !dbg !7
 // CHECK:STDOUT:   call void @_CRange.Core(ptr %.loc21_32.1.temp, i32 100), !dbg !7
@@ -121,7 +121,7 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNext.IntRange.9452f4c51951679b.Core:Iterate.Core.be1e879c1ad406d8"(ptr sret({ i32, i1 }) %return, ptr %self, ptr %cursor) #0 !dbg !56 {
+// CHECK:STDOUT: define linkonce_odr void @"_CNext.IntRange.9452f4c51951679b.Core:Iterate.Core.be1e879c1ad406d8"(ptr sret(<{ i32, i1 }>) %return, ptr %self, ptr %cursor) #0 !dbg !56 {
 // CHECK:STDOUT:   %1 = alloca i32, align 4, !dbg !62
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %1), !dbg !62
 // CHECK:STDOUT:   %2 = load i32, ptr %cursor, align 4, !dbg !63
@@ -166,20 +166,20 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.a0986cb4211ec3fb(ptr sret({ i32, i1 }) %return, i32 %value) #0 !dbg !92 {
+// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.a0986cb4211ec3fb(ptr sret(<{ i32, i1 }>) %return, i32 %value) #0 !dbg !92 {
 // CHECK:STDOUT:   call void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %return, i32 %value), !dbg !97
 // CHECK:STDOUT:   ret void, !dbg !98
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.a0986cb4211ec3fb(ptr sret({ i32, i1 }) %return) #0 !dbg !99 {
+// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.a0986cb4211ec3fb(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !99 {
 // CHECK:STDOUT:   call void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %return), !dbg !102
 // CHECK:STDOUT:   ret void, !dbg !103
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i1 @"_CHas.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %value) #0 !dbg !104 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %value, i32 0, i32 1, !dbg !107
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %value, i32 0, i32 1, !dbg !107
 // CHECK:STDOUT:   %1 = load i8, ptr %has_value, align 1, !dbg !107
 // CHECK:STDOUT:   %2 = trunc i8 %1 to i1, !dbg !107
 // CHECK:STDOUT:   ret i1 %2, !dbg !108
@@ -187,7 +187,7 @@ fn For() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define linkonce_odr i32 @"_CGet.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr %value) #0 !dbg !109 {
-// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw { i32, i1 }, ptr %value, i32 0, i32 0, !dbg !112
+// CHECK:STDOUT:   %value1 = getelementptr inbounds nuw <{ i32, i1 }>, ptr %value, i32 0, i32 0, !dbg !112
 // CHECK:STDOUT:   %1 = load i32, ptr %value1, align 4, !dbg !112
 // CHECK:STDOUT:   ret i32 %1, !dbg !113
 // CHECK:STDOUT: }
@@ -202,17 +202,17 @@ fn For() {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !122 {
-// CHECK:STDOUT:   %value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 0, !dbg !125
+// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !122 {
+// CHECK:STDOUT:   %value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 0, !dbg !125
 // CHECK:STDOUT:   store i32 %self, ptr %value, align 4, !dbg !125
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !126
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !126
 // CHECK:STDOUT:   store i8 1, ptr %has_value, align 1, !dbg !126
 // CHECK:STDOUT:   ret void, !dbg !127
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret({ i32, i1 }) %return) #0 !dbg !128 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !129
+// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.0b1ea4541e2057b5"(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !128 {
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !129
 // CHECK:STDOUT:   store i8 0, ptr %has_value, align 1, !dbg !129
 // CHECK:STDOUT:   ret void, !dbg !130
 // CHECK:STDOUT: }

+ 30 - 30
toolchain/lower/testdata/primitives/optional.carbon

@@ -41,7 +41,7 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define void @_CConvert.Main(ptr sret({ i32, i1 }) %return, ptr %o) #0 !dbg !11 {
+// CHECK:STDOUT: define void @_CConvert.Main(ptr sret(<{ i32, i1 }>) %return, ptr %o) #0 !dbg !11 {
 // CHECK:STDOUT: entry:
 // CHECK:STDOUT:   %Optional.HasValue.call = call i1 @_CHasValue.Optional.Core.03e3348579103d79(ptr %o), !dbg !17
 // CHECK:STDOUT:   br i1 %Optional.HasValue.call, label %if.then, label %if.else, !dbg !18
@@ -60,14 +60,14 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_CAddOrRemoveConst.Main(i32 %a, i32 %b) #0 !dbg !24 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %_.var.loc24 = alloca { i32, i1 }, align 8, !dbg !30
-// CHECK:STDOUT:   %_.var.loc25 = alloca { i32, i1 }, align 8, !dbg !31
-// CHECK:STDOUT:   %_.var.loc26 = alloca { i32, i1 }, align 8, !dbg !32
-// CHECK:STDOUT:   %_.var.loc27 = alloca { i32, i1 }, align 8, !dbg !33
-// CHECK:STDOUT:   %_.var.loc28 = alloca { i32, i1 }, align 8, !dbg !34
-// CHECK:STDOUT:   %_.var.loc29 = alloca { i32, i1 }, align 8, !dbg !35
-// CHECK:STDOUT:   %_.var.loc30 = alloca { i32, i1 }, align 8, !dbg !36
-// CHECK:STDOUT:   %_.var.loc31 = alloca { i32, i1 }, align 8, !dbg !37
+// CHECK:STDOUT:   %_.var.loc24 = alloca <{ i32, i1 }>, align 8, !dbg !30
+// CHECK:STDOUT:   %_.var.loc25 = alloca <{ i32, i1 }>, align 8, !dbg !31
+// CHECK:STDOUT:   %_.var.loc26 = alloca <{ i32, i1 }>, align 8, !dbg !32
+// CHECK:STDOUT:   %_.var.loc27 = alloca <{ i32, i1 }>, align 8, !dbg !33
+// CHECK:STDOUT:   %_.var.loc28 = alloca <{ i32, i1 }>, align 8, !dbg !34
+// CHECK:STDOUT:   %_.var.loc29 = alloca <{ i32, i1 }>, align 8, !dbg !35
+// CHECK:STDOUT:   %_.var.loc30 = alloca <{ i32, i1 }>, align 8, !dbg !36
+// CHECK:STDOUT:   %_.var.loc31 = alloca <{ i32, i1 }>, align 8, !dbg !37
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc24), !dbg !30
 // CHECK:STDOUT:   call void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.6b1f19368d09e579"(ptr %_.var.loc24, i32 %a), !dbg !30
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc25), !dbg !31
@@ -174,13 +174,13 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.6b1f19368d09e579"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !96 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.6b1f19368d09e579"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !96 {
 // CHECK:STDOUT:   call void @"_CConvert.90961d7b1ce4f089:OptionalAs.0e326e799dad0c64.Core.30ced37eb1e63547"(ptr %return, i32 %self), !dbg !101
 // CHECK:STDOUT:   ret void, !dbg !102
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.30ced37eb1e63547(ptr sret({ i32, i1 }) %return) #0 !dbg !103 {
+// CHECK:STDOUT: define linkonce_odr void @_CNone.Optional.Core.30ced37eb1e63547(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !103 {
 // CHECK:STDOUT:   call void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr %return), !dbg !106
 // CHECK:STDOUT:   ret void, !dbg !107
 // CHECK:STDOUT: }
@@ -189,43 +189,43 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.77b263a5fd240ddf"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !108 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.77b263a5fd240ddf"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !108 {
 // CHECK:STDOUT:   call void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.6b1f19368d09e579"(ptr %return, i32 %self), !dbg !112
 // CHECK:STDOUT:   ret void, !dbg !113
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.787d589b54a95071"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !114 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.787d589b54a95071"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !114 {
 // CHECK:STDOUT:   call void @"_CConvert.3667eb502d1ddfa9:OptionalAs.c7a50af9bdd61b43.Core.5d35ad54738af8e4"(ptr %return, i32 %self), !dbg !117
 // CHECK:STDOUT:   ret void, !dbg !118
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.25fb32d66f981ccd"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !119 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.25fb32d66f981ccd"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !119 {
 // CHECK:STDOUT:   call void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.787d589b54a95071"(ptr %return, i32 %self), !dbg !122
 // CHECK:STDOUT:   ret void, !dbg !123
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.77b263a5fd240ddf"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !124 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.77b263a5fd240ddf"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !124 {
 // CHECK:STDOUT:   call void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.6b1f19368d09e579"(ptr %return, i32 %self), !dbg !127
 // CHECK:STDOUT:   ret void, !dbg !128
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.693532dcb04bf6dc"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !129 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.693532dcb04bf6dc"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !129 {
 // CHECK:STDOUT:   call void @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.77b263a5fd240ddf"(ptr %return, i32 %self), !dbg !132
 // CHECK:STDOUT:   ret void, !dbg !133
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.25fb32d66f981ccd"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !134 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.25fb32d66f981ccd"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !134 {
 // CHECK:STDOUT:   call void @"_CConvert.e5cf8fcbb4feaae2:ImplicitAs.0f95c9e18c91e00a.Core.787d589b54a95071"(ptr %return, i32 %self), !dbg !137
 // CHECK:STDOUT:   ret void, !dbg !138
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.1c991c9fdfba671f"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !139 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.17f42c13d842b71d:ImplicitAs.eb057aa32837c84e.Core.1c991c9fdfba671f"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !139 {
 // CHECK:STDOUT:   call void @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.25fb32d66f981ccd"(ptr %return, i32 %self), !dbg !142
 // CHECK:STDOUT:   ret void, !dbg !143
 // CHECK:STDOUT: }
@@ -243,20 +243,20 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.90961d7b1ce4f089:OptionalAs.0e326e799dad0c64.Core.30ced37eb1e63547"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !154 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.90961d7b1ce4f089:OptionalAs.0e326e799dad0c64.Core.30ced37eb1e63547"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !154 {
 // CHECK:STDOUT:   call void @_CSome.Optional.Core.30ced37eb1e63547(ptr %return, i32 %self), !dbg !157
 // CHECK:STDOUT:   ret void, !dbg !158
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret({ i32, i1 }) %return) #0 !dbg !159 {
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !160
+// CHECK:STDOUT: define linkonce_odr void @"_CNone.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret(<{ i32, i1 }>) %return) #0 !dbg !159 {
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !160
 // CHECK:STDOUT:   store i8 0, ptr %has_value, align 1, !dbg !160
 // CHECK:STDOUT:   ret void, !dbg !161
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CConvert.3667eb502d1ddfa9:OptionalAs.c7a50af9bdd61b43.Core.5d35ad54738af8e4"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !162 {
+// CHECK:STDOUT: define linkonce_odr void @"_CConvert.3667eb502d1ddfa9:OptionalAs.c7a50af9bdd61b43.Core.5d35ad54738af8e4"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !162 {
 // CHECK:STDOUT:   %temp = alloca i32, align 4, !dbg !165
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %temp), !dbg !165
 // CHECK:STDOUT:   %1 = call i32 @"_CConvert.a44fce96e16342e7:ImplicitAs.ad22d1bbc0605210.Core.90a4f5ca3e06badd"(i32 %self), !dbg !165
@@ -268,7 +268,7 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.30ced37eb1e63547(ptr sret({ i32, i1 }) %return, i32 %value) #0 !dbg !168 {
+// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.30ced37eb1e63547(ptr sret(<{ i32, i1 }>) %return, i32 %value) #0 !dbg !168 {
 // CHECK:STDOUT:   call void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr %return, i32 %value), !dbg !171
 // CHECK:STDOUT:   ret void, !dbg !172
 // CHECK:STDOUT: }
@@ -280,16 +280,16 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.b0246d3dd29c9210(ptr sret({ i32, i1 }) %return, i32 %value) #0 !dbg !178 {
+// CHECK:STDOUT: define linkonce_odr void @_CSome.Optional.Core.b0246d3dd29c9210(ptr sret(<{ i32, i1 }>) %return, i32 %value) #0 !dbg !178 {
 // CHECK:STDOUT:   call void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.5325ffa2b57e13bd"(ptr %return, i32 %value), !dbg !181
 // CHECK:STDOUT:   ret void, !dbg !182
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !183 {
-// CHECK:STDOUT:   %value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 0, !dbg !186
+// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.bfb441135f07cbe1"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !183 {
+// CHECK:STDOUT:   %value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 0, !dbg !186
 // CHECK:STDOUT:   store i32 %self, ptr %value, align 4, !dbg !186
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !187
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !187
 // CHECK:STDOUT:   store i8 1, ptr %has_value, align 1, !dbg !187
 // CHECK:STDOUT:   ret void, !dbg !188
 // CHECK:STDOUT: }
@@ -301,11 +301,11 @@ fn AddOrRemoveConst(a: i32, b: const i32) {
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
-// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.5325ffa2b57e13bd"(ptr sret({ i32, i1 }) %return, i32 %self) #0 !dbg !197 {
-// CHECK:STDOUT:   %value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 0, !dbg !200
+// CHECK:STDOUT: define linkonce_odr void @"_CSome.3e8267224c5dc9c2:OptionalStorage.Core.5325ffa2b57e13bd"(ptr sret(<{ i32, i1 }>) %return, i32 %self) #0 !dbg !197 {
+// CHECK:STDOUT:   %value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 0, !dbg !200
 // CHECK:STDOUT:   %1 = call i32 @"_COp.34e7a685d3648824:Copy.Core.64ccbb8e5d9a0b8e"(i32 %self), !dbg !201
 // CHECK:STDOUT:   store i32 %1, ptr %value, align 4, !dbg !200
-// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw { i32, i1 }, ptr %return, i32 0, i32 1, !dbg !202
+// CHECK:STDOUT:   %has_value = getelementptr inbounds nuw <{ i32, i1 }>, ptr %return, i32 0, i32 1, !dbg !202
 // CHECK:STDOUT:   store i8 1, ptr %has_value, align 1, !dbg !202
 // CHECK:STDOUT:   ret void, !dbg !203
 // CHECK:STDOUT: }

+ 433 - 0
toolchain/lower/testdata/struct/layout.carbon

@@ -0,0 +1,433 @@
+// 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
+//
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/int.carbon
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/struct/layout.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/struct/layout.carbon
+
+// --- no_padding.carbon
+library "[[@TEST_NAME]]";
+
+fn G(p: i32*);
+
+fn F() {
+  // LLVM {i32, i32, i32} matches the Carbon layout, so we use it.
+  var no_padding: {.x: i32, .y: i32, .z: i32};
+  G(&no_padding.x);
+  G(&no_padding.y);
+  G(&no_padding.z);
+}
+
+// --- avoid_tail_padding.carbon
+library "[[@TEST_NAME]]";
+
+fn G(p: i32*);
+
+fn F() {
+  // LLVM {ptr, i32} would have a size of 16, so we use a packed struct.
+  var avoid_tail_padding: {.x: ()*, .y: i32};
+  G(&avoid_tail_padding.y);
+}
+
+// --- use_tail_padding.carbon
+library "[[@TEST_NAME]]";
+
+fn G(p: i32*);
+
+fn F() {
+  // LLVM {ptr, i32} would have a size of 16, so we use a packed struct as the
+  // type of `.a`. `.b` goes in the tail padding.
+  var use_tail_padding: {.a: {.x: ()*, .y: i32}, .b: i32};
+  G(&use_tail_padding.a.y);
+  G(&use_tail_padding.b);
+}
+
+// --- implicit_padding.carbon
+library "[[@TEST_NAME]]";
+
+fn G(p: ()**);
+
+fn F() {
+  // LLVM {<{ptr, i32}>, ptr} inserts the right padding before `.b`, so we use
+  // it directly.
+  var implicit_padding: {.a: {.x: ()*, .y: i32}, .b: ()*};
+  G(&implicit_padding.a.x);
+  G(&implicit_padding.b);
+}
+
+// --- insert_padding.carbon
+library "[[@TEST_NAME]]";
+
+fn G(p: ()**);
+
+fn F() {
+  // LLVM {<{i32, i32, i32}>, ptr, i32} would have extra tail padding, so we use
+  // a packed struct. That requires that we explicitly add padding between `.a`
+  // and `.b`.
+  var insert_padding: {.a: {.x: i32, .y: i32, .z: i32}, .b: ()*, .c: i32};
+  G(&insert_padding.b);
+}
+
+// CHECK:STDOUT: ; ModuleID = 'no_padding.carbon'
+// CHECK:STDOUT: source_filename = "no_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CG.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CF.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %no_padding.var = alloca { i32, i32, i32 }, align 8, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %no_padding.var), !dbg !7
+// CHECK:STDOUT:   %.loc8.x = getelementptr inbounds nuw { i32, i32, i32 }, ptr %no_padding.var, i32 0, i32 0, !dbg !8
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc8.x), !dbg !9
+// CHECK:STDOUT:   %.loc9.y = getelementptr inbounds nuw { i32, i32, i32 }, ptr %no_padding.var, i32 0, i32 1, !dbg !10
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc9.y), !dbg !11
+// CHECK:STDOUT:   %.loc10.z = getelementptr inbounds nuw { i32, i32, i32 }, ptr %no_padding.var, i32 0, i32 2, !dbg !12
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc10.z), !dbg !13
+// CHECK:STDOUT:   call void @"_COp.3124a28397dbe9e9:core.Destroy.Core"(ptr %no_padding.var), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %self) #0 !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !21
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.3124a28397dbe9e9:core.Destroy.Core"(ptr %self) #0 !dbg !22 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !28
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "no_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 7, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 8, column: 6, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 8, column: 3, scope: !4)
+// CHECK:STDOUT: !10 = !DILocation(line: 9, column: 6, scope: !4)
+// CHECK:STDOUT: !11 = !DILocation(line: 9, column: 3, scope: !4)
+// CHECK:STDOUT: !12 = !DILocation(line: 10, column: 6, scope: !4)
+// CHECK:STDOUT: !13 = !DILocation(line: 10, column: 3, scope: !4)
+// CHECK:STDOUT: !14 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "Op", linkageName: "_COp.7e389eab4a7e5487:core.Destroy.Core", scope: null, file: !3, line: 7, type: !16, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !19)
+// CHECK:STDOUT: !16 = !DISubroutineType(types: !17)
+// CHECK:STDOUT: !17 = !{null, !18}
+// CHECK:STDOUT: !18 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !19 = !{!20}
+// CHECK:STDOUT: !20 = !DILocalVariable(arg: 1, scope: !15, type: !18)
+// CHECK:STDOUT: !21 = !DILocation(line: 7, column: 3, scope: !15)
+// CHECK:STDOUT: !22 = distinct !DISubprogram(name: "Op", linkageName: "_COp.3124a28397dbe9e9:core.Destroy.Core", scope: null, file: !3, line: 7, type: !23, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !26)
+// CHECK:STDOUT: !23 = !DISubroutineType(types: !24)
+// CHECK:STDOUT: !24 = !{null, !25}
+// CHECK:STDOUT: !25 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !26 = !{!27}
+// CHECK:STDOUT: !27 = !DILocalVariable(arg: 1, scope: !22, type: !25)
+// CHECK:STDOUT: !28 = !DILocation(line: 7, column: 3, scope: !22)
+// CHECK:STDOUT: ; ModuleID = 'avoid_tail_padding.carbon'
+// CHECK:STDOUT: source_filename = "avoid_tail_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CG.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CF.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %avoid_tail_padding.var = alloca <{ ptr, i32 }>, align 8, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %avoid_tail_padding.var), !dbg !7
+// CHECK:STDOUT:   %.loc8.y = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %avoid_tail_padding.var, i32 0, i32 1, !dbg !8
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc8.y), !dbg !9
+// CHECK:STDOUT:   call void @"_COp.0d530a9b810c77a1:core.Destroy.Core"(ptr %avoid_tail_padding.var), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !10
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %self) #0 !dbg !11 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !17
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.0d530a9b810c77a1:core.Destroy.Core"(ptr %self) #0 !dbg !18 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !24
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "avoid_tail_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 7, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 8, column: 6, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 8, column: 3, scope: !4)
+// CHECK:STDOUT: !10 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: !11 = distinct !DISubprogram(name: "Op", linkageName: "_COp.7e389eab4a7e5487:core.Destroy.Core", scope: null, file: !3, line: 7, type: !12, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !15)
+// CHECK:STDOUT: !12 = !DISubroutineType(types: !13)
+// CHECK:STDOUT: !13 = !{null, !14}
+// CHECK:STDOUT: !14 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !15 = !{!16}
+// CHECK:STDOUT: !16 = !DILocalVariable(arg: 1, scope: !11, type: !14)
+// CHECK:STDOUT: !17 = !DILocation(line: 7, column: 3, scope: !11)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "Op", linkageName: "_COp.0d530a9b810c77a1:core.Destroy.Core", scope: null, file: !3, line: 7, type: !19, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !22)
+// CHECK:STDOUT: !19 = !DISubroutineType(types: !20)
+// CHECK:STDOUT: !20 = !{null, !21}
+// CHECK:STDOUT: !21 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !22 = !{!23}
+// CHECK:STDOUT: !23 = !DILocalVariable(arg: 1, scope: !18, type: !21)
+// CHECK:STDOUT: !24 = !DILocation(line: 7, column: 3, scope: !18)
+// CHECK:STDOUT: ; ModuleID = 'use_tail_padding.carbon'
+// CHECK:STDOUT: source_filename = "use_tail_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CG.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CF.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %use_tail_padding.var = alloca { <{ ptr, i32 }>, i32 }, align 8, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %use_tail_padding.var), !dbg !7
+// CHECK:STDOUT:   %.loc9_22.a = getelementptr inbounds nuw { <{ ptr, i32 }>, i32 }, ptr %use_tail_padding.var, i32 0, i32 0, !dbg !8
+// CHECK:STDOUT:   %.loc9_24.y = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %.loc9_22.a, i32 0, i32 1, !dbg !8
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc9_24.y), !dbg !9
+// CHECK:STDOUT:   %.loc10.b = getelementptr inbounds nuw { <{ ptr, i32 }>, i32 }, ptr %use_tail_padding.var, i32 0, i32 1, !dbg !10
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc10.b), !dbg !11
+// CHECK:STDOUT:   call void @"_COp.2106667af8a21f02:core.Destroy.Core"(ptr %use_tail_padding.var), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !12
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %self) #0 !dbg !13 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !19
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.0d530a9b810c77a1:core.Destroy.Core"(ptr %self) #0 !dbg !20 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !26
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.2106667af8a21f02:core.Destroy.Core"(ptr %self) #0 !dbg !27 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !30
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "use_tail_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 8, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 9, column: 6, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 9, column: 3, scope: !4)
+// CHECK:STDOUT: !10 = !DILocation(line: 10, column: 6, scope: !4)
+// CHECK:STDOUT: !11 = !DILocation(line: 10, column: 3, scope: !4)
+// CHECK:STDOUT: !12 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: !13 = distinct !DISubprogram(name: "Op", linkageName: "_COp.7e389eab4a7e5487:core.Destroy.Core", scope: null, file: !3, line: 8, type: !14, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !17)
+// CHECK:STDOUT: !14 = !DISubroutineType(types: !15)
+// CHECK:STDOUT: !15 = !{null, !16}
+// CHECK:STDOUT: !16 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !17 = !{!18}
+// CHECK:STDOUT: !18 = !DILocalVariable(arg: 1, scope: !13, type: !16)
+// CHECK:STDOUT: !19 = !DILocation(line: 8, column: 3, scope: !13)
+// CHECK:STDOUT: !20 = distinct !DISubprogram(name: "Op", linkageName: "_COp.0d530a9b810c77a1:core.Destroy.Core", scope: null, file: !3, line: 8, type: !21, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !24)
+// CHECK:STDOUT: !21 = !DISubroutineType(types: !22)
+// CHECK:STDOUT: !22 = !{null, !23}
+// CHECK:STDOUT: !23 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !24 = !{!25}
+// CHECK:STDOUT: !25 = !DILocalVariable(arg: 1, scope: !20, type: !23)
+// CHECK:STDOUT: !26 = !DILocation(line: 8, column: 3, scope: !20)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "Op", linkageName: "_COp.2106667af8a21f02:core.Destroy.Core", scope: null, file: !3, line: 8, type: !21, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !28)
+// CHECK:STDOUT: !28 = !{!29}
+// CHECK:STDOUT: !29 = !DILocalVariable(arg: 1, scope: !27, type: !23)
+// CHECK:STDOUT: !30 = !DILocation(line: 8, column: 3, scope: !27)
+// CHECK:STDOUT: ; ModuleID = 'implicit_padding.carbon'
+// CHECK:STDOUT: source_filename = "implicit_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CG.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CF.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %implicit_padding.var = alloca { <{ ptr, i32 }>, ptr }, align 8, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %implicit_padding.var), !dbg !7
+// CHECK:STDOUT:   %.loc9_22.a = getelementptr inbounds nuw { <{ ptr, i32 }>, ptr }, ptr %implicit_padding.var, i32 0, i32 0, !dbg !8
+// CHECK:STDOUT:   %.loc9_24.x = getelementptr inbounds nuw <{ ptr, i32 }>, ptr %.loc9_22.a, i32 0, i32 0, !dbg !8
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc9_24.x), !dbg !9
+// CHECK:STDOUT:   %.loc10.b = getelementptr inbounds nuw { <{ ptr, i32 }>, ptr }, ptr %implicit_padding.var, i32 0, i32 1, !dbg !10
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc10.b), !dbg !11
+// CHECK:STDOUT:   call void @"_COp.a17d504d00302116:core.Destroy.Core"(ptr %implicit_padding.var), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !12
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %self) #0 !dbg !13 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !19
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.0d530a9b810c77a1:core.Destroy.Core"(ptr %self) #0 !dbg !20 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !26
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.a17d504d00302116:core.Destroy.Core"(ptr %self) #0 !dbg !27 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !30
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "implicit_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 8, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 9, column: 6, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 9, column: 3, scope: !4)
+// CHECK:STDOUT: !10 = !DILocation(line: 10, column: 6, scope: !4)
+// CHECK:STDOUT: !11 = !DILocation(line: 10, column: 3, scope: !4)
+// CHECK:STDOUT: !12 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: !13 = distinct !DISubprogram(name: "Op", linkageName: "_COp.7e389eab4a7e5487:core.Destroy.Core", scope: null, file: !3, line: 8, type: !14, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !17)
+// CHECK:STDOUT: !14 = !DISubroutineType(types: !15)
+// CHECK:STDOUT: !15 = !{null, !16}
+// CHECK:STDOUT: !16 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !17 = !{!18}
+// CHECK:STDOUT: !18 = !DILocalVariable(arg: 1, scope: !13, type: !16)
+// CHECK:STDOUT: !19 = !DILocation(line: 8, column: 3, scope: !13)
+// CHECK:STDOUT: !20 = distinct !DISubprogram(name: "Op", linkageName: "_COp.0d530a9b810c77a1:core.Destroy.Core", scope: null, file: !3, line: 8, type: !21, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !24)
+// CHECK:STDOUT: !21 = !DISubroutineType(types: !22)
+// CHECK:STDOUT: !22 = !{null, !23}
+// CHECK:STDOUT: !23 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !24 = !{!25}
+// CHECK:STDOUT: !25 = !DILocalVariable(arg: 1, scope: !20, type: !23)
+// CHECK:STDOUT: !26 = !DILocation(line: 8, column: 3, scope: !20)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "Op", linkageName: "_COp.a17d504d00302116:core.Destroy.Core", scope: null, file: !3, line: 8, type: !21, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !28)
+// CHECK:STDOUT: !28 = !{!29}
+// CHECK:STDOUT: !29 = !DILocalVariable(arg: 1, scope: !27, type: !23)
+// CHECK:STDOUT: !30 = !DILocation(line: 8, column: 3, scope: !27)
+// CHECK:STDOUT: ; ModuleID = 'insert_padding.carbon'
+// CHECK:STDOUT: source_filename = "insert_padding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CG.Main(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define void @_CF.Main() #0 !dbg !4 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %insert_padding.var = alloca <{ <{ { i32, i32, i32 }, [4 x i8] }>, ptr, i32 }>, align 8, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %insert_padding.var), !dbg !7
+// CHECK:STDOUT:   %.loc10.b = getelementptr inbounds nuw <{ <{ { i32, i32, i32 }, [4 x i8] }>, ptr, i32 }>, ptr %insert_padding.var, i32 0, i32 1, !dbg !8
+// CHECK:STDOUT:   call void @_CG.Main(ptr %.loc10.b), !dbg !9
+// CHECK:STDOUT:   call void @"_COp.ad98736f0dc6e810:core.Destroy.Core"(ptr %insert_padding.var), !dbg !7
+// CHECK:STDOUT:   ret void, !dbg !10
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %self) #0 !dbg !11 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !17
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.3124a28397dbe9e9:core.Destroy.Core"(ptr %self) #0 !dbg !18 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !24
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nounwind
+// CHECK:STDOUT: define weak_odr void @"_COp.ad98736f0dc6e810:core.Destroy.Core"(ptr %self) #0 !dbg !25 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   ret void, !dbg !28
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nounwind }
+// CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!2}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C_plus_plus, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !3 = !DIFile(filename: "insert_padding.carbon", directory: "")
+// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !3, line: 5, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
+// CHECK:STDOUT: !6 = !{null}
+// CHECK:STDOUT: !7 = !DILocation(line: 9, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 10, column: 6, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 10, column: 3, scope: !4)
+// CHECK:STDOUT: !10 = !DILocation(line: 5, column: 1, scope: !4)
+// CHECK:STDOUT: !11 = distinct !DISubprogram(name: "Op", linkageName: "_COp.7e389eab4a7e5487:core.Destroy.Core", scope: null, file: !3, line: 9, type: !12, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !15)
+// CHECK:STDOUT: !12 = !DISubroutineType(types: !13)
+// CHECK:STDOUT: !13 = !{null, !14}
+// CHECK:STDOUT: !14 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
+// CHECK:STDOUT: !15 = !{!16}
+// CHECK:STDOUT: !16 = !DILocalVariable(arg: 1, scope: !11, type: !14)
+// CHECK:STDOUT: !17 = !DILocation(line: 9, column: 3, scope: !11)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "Op", linkageName: "_COp.3124a28397dbe9e9:core.Destroy.Core", scope: null, file: !3, line: 9, type: !19, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !22)
+// CHECK:STDOUT: !19 = !DISubroutineType(types: !20)
+// CHECK:STDOUT: !20 = !{null, !21}
+// CHECK:STDOUT: !21 = !DIDerivedType(tag: DW_TAG_pointer_type, baseType: null, size: 64)
+// CHECK:STDOUT: !22 = !{!23}
+// CHECK:STDOUT: !23 = !DILocalVariable(arg: 1, scope: !18, type: !21)
+// CHECK:STDOUT: !24 = !DILocation(line: 9, column: 3, scope: !18)
+// CHECK:STDOUT: !25 = distinct !DISubprogram(name: "Op", linkageName: "_COp.ad98736f0dc6e810:core.Destroy.Core", scope: null, file: !3, line: 9, type: !19, spFlags: DISPFlagDefinition, unit: !2, retainedNodes: !26)
+// CHECK:STDOUT: !26 = !{!27}
+// CHECK:STDOUT: !27 = !DILocalVariable(arg: 1, scope: !25, type: !21)
+// CHECK:STDOUT: !28 = !DILocation(line: 9, column: 3, scope: !25)

+ 6 - 6
toolchain/lower/testdata/struct/member_access.carbon

@@ -20,20 +20,20 @@ fn Run() -> i32 {
 // CHECK:STDOUT: ; ModuleID = 'member_access.carbon'
 // CHECK:STDOUT: source_filename = "member_access.carbon"
 // CHECK:STDOUT:
-// CHECK:STDOUT: @struct.454.loc14_3 = internal constant { double, i32 } { double 0.000000e+00, i32 1 }
+// CHECK:STDOUT: @struct.454.loc14_3 = internal constant <{ double, i32 }> <{ double 0.000000e+00, i32 1 }>
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define i32 @main() #0 !dbg !4 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %x.var = alloca { double, i32 }, align 8, !dbg !8
+// CHECK:STDOUT:   %x.var = alloca <{ double, i32 }>, align 8, !dbg !8
 // CHECK:STDOUT:   %y.var = alloca i32, align 4, !dbg !9
 // CHECK:STDOUT:   %_.var = alloca i32, align 4, !dbg !10
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %x.var), !dbg !8
-// CHECK:STDOUT:   %.loc14_48.3.a = getelementptr inbounds nuw { double, i32 }, ptr %x.var, i32 0, i32 0, !dbg !11
-// CHECK:STDOUT:   %.loc14_48.6.b = getelementptr inbounds nuw { double, i32 }, ptr %x.var, i32 0, i32 1, !dbg !11
-// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 8 %x.var, ptr align 8 @struct.454.loc14_3, i64 16, i1 false), !dbg !8
+// CHECK:STDOUT:   %.loc14_48.3.a = getelementptr inbounds nuw <{ double, i32 }>, ptr %x.var, i32 0, i32 0, !dbg !11
+// CHECK:STDOUT:   %.loc14_48.6.b = getelementptr inbounds nuw <{ double, i32 }>, ptr %x.var, i32 0, i32 1, !dbg !11
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %x.var, ptr align 1 @struct.454.loc14_3, i64 12, i1 false), !dbg !8
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %y.var), !dbg !9
-// CHECK:STDOUT:   %.loc15_17.1.b = getelementptr inbounds nuw { double, i32 }, ptr %x.var, i32 0, i32 1, !dbg !12
+// CHECK:STDOUT:   %.loc15_17.1.b = getelementptr inbounds nuw <{ double, i32 }>, ptr %x.var, i32 0, i32 1, !dbg !12
 // CHECK:STDOUT:   %.loc15_17.2 = load i32, ptr %.loc15_17.1.b, align 4, !dbg !12
 // CHECK:STDOUT:   store i32 %.loc15_17.2, ptr %y.var, align 4, !dbg !9
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var), !dbg !10

+ 8 - 3
toolchain/sem_ir/file.cpp

@@ -70,15 +70,20 @@ File::File(const Parse::Tree* parse_tree, CheckIRId check_ir_id,
       expr_regions_(check_ir_id),
       clang_source_locs_(check_ir_id) {
   // `type`, `form`, and the error type are both complete & concrete types.
+  // TODO: This duplicates the code in `check/type_completion.cpp`. Consider
+  // requiring these types to be complete from Check initialization instead.
   types_.SetComplete(
       TypeType::TypeId,
-      {.value_repr = {.kind = ValueRepr::Copy, .type_id = TypeType::TypeId}});
+      {.value_repr = {.kind = ValueRepr::Copy, .type_id = TypeType::TypeId},
+       .object_layout = SemIR::ObjectLayout::Empty()});
   types_.SetComplete(
       FormType::TypeId,
-      {.value_repr = {.kind = ValueRepr::Copy, .type_id = FormType::TypeId}});
+      {.value_repr = {.kind = ValueRepr::Copy, .type_id = FormType::TypeId},
+       .object_layout = SemIR::ObjectLayout::Empty()});
   types_.SetComplete(
       ErrorInst::TypeId,
-      {.value_repr = {.kind = ValueRepr::Copy, .type_id = ErrorInst::TypeId}});
+      {.value_repr = {.kind = ValueRepr::Copy, .type_id = ErrorInst::TypeId},
+       .object_layout = SemIR::ObjectLayout::Empty()});
 
   insts_.Reserve(SingletonInstKinds.size());
   for (auto kind : SingletonInstKinds) {

+ 13 - 6
toolchain/sem_ir/file.h

@@ -66,7 +66,7 @@ struct ExprRegion {
 using ExprRegionStore = ValueStore<ExprRegionId, ExprRegion, Tag<CheckIRId>>;
 
 using CustomLayoutStore =
-    BlockValueStore<CustomLayoutId, uint64_t, Tag<CheckIRId>>;
+    BlockValueStore<CustomLayoutId, ObjectSize, Tag<CheckIRId>>;
 
 // The semantic IR for a single file.
 class File : public Printable<File> {
@@ -95,16 +95,23 @@ class File : public Printable<File> {
   auto CollectMemUsage(MemUsage& mem_usage, llvm::StringRef label) const
       -> void;
 
-  // Returns array bound value from the bound instruction.
+  // Returns the zero-extended integer value of the given instruction if it is a
+  // constant integer.
   // TODO: Move this function elsewhere.
-  auto GetArrayBoundValue(InstId bound_id) const -> std::optional<uint64_t> {
-    if (auto bound = insts().TryGetAs<IntValue>(
-            constant_values().GetConstantInstId(bound_id))) {
-      return ints().Get(bound->int_id).getZExtValue();
+  auto GetZExtIntValue(InstId inst_id) const -> std::optional<uint64_t> {
+    if (auto val = insts().TryGetAs<IntValue>(
+            constant_values().GetConstantInstId(inst_id))) {
+      return ints().Get(val->int_id).getZExtValue();
     }
     return std::nullopt;
   }
 
+  // Returns the layout of a pointer.
+  // TODO: This should depend on the target machine.
+  auto GetPointerLayout() const -> ObjectLayout {
+    return {.size = ObjectSize::Bytes(8), .alignment = ObjectSize::Bytes(8)};
+  }
+
   // Gets the pointee type of the given type, which must be a pointer type.
   // TODO: Move this function elsewhere.
   auto GetPointeeType(TypeId pointer_id) const -> TypeId {

+ 3 - 1
toolchain/sem_ir/inst_fingerprinter.cpp

@@ -191,7 +191,9 @@ struct Worklist {
     }
     auto block = sem_ir->custom_layouts().Get(custom_layout_id);
     contents.push_back(block.size());
-    contents.insert(contents.end(), block.begin(), block.end());
+    for (auto size : block) {
+      contents.push_back(size.bits());
+    }
   }
 
   auto Add(NameScopeId name_scope_id) -> void {

+ 9 - 0
toolchain/sem_ir/type.h

@@ -249,6 +249,15 @@ class TypeStore : public Yaml::Printable<TypeStore> {
         map.Add(PrintToString(type_id),
                 Yaml::OutputMapping([&](Yaml::OutputMapping::Map map2) {
                   map2.Add("value_repr", Yaml::OutputScalar(info.value_repr));
+                  map2.Add(
+                      "object_layout",
+                      Yaml::OutputMapping([&](Yaml::OutputMapping::Map map3) {
+                        map3.Add("size",
+                                 Yaml::OutputScalar(info.object_layout.size));
+                        map3.Add(
+                            "alignment",
+                            Yaml::OutputScalar(info.object_layout.alignment));
+                      }));
                   if (info.abstract_class_id.has_value()) {
                     map2.Add("abstract_class_id",
                              Yaml::OutputScalar(info.abstract_class_id));

+ 12 - 1
toolchain/sem_ir/type_info.cpp

@@ -37,7 +37,18 @@ auto ValueRepr::Print(llvm::raw_ostream& out) const -> void {
 }
 
 auto CompleteTypeInfo::Print(llvm::raw_ostream& out) const -> void {
-  out << "{value_rep: " << value_repr << "}";
+  out << "{value_rep: " << value_repr
+      << ", layout: {size: " << object_layout.size
+      << ", align: " << object_layout.alignment << "}}";
+}
+
+auto ObjectSize::Print(llvm::raw_ostream& out) const -> void {
+  int64_t bytes = bits_ / 8;
+  int64_t bits = bits_ % 8;
+  out << bytes;
+  if (bits != 0) {
+    out << ":" << bits;
+  }
 }
 
 auto ValueRepr::ForType(const File& file, TypeId type_id) -> ValueRepr {

+ 144 - 0
toolchain/sem_ir/type_info.h

@@ -73,6 +73,146 @@ struct ValueRepr : public Printable<ValueRepr> {
   TypeId type_id = TypeId::None;
 };
 
+// A size within an object representation. This stores a bit count, but provides
+// convenience methods to access the size in bytes instead.
+class ObjectSize : public Printable<ObjectSize> {
+ public:
+  static constexpr auto Zero() -> ObjectSize { return ObjectSize(0); }
+
+  static constexpr auto Bits(int64_t bits) -> ObjectSize {
+    return ObjectSize(bits);
+  }
+
+  static constexpr auto Bytes(int64_t bytes) -> ObjectSize {
+    return ObjectSize(bytes * 8);
+  }
+
+  // Returns the size in bits.
+  auto bits() const -> int64_t { return bits_; }
+
+  // Returns the minimum number of bytes that would contain this size. This is
+  // the size in bytes, rounded up.
+  auto bytes() const -> int64_t { return (bits_ + 7) / 8; }
+
+  // Return this size rounded up to a multiple of `align`, which must be a power
+  // of 2.
+  [[nodiscard]] auto AlignedTo(ObjectSize align) const -> ObjectSize {
+    CARBON_CHECK(llvm::isPowerOf2_64(align.bits_),
+                 "Non power-of-2 alignment {0}", align.bits_);
+    return Bits((bits_ + align.bits_ - 1) & -align.bits_);
+  }
+
+  auto Print(llvm::raw_ostream& out) const -> void;
+
+  friend constexpr auto operator<=>(ObjectSize a, ObjectSize b) {
+    return a.bits_ <=> b.bits_;
+  }
+
+  friend constexpr auto operator==(ObjectSize a, ObjectSize b) -> bool {
+    return a.bits_ == b.bits_;
+  }
+
+  // Computes the sum of two object sizes. This is the size of the smallest
+  // object that could fit both objects consecutively, without any alignment
+  // constraints or other padding bits.
+  auto operator+=(ObjectSize other) -> ObjectSize& {
+    bits_ += other.bits_;
+    return *this;
+  }
+  friend auto operator+(ObjectSize a, ObjectSize b) -> ObjectSize {
+    return a += b;
+  }
+
+  // Scales a size by a dimensionless integer. This is the size of `n` objects
+  // of the given size, laid out contiguously with no padding between them.
+  // Equivalent to adding the size to itself `n` times.
+  auto operator*=(int64_t n) -> ObjectSize& {
+    CARBON_CHECK(n >= 0, "sizes should not be negative");
+    bits_ *= n;
+    return *this;
+  }
+  friend auto operator*(ObjectSize a, int64_t n) -> ObjectSize {
+    return a *= n;
+  }
+  friend auto operator*(int64_t n, ObjectSize a) -> ObjectSize {
+    return a *= n;
+  }
+
+ private:
+  explicit constexpr ObjectSize(int64_t bits) : bits_(bits) {
+    CARBON_CHECK(bits >= 0, "sizes should not be negative");
+  }
+
+  int64_t bits_;
+};
+
+// The layout of an object representation.
+struct ObjectLayout {
+  // The size of the object representation. This may be zero for empty types.
+  // This is not guaranteed to be a multiple of `alignment`, so explicit tail
+  // padding may be required if objects using this layout are stored in an
+  // array.
+  ObjectSize size = ObjectSize::Zero();
+
+  // The alignment of the object representation. This will be a power of 2 for a
+  // valid representation, or zero if the representation is unset (such as for
+  // the object layout of an incomplete type). This will typically be 1 byte for
+  // an empty type.
+  ObjectSize alignment = ObjectSize::Zero();
+
+  // Returns the object layout to use for an empty type.
+  [[nodiscard]] static auto Empty() -> ObjectLayout {
+    return {.size = ObjectSize::Zero(), .alignment = ObjectSize::Bytes(1)};
+  }
+
+  // Returns the object layout to use for an array of `count` elements. The
+  // result never has tail padding, so this is *not* the same as adding the
+  // element to itself `count` times in general.
+  static auto ForArray(ObjectLayout element_layout, int64_t count)
+      -> ObjectLayout {
+    return {.size = element_layout.ArrayStride() * count,
+            .alignment = element_layout.alignment};
+  }
+
+  // Returns the stride of an array with this element type.
+  auto ArrayStride() const -> ObjectSize { return size.AlignedTo(alignment); }
+
+  // Returns the offset that a field with the given layout would have if
+  // appended to this struct.
+  auto FieldOffset(ObjectLayout field) -> ObjectSize {
+    CARBON_CHECK(field.has_value());
+    return size.AlignedTo(field.alignment);
+  }
+
+  // Updates this aggregate layout by concatenating naturally-aligned space for
+  // the given field layout, which is required to be valid.
+  auto AppendField(ObjectLayout field) -> void {
+    size = FieldOffset(field) + field.size;
+    alignment = std::max(alignment, field.alignment);
+  }
+
+  // Updates this aggregate layout by concatenating naturally-aligned space for
+  // the given field layout, if the field has a valid layout. If any field with
+  // an invalid layout is appended, the layout of the aggregate becomes invalid.
+  //
+  // An invalid layout for a field with a complete type indicates that its
+  // layout is dependent on a generic parameter; this causes the layout of the
+  // enclosing aggregate to also be dependent on that parameter.
+  auto TryAppendField(ObjectLayout field) -> void {
+    if (has_value()) {
+      if (field.has_value()) {
+        AppendField(field);
+      } else {
+        *this = ObjectLayout();
+      }
+    }
+  }
+
+  // Returns true if the layout has been set. For a complete type, this will be
+  // true if the layout is non-dependent.
+  auto has_value() const -> bool { return alignment != ObjectSize::Zero(); }
+};
+
 // Information stored about a TypeId corresponding to a complete type.
 struct CompleteTypeInfo : public Printable<CompleteTypeInfo> {
   auto Print(llvm::raw_ostream& out) const -> void;
@@ -81,6 +221,10 @@ struct CompleteTypeInfo : public Printable<CompleteTypeInfo> {
   // not complete.
   ValueRepr value_repr = ValueRepr();
 
+  // The layout of the object representation of this type. This will be valid
+  // unless the type is incomplete or dependent.
+  ObjectLayout object_layout = ObjectLayout();
+
   // If this type is abstract, this is id of an abstract class it uses.
   ClassId abstract_class_id = ClassId::None;