Parcourir la source

Unify initialization and conversion logic (#3255)

Combine the initialization, implicit conversion, and value category
conversion functions into a single function.

This substantially reduces the duplication between these steps, and
ensures that we support the same set of conversions in all these
contexts. This also fixes some issues where we would not use the proper
value representation for tuples and structs after performing implicit
conversions.
Richard Smith il y a 2 ans
Parent
commit
fea6cf88fc
74 fichiers modifiés avec 1646 ajouts et 1074 suppressions
  1. 3 0
      toolchain/check/BUILD
  2. 1 638
      toolchain/check/context.cpp
  3. 0 83
      toolchain/check/context.h
  4. 838 0
      toolchain/check/convert.cpp
  5. 93 0
      toolchain/check/convert.h
  6. 2 1
      toolchain/check/handle_array.cpp
  7. 4 3
      toolchain/check/handle_call_expression.cpp
  8. 2 1
      toolchain/check/handle_function.cpp
  9. 4 3
      toolchain/check/handle_if_expression.cpp
  10. 2 1
      toolchain/check/handle_if_statement.cpp
  11. 4 3
      toolchain/check/handle_index.cpp
  12. 2 1
      toolchain/check/handle_name.cpp
  13. 12 10
      toolchain/check/handle_operator.cpp
  14. 2 1
      toolchain/check/handle_pattern_binding.cpp
  15. 18 4
      toolchain/check/handle_statement.cpp
  16. 2 1
      toolchain/check/handle_struct.cpp
  17. 2 1
      toolchain/check/handle_variable.cpp
  18. 94 0
      toolchain/check/pending_block.h
  19. 3 4
      toolchain/check/testdata/array/array_in_place.carbon
  20. 2 3
      toolchain/check/testdata/array/assign_return_value.carbon
  21. 13 14
      toolchain/check/testdata/array/assign_var.carbon
  22. 18 21
      toolchain/check/testdata/array/base.carbon
  23. 7 9
      toolchain/check/testdata/array/fail_type_mismatch.carbon
  24. 10 11
      toolchain/check/testdata/array/nine_elements.carbon
  25. 14 16
      toolchain/check/testdata/basics/numeric_literals.carbon
  26. 7 7
      toolchain/check/testdata/basics/raw_and_textual_ir.carbon
  27. 7 7
      toolchain/check/testdata/basics/textual_ir.carbon
  28. 57 0
      toolchain/check/testdata/if_expression/struct.carbon
  29. 3 4
      toolchain/check/testdata/index/array_element_access.carbon
  30. 2 3
      toolchain/check/testdata/index/fail_array_large_index.carbon
  31. 2 3
      toolchain/check/testdata/index/fail_array_non_int_indexing.carbon
  32. 2 3
      toolchain/check/testdata/index/fail_array_out_of_bound_access.carbon
  33. 7 7
      toolchain/check/testdata/index/fail_non_deterministic_type.carbon
  34. 7 5
      toolchain/check/testdata/index/fail_tuple_large_index.carbon
  35. 7 7
      toolchain/check/testdata/index/fail_tuple_non_int_indexing.carbon
  36. 7 7
      toolchain/check/testdata/index/fail_tuple_out_of_bound_access.carbon
  37. 7 5
      toolchain/check/testdata/index/tuple_element_access.carbon
  38. 14 14
      toolchain/check/testdata/operators/assignment.carbon
  39. 21 21
      toolchain/check/testdata/operators/fail_assigment_to_non_assignable.carbon
  40. 14 14
      toolchain/check/testdata/pointer/address_of_lvalue.carbon
  41. 7 7
      toolchain/check/testdata/return/tuple.carbon
  42. 4 4
      toolchain/check/testdata/struct/empty.carbon
  43. 2 2
      toolchain/check/testdata/struct/fail_assign_empty.carbon
  44. 2 2
      toolchain/check/testdata/struct/fail_assign_nested.carbon
  45. 2 2
      toolchain/check/testdata/struct/fail_assign_to_empty.carbon
  46. 2 2
      toolchain/check/testdata/struct/fail_field_name_mismatch.carbon
  47. 2 2
      toolchain/check/testdata/struct/fail_field_type_mismatch.carbon
  48. 3 3
      toolchain/check/testdata/struct/fail_member_access_type.carbon
  49. 3 3
      toolchain/check/testdata/struct/fail_non_member_access.carbon
  50. 2 2
      toolchain/check/testdata/struct/fail_too_few_values.carbon
  51. 40 0
      toolchain/check/testdata/struct/literal_member_access.carbon
  52. 7 7
      toolchain/check/testdata/struct/member_access.carbon
  53. 7 7
      toolchain/check/testdata/struct/nested_struct_in_place.carbon
  54. 7 5
      toolchain/check/testdata/struct/one_entry.carbon
  55. 21 11
      toolchain/check/testdata/struct/tuple_as_element.carbon
  56. 17 9
      toolchain/check/testdata/struct/two_entries.carbon
  57. 4 4
      toolchain/check/testdata/tuples/empty.carbon
  58. 2 2
      toolchain/check/testdata/tuples/fail_assign_empty.carbon
  59. 4 4
      toolchain/check/testdata/tuples/fail_assign_nested.carbon
  60. 4 2
      toolchain/check/testdata/tuples/fail_element_type_mismatch.carbon
  61. 2 2
      toolchain/check/testdata/tuples/fail_too_few_element.carbon
  62. 2 2
      toolchain/check/testdata/tuples/fail_type_assign.carbon
  63. 14 14
      toolchain/check/testdata/tuples/nested_tuple.carbon
  64. 16 16
      toolchain/check/testdata/tuples/nested_tuple_in_place.carbon
  65. 7 5
      toolchain/check/testdata/tuples/one_element.carbon
  66. 17 9
      toolchain/check/testdata/tuples/two_elements.carbon
  67. 3 0
      toolchain/diagnostics/diagnostic_kind.def
  68. 4 2
      toolchain/lower/testdata/struct/one_entry.carbon
  69. 8 0
      toolchain/lower/testdata/struct/two_entries.carbon
  70. 4 2
      toolchain/lower/testdata/tuple/one_entry.carbon
  71. 8 0
      toolchain/lower/testdata/tuple/two_entries.carbon
  72. 56 0
      toolchain/lower/testdata/tuple/value_formation.carbon
  73. 26 0
      toolchain/lower/testdata/tuple/value_forwarding.carbon
  74. 21 6
      toolchain/sem_ir/file.h

+ 3 - 0
toolchain/check/BUILD

@@ -33,6 +33,7 @@ cc_library(
     srcs = [
         "check.cpp",
         "context.cpp",
+        "convert.cpp",
         "declaration_name_stack.cpp",
         "node_block_stack.cpp",
     ] +
@@ -43,8 +44,10 @@ cc_library(
     hdrs = [
         "check.h",
         "context.h",
+        "convert.h",
         "declaration_name_stack.h",
         "node_block_stack.h",
+        "pending_block.h",
     ],
     deps = [
         ":node_stack",

+ 1 - 638
toolchain/check/context.cpp

@@ -4,11 +4,11 @@
 
 #include "toolchain/check/context.h"
 
+#include <string>
 #include <utility>
 
 #include "common/check.h"
 #include "common/vlog.h"
-#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/Sequence.h"
 #include "toolchain/check/declaration_name_stack.h"
 #include "toolchain/check/node_block_stack.h"
@@ -251,643 +251,6 @@ auto Context::is_current_position_reachable() -> bool {
          SemIR::TerminatorKind::Terminator;
 }
 
-namespace {
-// A handle to a new block that may be modified, with copy-on-write semantics.
-//
-// The constructor is given the ID of an existing block that provides the
-// initial contents of the new block. The new block is lazily allocated; if no
-// modifications have been made, the `id()` function will return the original
-// block ID.
-//
-// This is intended to avoid an unnecessary block allocation in the case where
-// the new block ends up being exactly the same as the original block.
-class CopyOnWriteBlock {
- public:
-  CopyOnWriteBlock(SemIR::File& file, SemIR::NodeBlockId source_id)
-      : file_(file), source_id_(source_id) {}
-
-  auto id() -> SemIR::NodeBlockId const { return id_; }
-
-  auto Set(int i, SemIR::NodeId value) -> void {
-    if (file_.GetNodeBlock(id_)[i] == value) {
-      return;
-    }
-    if (id_ == source_id_) {
-      id_ = file_.AddNodeBlock(file_.GetNodeBlock(source_id_));
-    }
-    file_.GetNodeBlock(id_)[i] = value;
-  }
-
- private:
-  SemIR::File& file_;
-  SemIR::NodeBlockId source_id_;
-  SemIR::NodeBlockId id_ = source_id_;
-};
-}  // namespace
-
-// A block of code that contains pending instructions that might be needed but
-// that haven't been inserted yet.
-class Context::PendingBlock {
- public:
-  PendingBlock(Context& context) : context_(context) {}
-
-  PendingBlock(const PendingBlock&) = delete;
-  PendingBlock& operator=(const PendingBlock&) = delete;
-
-  // A scope in which we will tentatively add nodes to a pending block. If we
-  // leave the scope without inserting or merging the block, nodes added after
-  // this point will be removed again.
-  class DiscardUnusedNodesScope {
-   public:
-    DiscardUnusedNodesScope(PendingBlock& block)
-        : block_(block), size_(block.nodes_.size()) {}
-    ~DiscardUnusedNodesScope() {
-      if (block_.nodes_.size() > size_) {
-        block_.nodes_.truncate(size_);
-      }
-    }
-
-   private:
-    PendingBlock& block_;
-    size_t size_;
-  };
-
-  auto AddNode(SemIR::Node node) -> SemIR::NodeId {
-    auto node_id = context_.semantics_ir().AddNodeInNoBlock(node);
-    nodes_.push_back(node_id);
-    return node_id;
-  }
-
-  // Insert the pending block of code at the current position.
-  auto InsertHere() -> void {
-    for (auto id : nodes_) {
-      context_.node_block_stack().AddNodeId(id);
-    }
-    nodes_.clear();
-  }
-
-  // Replace the node at target_id with the nodes in this block. The new value
-  // for target_id should be value_id.
-  auto MergeReplacing(SemIR::NodeId target_id, SemIR::NodeId value_id) -> void {
-    auto value = context_.semantics_ir().GetNode(value_id);
-
-    // There are three cases here:
-
-    if (nodes_.empty()) {
-      // 1) The block is empty. Replace `target_id` with an empty splice
-      // pointing at `value_id`.
-      context_.semantics_ir().ReplaceNode(
-          target_id,
-          SemIR::Node::SpliceBlock::Make(value.parse_node(), value.type_id(),
-                                         SemIR::NodeBlockId::Empty, value_id));
-    } else if (nodes_.size() == 1 && nodes_[0] == value_id) {
-      // 2) The block is {value_id}. Replace `target_id` with the node referred
-      // to by `value_id`. This is intended to be the common case.
-      context_.semantics_ir().ReplaceNode(target_id, value);
-    } else {
-      // 3) Anything else: splice it into the IR, replacing `target_id`.
-      context_.semantics_ir().ReplaceNode(
-          target_id,
-          SemIR::Node::SpliceBlock::Make(
-              value.parse_node(), value.type_id(),
-              context_.semantics_ir().AddNodeBlock(nodes_), value_id));
-    }
-
-    // Prepare to stash more pending instructions.
-    nodes_.clear();
-  }
-
- private:
-  Context& context_;
-  llvm::SmallVector<SemIR::NodeId> nodes_;
-};
-
-auto Context::Initialize(Parse::Node parse_node, SemIR::NodeId target_id,
-                         SemIR::NodeId value_id) -> SemIR::NodeId {
-  PendingBlock target_block(*this);
-  return InitializeImpl(parse_node, target_id, target_block, value_id);
-}
-
-auto Context::InitializeImpl(Parse::Node parse_node, SemIR::NodeId target_id,
-                             PendingBlock& target_block, SemIR::NodeId value_id)
-    -> SemIR::NodeId {
-  // Implicitly convert the value to the type of the target.
-  auto type_id = semantics_ir().GetNode(target_id).type_id();
-  auto expr_id = ImplicitAs(parse_node, value_id, type_id);
-  SemIR::Node expr = semantics_ir().GetNode(expr_id);
-
-  // Perform initialization now that we have an expression of the right type.
-  switch (SemIR::GetExpressionCategory(semantics_ir(), expr_id)) {
-    case SemIR::ExpressionCategory::NotExpression:
-      CARBON_FATAL() << "Converting non-expression node " << expr
-                     << " to initializing expression";
-
-    case SemIR::ExpressionCategory::DurableReference:
-    case SemIR::ExpressionCategory::EphemeralReference:
-      // The design uses a custom "copy initialization" process here. We model
-      // that as value binding followed by direct initialization.
-      //
-      // TODO: Determine whether this is observably different from the design,
-      // and change either the toolchain or the design so they match.
-      return AddNode(SemIR::Node::BindValue::Make(expr.parse_node(),
-                                                  expr.type_id(), expr_id));
-
-    case SemIR::ExpressionCategory::Value:
-      // TODO: For class types, use an interface to determine how to perform
-      // this operation.
-      return expr_id;
-
-    case SemIR::ExpressionCategory::Initializing:
-      MarkInitializerFor(expr_id, target_id, target_block);
-      return expr_id;
-
-    case SemIR::ExpressionCategory::Mixed:
-      expr = semantics_ir().GetNode(expr_id);
-
-      // TODO: Make non-recursive.
-      // TODO: This should be done as part of the `ImplicitAs` processing so
-      // that we can still initialize directly from one tuple element if
-      // another one needs to be converted.
-      switch (expr.kind()) {
-        case SemIR::NodeKind::TupleLiteral:
-        case SemIR::NodeKind::StructLiteral: {
-          bool is_tuple = expr.kind() == SemIR::NodeKind::TupleLiteral;
-          auto elements_id =
-              is_tuple ? expr.GetAsTupleLiteral() : expr.GetAsStructLiteral();
-          auto elements = semantics_ir().GetNodeBlock(elements_id);
-          CopyOnWriteBlock new_block(semantics_ir(), elements_id);
-          bool is_in_place =
-              SemIR::GetInitializingRepresentation(semantics_ir(), type_id)
-                  .kind == SemIR::InitializingRepresentation::InPlace;
-          for (auto [i, elem_id] : llvm::enumerate(elements)) {
-            // TODO: We know the type already matches because we already invoked
-            // `ImplicitAsRequired`, but this will need to change once we stop
-            // doing that.
-            auto inner_target_type = semantics_ir().GetNode(elem_id).type_id();
-            PendingBlock::DiscardUnusedNodesScope scope(target_block);
-            auto inner_target_id = target_block.AddNode(
-                is_tuple ? SemIR::Node::TupleAccess::Make(
-                               parse_node, inner_target_type, target_id,
-                               SemIR::MemberIndex(i))
-                         : SemIR::Node::StructAccess::Make(
-                               parse_node, inner_target_type, target_id,
-                               SemIR::MemberIndex(i)));
-            auto new_id =
-                is_in_place ? InitializeAndFinalize(parse_node, inner_target_id,
-                                                    target_block, elem_id)
-                            : InitializeImpl(parse_node, inner_target_id,
-                                             target_block, elem_id);
-            new_block.Set(i, new_id);
-          }
-          return AddNode(
-              is_tuple ? SemIR::Node::TupleInit::Make(parse_node, type_id,
-                                                      expr_id, new_block.id())
-                       : SemIR::Node::StructInit::Make(
-                             parse_node, type_id, expr_id, new_block.id()));
-        }
-
-        default:
-          CARBON_FATAL() << "Unexpected kind for mixed-category expression "
-                         << expr.kind();
-      }
-  }
-}
-
-auto Context::InitializeAndFinalize(Parse::Node parse_node,
-                                    SemIR::NodeId target_id,
-                                    PendingBlock& target_block,
-                                    SemIR::NodeId value_id) -> SemIR::NodeId {
-  auto init_id = InitializeImpl(parse_node, target_id, target_block, value_id);
-  if (init_id == SemIR::NodeId::BuiltinError) {
-    return init_id;
-  }
-  auto target_type_id = semantics_ir().GetNode(target_id).type_id();
-  if (auto init_rep =
-          SemIR::GetInitializingRepresentation(semantics_ir(), target_type_id);
-      init_rep.kind == SemIR::InitializingRepresentation::ByCopy) {
-    target_block.InsertHere();
-    init_id = AddNode(SemIR::Node::InitializeFrom::Make(
-        parse_node, target_type_id, init_id, target_id));
-  }
-  return init_id;
-}
-
-auto Context::ConvertToValueExpression(SemIR::NodeId expr_id) -> SemIR::NodeId {
-  if (expr_id == SemIR::NodeId::BuiltinError) {
-    return expr_id;
-  }
-
-  switch (SemIR::GetExpressionCategory(semantics_ir(), expr_id)) {
-    case SemIR::ExpressionCategory::NotExpression: {
-      // TODO: We currently encounter this for use of namespaces and functions.
-      // We should provide a better diagnostic for inappropriate use of
-      // namespace names, and allow use of functions as values.
-      CARBON_DIAGNOSTIC(UseOfNonExpressionAsValue, Error,
-                        "Expression cannot be used as a value.");
-      emitter().Emit(semantics_ir().GetNode(expr_id).parse_node(),
-                     UseOfNonExpressionAsValue);
-      return SemIR::NodeId::BuiltinError;
-    }
-
-    case SemIR::ExpressionCategory::Initializing:
-      // Commit to using a temporary for this initializing expression.
-      // TODO: Don't create a temporary if the initializing representation is
-      // already a value representation.
-      expr_id = FinalizeTemporary(expr_id, /*discarded=*/false);
-      [[fallthrough]];
-
-    case SemIR::ExpressionCategory::DurableReference:
-    case SemIR::ExpressionCategory::EphemeralReference: {
-      // TODO: Support types with custom value representations.
-      SemIR::Node expr = semantics_ir().GetNode(expr_id);
-      return AddNode(SemIR::Node::BindValue::Make(expr.parse_node(),
-                                                  expr.type_id(), expr_id));
-    }
-
-    case SemIR::ExpressionCategory::Value:
-      return expr_id;
-
-    case SemIR::ExpressionCategory::Mixed: {
-      SemIR::Node expr = semantics_ir().GetNode(expr_id);
-
-      // TODO: Make non-recursive.
-      switch (expr.kind()) {
-        case SemIR::NodeKind::TupleLiteral:
-        case SemIR::NodeKind::StructLiteral: {
-          bool is_tuple = expr.kind() == SemIR::NodeKind::TupleLiteral;
-          auto elements_id =
-              is_tuple ? expr.GetAsTupleLiteral() : expr.GetAsStructLiteral();
-          auto elements = semantics_ir().GetNodeBlock(elements_id);
-          CopyOnWriteBlock new_block(semantics_ir(), elements_id);
-          for (auto [i, elem_id] : llvm::enumerate(elements)) {
-            new_block.Set(i, ConvertToValueExpression(elem_id));
-          }
-          return AddNode(is_tuple ? SemIR::Node::TupleValue::Make(
-                                        expr.parse_node(), expr.type_id(),
-                                        expr_id, new_block.id())
-                                  : SemIR::Node::StructValue::Make(
-                                        expr.parse_node(), expr.type_id(),
-                                        expr_id, new_block.id()));
-        }
-
-        default:
-          CARBON_FATAL() << "Unexpected kind for mixed-category expression "
-                         << expr.kind();
-      }
-    }
-  }
-}
-
-// Convert the given expression to a value or reference expression of the same
-// type.
-auto Context::ConvertToValueOrReferenceExpression(SemIR::NodeId expr_id,
-                                                  bool discarded)
-    -> SemIR::NodeId {
-  switch (GetExpressionCategory(semantics_ir(), expr_id)) {
-    case SemIR::ExpressionCategory::Value:
-    case SemIR::ExpressionCategory::DurableReference:
-    case SemIR::ExpressionCategory::EphemeralReference:
-      return expr_id;
-
-    case SemIR::ExpressionCategory::Initializing:
-      return FinalizeTemporary(expr_id, discarded);
-
-    case SemIR::ExpressionCategory::Mixed:
-    case SemIR::ExpressionCategory::NotExpression:
-      return ConvertToValueExpression(expr_id);
-  }
-}
-
-// Given an initializing expression, find its return slot. Returns `Invalid` if
-// there is no return slot, because the initialization is not performed in
-// place.
-static auto FindReturnSlotForInitializer(SemIR::File& semantics_ir,
-                                         SemIR::NodeId init_id)
-    -> SemIR::NodeId {
-  SemIR::Node init = semantics_ir.GetNode(init_id);
-  switch (init.kind()) {
-    default:
-      CARBON_FATAL() << "Initialization from unexpected node " << init;
-
-    case SemIR::NodeKind::StructInit:
-    case SemIR::NodeKind::TupleInit:
-      // TODO: Track a return slot for these initializers.
-      CARBON_FATAL() << init
-                     << " should be created with its return slot already "
-                        "filled in properly";
-
-    case SemIR::NodeKind::InitializeFrom: {
-      auto [src_id, dest_id] = init.GetAsInitializeFrom();
-      return dest_id;
-    }
-
-    case SemIR::NodeKind::Call: {
-      auto [refs_id, callee_id] = init.GetAsCall();
-      if (!semantics_ir.GetFunction(callee_id).return_slot_id.is_valid()) {
-        return SemIR::NodeId::Invalid;
-      }
-      return semantics_ir.GetNodeBlock(refs_id).back();
-    }
-
-    case SemIR::NodeKind::ArrayInit: {
-      auto [src_id, refs_id] = init.GetAsArrayInit();
-      return semantics_ir.GetNodeBlock(refs_id).back();
-    }
-  }
-}
-
-auto Context::MarkInitializerFor(SemIR::NodeId init_id, SemIR::NodeId target_id,
-                                 PendingBlock& target_block) -> void {
-  auto return_slot_id = FindReturnSlotForInitializer(semantics_ir(), init_id);
-  if (return_slot_id.is_valid()) {
-    // Replace the temporary in the return slot with a reference to our target.
-    CARBON_CHECK(semantics_ir().GetNode(return_slot_id).kind() ==
-                 SemIR::NodeKind::TemporaryStorage)
-        << "Return slot for initializer does not contain a temporary; "
-        << "initialized multiple times? Have "
-        << semantics_ir().GetNode(return_slot_id);
-    target_block.MergeReplacing(return_slot_id, target_id);
-  }
-}
-
-auto Context::FinalizeTemporary(SemIR::NodeId init_id, bool discarded)
-    -> SemIR::NodeId {
-  auto return_slot_id = FindReturnSlotForInitializer(semantics_ir(), init_id);
-  if (return_slot_id.is_valid()) {
-    // The return slot should already have a materialized temporary in it.
-    CARBON_CHECK(semantics_ir().GetNode(return_slot_id).kind() ==
-                 SemIR::NodeKind::TemporaryStorage)
-        << "Return slot for initializer does not contain a temporary; "
-        << "initialized multiple times? Have "
-        << semantics_ir().GetNode(return_slot_id);
-    auto init = semantics_ir().GetNode(init_id);
-    return AddNode(SemIR::Node::Temporary::Make(
-        init.parse_node(), init.type_id(), return_slot_id, init_id));
-  }
-
-  if (discarded) {
-    // Don't invent a temporary that we're going to discard.
-    return SemIR::NodeId::Invalid;
-  }
-
-  // The initializer has no return slot, but we want to produce a temporary
-  // object. Materialize one now.
-  // TODO: Consider using an invalid ID to mean that we immediately
-  // materialize and initialize a temporary, rather than two separate
-  // nodes.
-  auto init = semantics_ir().GetNode(init_id);
-  auto temporary_id = AddNode(
-      SemIR::Node::TemporaryStorage::Make(init.parse_node(), init.type_id()));
-  return AddNode(SemIR::Node::Temporary::Make(init.parse_node(), init.type_id(),
-                                              temporary_id, init_id));
-}
-
-auto Context::HandleDiscardedExpression(SemIR::NodeId expr_id) -> void {
-  // If we discard an initializing expression, convert it to a value or
-  // reference so that it has something to initialize.
-  ConvertToValueOrReferenceExpression(expr_id, /*discarded=*/true);
-
-  // TODO: This will eventually need to do some "do not discard" analysis.
-}
-
-auto Context::ImplicitAsForArgs(Parse::Node call_parse_node,
-                                SemIR::NodeBlockId arg_refs_id,
-                                Parse::Node param_parse_node,
-                                SemIR::NodeBlockId param_refs_id,
-                                bool has_return_slot) -> bool {
-  // If both arguments and parameters are empty, return quickly. Otherwise,
-  // we'll fetch both so that errors are consistent.
-  if (arg_refs_id == SemIR::NodeBlockId::Empty &&
-      param_refs_id == SemIR::NodeBlockId::Empty) {
-    return true;
-  }
-
-  auto arg_refs = semantics_ir_->GetNodeBlock(arg_refs_id);
-  auto param_refs = semantics_ir_->GetNodeBlock(param_refs_id);
-
-  if (has_return_slot) {
-    // There's no entry in the parameter block for the return slot, so ignore
-    // the corresponding entry in the argument block.
-    // TODO: Consider adding the return slot to the parameter list.
-    CARBON_CHECK(!arg_refs.empty()) << "missing return slot";
-    arg_refs = arg_refs.drop_back();
-  }
-
-  // If sizes mismatch, fail early.
-  if (arg_refs.size() != param_refs.size()) {
-    CARBON_DIAGNOSTIC(CallArgCountMismatch, Error,
-                      "{0} argument(s) passed to function expecting "
-                      "{1} argument(s).",
-                      int, int);
-    CARBON_DIAGNOSTIC(InCallToFunction, Note,
-                      "Calling function declared here.");
-    emitter_
-        ->Build(call_parse_node, CallArgCountMismatch, arg_refs.size(),
-                param_refs.size())
-        .Note(param_parse_node, InCallToFunction)
-        .Emit();
-    return false;
-  }
-
-  if (param_refs.empty()) {
-    return true;
-  }
-
-  int diag_param_index;
-  DiagnosticAnnotationScope annotate_diagnostics(emitter_, [&](auto& builder) {
-    CARBON_DIAGNOSTIC(InCallToFunctionParam, Note,
-                      "Initializing parameter {0} of function declared here.",
-                      int);
-    builder.Note(param_parse_node, InCallToFunctionParam, diag_param_index + 1);
-  });
-
-  // Check type conversions per-element.
-  for (auto [i, value_id, param_ref] : llvm::enumerate(arg_refs, param_refs)) {
-    diag_param_index = i;
-
-    auto as_type_id = semantics_ir_->GetNode(param_ref).type_id();
-    // TODO: Convert to the proper expression category. For now, we assume
-    // parameters are all `let` bindings.
-    value_id = ConvertToValueOfType(call_parse_node, value_id, as_type_id);
-    if (value_id == SemIR::NodeId::BuiltinError) {
-      return false;
-    }
-    arg_refs[i] = value_id;
-  }
-
-  return true;
-}
-
-// Performs a conversion from a tuple to an array type.
-static auto ConvertTupleToArray(Context& context, SemIR::Node tuple_type,
-                                SemIR::Node array_type,
-                                SemIR::TypeId array_type_id,
-                                SemIR::NodeId value_id) -> SemIR::NodeId {
-  auto [array_bound_id, element_type_id] = array_type.GetAsArrayType();
-  auto tuple_elem_types_id = tuple_type.GetAsTupleType();
-  const auto& tuple_elem_types =
-      context.semantics_ir().GetTypeBlock(tuple_elem_types_id);
-
-  auto value = context.semantics_ir().GetNode(value_id);
-
-  llvm::ArrayRef<SemIR::NodeId> literal_elems;
-  if (value.kind() == SemIR::NodeKind::TupleLiteral) {
-    literal_elems =
-        context.semantics_ir().GetNodeBlock(value.GetAsTupleLiteral());
-  }
-
-  // Check that the tuple is the right size.
-  uint64_t array_bound =
-      context.semantics_ir().GetArrayBoundValue(array_bound_id);
-  if (tuple_elem_types.size() != array_bound) {
-    CARBON_DIAGNOSTIC(
-        ArrayInitFromLiteralArgCountMismatch, Error,
-        "Cannot initialize array of {0} element(s) from {1} initializer(s).",
-        uint64_t, size_t);
-    CARBON_DIAGNOSTIC(ArrayInitFromExpressionArgCountMismatch, Error,
-                      "Cannot initialize array of {0} element(s) from tuple "
-                      "with {1} element(s).",
-                      uint64_t, size_t);
-    context.emitter().Emit(
-        context.semantics_ir().GetNode(value_id).parse_node(),
-        literal_elems.empty() ? ArrayInitFromExpressionArgCountMismatch
-                              : ArrayInitFromLiteralArgCountMismatch,
-        array_bound, tuple_elem_types.size());
-    return SemIR::NodeId::BuiltinError;
-  }
-
-  // If we're initializing from a tuple literal, we will use its elements
-  // directly. Otherwise, materialize a temporary if needed and index into the
-  // result.
-  if (literal_elems.empty()) {
-    value_id = context.ConvertToValueOrReferenceExpression(value_id);
-  }
-
-  Context::PendingBlock target_block(context);
-
-  // Arrays are always initialized in-place. Tentatively allocate a temporary
-  // as the destination for the array initialization.
-  auto return_slot_id = target_block.AddNode(
-      SemIR::Node::TemporaryStorage::Make(value.parse_node(), array_type_id));
-
-  // Initialize each element of the array from the corresponding element of the
-  // tuple.
-  llvm::SmallVector<SemIR::NodeId> inits;
-  inits.reserve(array_bound + 1);
-  for (auto [i, src_type_id] : llvm::enumerate(tuple_elem_types)) {
-    Context::PendingBlock::DiscardUnusedNodesScope scope(target_block);
-    // TODO: Add a new node kind for indexing an array at a constant index
-    // so that we don't need an integer literal node here.
-    auto index_id = target_block.AddNode(SemIR::Node::IntegerLiteral::Make(
-        value.parse_node(),
-        context.CanonicalizeType(SemIR::NodeId::BuiltinIntegerType),
-        context.semantics_ir().AddIntegerLiteral(llvm::APInt(32, i))));
-    auto target_id = target_block.AddNode(SemIR::Node::ArrayIndex::Make(
-        value.parse_node(), element_type_id, return_slot_id, index_id));
-    // Note, this is computing the source location not the destination, so it
-    // goes into the current code block, not into the target block.
-    // TODO: Ideally we would also discard this node if it's unused.
-    auto src_id = !literal_elems.empty()
-                      ? literal_elems[i]
-                      : context.AddNode(SemIR::Node::TupleAccess::Make(
-                            value.parse_node(), src_type_id, value_id,
-                            SemIR::MemberIndex(i)));
-    auto init_id = context.InitializeAndFinalize(value.parse_node(), target_id,
-                                                 target_block, src_id);
-    if (init_id == SemIR::NodeId::BuiltinError) {
-      return SemIR::NodeId::BuiltinError;
-    }
-    inits.push_back(init_id);
-  }
-
-  // The last element of the refs block contains the return slot for the array
-  // initialization. Flush the temporary here if we didn't insert it earlier.
-  target_block.InsertHere();
-  inits.push_back(return_slot_id);
-
-  return context.AddNode(
-      SemIR::Node::ArrayInit::Make(value.parse_node(), array_type_id, value_id,
-                                   context.semantics_ir().AddNodeBlock(inits)));
-}
-
-auto Context::ImplicitAs(Parse::Node parse_node, SemIR::NodeId value_id,
-                         SemIR::TypeId as_type_id) -> SemIR::NodeId {
-  // Start by making sure both sides are valid. If any part is invalid, the
-  // result is invalid and we shouldn't error.
-  if (value_id == SemIR::NodeId::BuiltinError) {
-    // If the value is invalid, we can't do much, but do "succeed".
-    return value_id;
-  }
-  auto value = semantics_ir_->GetNode(value_id);
-  auto value_type_id = value.type_id();
-  if (value_type_id == SemIR::TypeId::Error ||
-      as_type_id == SemIR::TypeId::Error) {
-    return SemIR::NodeId::BuiltinError;
-  }
-
-  if (value_type_id == as_type_id) {
-    return value_id;
-  }
-
-  auto as_type = semantics_ir_->GetTypeAllowBuiltinTypes(as_type_id);
-  auto as_type_node = semantics_ir_->GetNode(as_type);
-
-  // A tuple (T1, T2, ..., Tn) converts to [T; n] if each Ti converts to T.
-  if (as_type_node.kind() == SemIR::NodeKind::ArrayType) {
-    auto value_type_node = semantics_ir_->GetNode(
-        semantics_ir_->GetTypeAllowBuiltinTypes(value_type_id));
-    if (value_type_node.kind() == SemIR::NodeKind::TupleType) {
-      // The conversion from tuple to array is `final`, so we don't need a
-      // fallback path here.
-      return ConvertTupleToArray(*this, value_type_node, as_type_node,
-                                 as_type_id, value_id);
-    }
-  }
-
-  if (as_type_id == SemIR::TypeId::TypeType) {
-    // A tuple of types converts to type `type`.
-    // TODO: This should apply even for non-literal tuples.
-    if (value.kind() == SemIR::NodeKind::TupleLiteral) {
-      // The conversion from tuple to `type` is `final`.
-      auto tuple_block_id = value.GetAsTupleLiteral();
-      llvm::SmallVector<SemIR::TypeId> type_ids;
-      // If it is empty tuple type, we don't fetch anything.
-      if (tuple_block_id != SemIR::NodeBlockId::Empty) {
-        const auto& tuple_block = semantics_ir_->GetNodeBlock(tuple_block_id);
-        for (auto tuple_node_id : tuple_block) {
-          // TODO: This call recurses back to this function. Switch to an
-          // iterative approach.
-          type_ids.push_back(
-              ExpressionAsType(value.parse_node(), tuple_node_id));
-        }
-      }
-      auto tuple_type_id =
-          CanonicalizeTupleType(value.parse_node(), std::move(type_ids));
-      return semantics_ir_->GetTypeAllowBuiltinTypes(tuple_type_id);
-    }
-    // When converting `{}` to a type, the result is `{} as type`.
-    // TODO: This conversion should also be performed for a non-literal value of
-    // type `{}`.
-    if (value.kind() == SemIR::NodeKind::StructLiteral &&
-        value.GetAsStructLiteral() == SemIR::NodeBlockId::Empty) {
-      return semantics_ir_->GetType(value_type_id);
-    }
-  }
-
-  // TODO: Handle ImplicitAs for compatible structs and tuples.
-
-  CARBON_DIAGNOSTIC(ImplicitAsConversionFailure, Error,
-                    "Cannot implicitly convert from `{0}` to `{1}`.",
-                    std::string, std::string);
-  emitter_
-      ->Build(parse_node, ImplicitAsConversionFailure,
-              semantics_ir_->StringifyType(
-                  semantics_ir_->GetNode(value_id).type_id()),
-              semantics_ir_->StringifyType(as_type_id))
-      .Emit();
-  return SemIR::NodeId::BuiltinError;
-}
-
 auto Context::ParamOrArgStart() -> void { params_or_args_stack_.Push(); }
 
 auto Context::ParamOrArgComma() -> void {

+ 0 - 83
toolchain/check/context.h

@@ -21,9 +21,6 @@ namespace Carbon::Check {
 // Context and shared functionality for semantics handlers.
 class Context {
  public:
-  // A block of code that has not yet been inserted into SemIR.
-  class PendingBlock;
-
   // Stores references for work.
   explicit Context(const Lex::TokenizedBuffer& tokens,
                    DiagnosticEmitter<Parse::Node>& emitter,
@@ -108,54 +105,6 @@ class Context {
   // Returns whether the current position in the current block is reachable.
   auto is_current_position_reachable() -> bool;
 
-  // Convert the given expression to a value expression of the same type.
-  auto ConvertToValueExpression(SemIR::NodeId expr_id) -> SemIR::NodeId;
-
-  // Convert the given expression to a value or reference expression of the same
-  // type.
-  auto ConvertToValueOrReferenceExpression(SemIR::NodeId expr_id,
-                                           bool discarded = false)
-      -> SemIR::NodeId;
-
-  // Performs initialization of `target_id` from `value_id`. Returns the
-  // possibly-converted initialization expression, which should be assigned to
-  // the target using a suitable node for the kind of initialization.
-  auto Initialize(Parse::Node parse_node, SemIR::NodeId target_id,
-                  SemIR::NodeId value_id) -> SemIR::NodeId;
-
-  // Performs and finalizes initialization of `target_id` from `value_id`. This
-  // is the same as `Initialize`, except that it also performs the final store
-  // from the initializer to the target if the initialization is not in-place.
-  // That final store is often undesirable as it is performed by the consumer
-  // of the initializer, such as an `Assign` or `ReturnExpression` node. The
-  // resulting node describes the initialization operation that was performed.
-  auto InitializeAndFinalize(Parse::Node parse_node, SemIR::NodeId target_id,
-                             PendingBlock& target_block, SemIR::NodeId value_id)
-      -> SemIR::NodeId;
-
-  // Converts `value_id` to a value expression of type `type_id`.
-  auto ConvertToValueOfType(Parse::Node parse_node, SemIR::NodeId value_id,
-                            SemIR::TypeId type_id) -> SemIR::NodeId {
-    return ConvertToValueExpression(ImplicitAs(parse_node, value_id, type_id));
-  }
-
-  // Converts `value_id` to a value expression of type `bool`.
-  auto ConvertToBoolValue(Parse::Node parse_node, SemIR::NodeId value_id)
-      -> SemIR::NodeId {
-    return ConvertToValueOfType(
-        parse_node, value_id, CanonicalizeType(SemIR::NodeId::BuiltinBoolType));
-  }
-
-  // Handles an expression whose result is discarded.
-  auto HandleDiscardedExpression(SemIR::NodeId id) -> void;
-
-  // Runs ImplicitAs for a set of arguments and parameters in a function call.
-  auto ImplicitAsForArgs(Parse::Node call_parse_node,
-                         SemIR::NodeBlockId arg_refs_id,
-                         Parse::Node param_parse_node,
-                         SemIR::NodeBlockId param_refs_id, bool has_return_slot)
-      -> bool;
-
   // Canonicalizes a type which is tracked as a single node.
   // TODO: This should eventually return a type ID.
   auto CanonicalizeType(SemIR::NodeId node_id) -> SemIR::TypeId;
@@ -180,13 +129,6 @@ class Context {
   auto GetPointerType(Parse::Node parse_node, SemIR::TypeId pointee_type_id)
       -> SemIR::TypeId;
 
-  // Converts an expression for use as a type.
-  auto ExpressionAsType(Parse::Node parse_node, SemIR::NodeId value_id)
-      -> SemIR::TypeId {
-    return CanonicalizeType(
-        ConvertToValueOfType(parse_node, value_id, SemIR::TypeId::TypeType));
-  }
-
   // Removes any top-level `const` qualifiers from a type.
   auto GetUnqualifiedType(SemIR::TypeId type_id) -> SemIR::TypeId;
 
@@ -268,31 +210,6 @@ class Context {
     // TODO: This likely needs to track things which need to be destructed.
   };
 
-  // Implementation of `Initialize`. Takes a `target_block` which contains
-  // pending instructions that are needed to form the value of `target_id`.
-  // These can be discarded if no initialization is needed.
-  auto InitializeImpl(Parse::Node parse_node, SemIR::NodeId target_id,
-                      PendingBlock& target_block, SemIR::NodeId value_id)
-      -> SemIR::NodeId;
-
-  // Commits to using a temporary to store the result of the initializing
-  // expression described by `init_id`, and returns the location of the
-  // temporary. If `discarded` is `true`, the result is discarded, and no
-  // temporary will be created if possible; if no temporary is created, the
-  // return value will be `SemIR::NodeId::Invalid`.
-  auto FinalizeTemporary(SemIR::NodeId init_id, bool discarded)
-      -> SemIR::NodeId;
-
-  // Marks the initializer `init_id` as initializing `target_id`.
-  auto MarkInitializerFor(SemIR::NodeId init_id, SemIR::NodeId target_id,
-                          PendingBlock& target_block) -> void;
-
-  // Runs ImplicitAs behavior to convert `value` to `as_type`, returning the
-  // converted result. Prints a diagnostic and returns an Error if the
-  // conversion cannot be performed.
-  auto ImplicitAs(Parse::Node parse_node, SemIR::NodeId value_id,
-                  SemIR::TypeId as_type_id) -> SemIR::NodeId;
-
   // Forms a canonical type ID for a type. This function is given two
   // callbacks:
   //

+ 838 - 0
toolchain/check/convert.cpp

@@ -0,0 +1,838 @@
+// 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 "toolchain/check/convert.h"
+
+#include <string>
+#include <utility>
+
+#include "common/check.h"
+#include "llvm/ADT/STLExtras.h"
+#include "toolchain/check/context.h"
+#include "toolchain/diagnostics/diagnostic_kind.h"
+#include "toolchain/parse/node_kind.h"
+#include "toolchain/sem_ir/file.h"
+#include "toolchain/sem_ir/node.h"
+#include "toolchain/sem_ir/node_kind.h"
+
+namespace Carbon::Check {
+
+// Given an initializing expression, find its return slot. Returns `Invalid` if
+// there is no return slot, because the initialization is not performed in
+// place.
+static auto FindReturnSlotForInitializer(SemIR::File& semantics_ir,
+                                         SemIR::NodeId init_id)
+    -> SemIR::NodeId {
+  SemIR::Node init = semantics_ir.GetNode(init_id);
+  switch (init.kind()) {
+    default:
+      CARBON_FATAL() << "Initialization from unexpected node " << init;
+
+    case SemIR::NodeKind::StructInit:
+    case SemIR::NodeKind::TupleInit:
+      // TODO: Track a return slot for these initializers.
+      CARBON_FATAL() << init
+                     << " should be created with its return slot already "
+                        "filled in properly";
+
+    case SemIR::NodeKind::InitializeFrom: {
+      auto [src_id, dest_id] = init.GetAsInitializeFrom();
+      return dest_id;
+    }
+
+    case SemIR::NodeKind::Call: {
+      auto [refs_id, callee_id] = init.GetAsCall();
+      if (!semantics_ir.GetFunction(callee_id).return_slot_id.is_valid()) {
+        return SemIR::NodeId::Invalid;
+      }
+      return semantics_ir.GetNodeBlock(refs_id).back();
+    }
+
+    case SemIR::NodeKind::ArrayInit: {
+      auto [src_id, refs_id] = init.GetAsArrayInit();
+      return semantics_ir.GetNodeBlock(refs_id).back();
+    }
+  }
+}
+
+// Marks the initializer `init_id` as initializing `target_id`.
+static auto MarkInitializerFor(SemIR::File& semantics_ir, SemIR::NodeId init_id,
+                               SemIR::NodeId target_id,
+                               PendingBlock& target_block) -> void {
+  auto return_slot_id = FindReturnSlotForInitializer(semantics_ir, init_id);
+  if (return_slot_id.is_valid()) {
+    // Replace the temporary in the return slot with a reference to our target.
+    CARBON_CHECK(semantics_ir.GetNode(return_slot_id).kind() ==
+                 SemIR::NodeKind::TemporaryStorage)
+        << "Return slot for initializer does not contain a temporary; "
+        << "initialized multiple times? Have "
+        << semantics_ir.GetNode(return_slot_id);
+    target_block.MergeReplacing(return_slot_id, target_id);
+  }
+}
+
+// Commits to using a temporary to store the result of the initializing
+// expression described by `init_id`, and returns the location of the
+// temporary. If `discarded` is `true`, the result is discarded, and no
+// temporary will be created if possible; if no temporary is created, the
+// return value will be `SemIR::NodeId::Invalid`.
+static auto FinalizeTemporary(Context& context, SemIR::NodeId init_id,
+                              bool discarded) -> SemIR::NodeId {
+  auto& semantics_ir = context.semantics_ir();
+  auto return_slot_id = FindReturnSlotForInitializer(semantics_ir, init_id);
+  if (return_slot_id.is_valid()) {
+    // The return slot should already have a materialized temporary in it.
+    CARBON_CHECK(semantics_ir.GetNode(return_slot_id).kind() ==
+                 SemIR::NodeKind::TemporaryStorage)
+        << "Return slot for initializer does not contain a temporary; "
+        << "initialized multiple times? Have "
+        << semantics_ir.GetNode(return_slot_id);
+    auto init = semantics_ir.GetNode(init_id);
+    return context.AddNode(SemIR::Node::Temporary::Make(
+        init.parse_node(), init.type_id(), return_slot_id, init_id));
+  }
+
+  if (discarded) {
+    // Don't invent a temporary that we're going to discard.
+    return SemIR::NodeId::Invalid;
+  }
+
+  // The initializer has no return slot, but we want to produce a temporary
+  // object. Materialize one now.
+  // TODO: Consider using an invalid ID to mean that we immediately
+  // materialize and initialize a temporary, rather than two separate
+  // nodes.
+  auto init = semantics_ir.GetNode(init_id);
+  auto temporary_id = context.AddNode(
+      SemIR::Node::TemporaryStorage::Make(init.parse_node(), init.type_id()));
+  return context.AddNode(SemIR::Node::Temporary::Make(
+      init.parse_node(), init.type_id(), temporary_id, init_id));
+}
+
+// Materialize a temporary to hold the result of the given expression if it is
+// an initializing expression.
+static auto MaterializeIfInitializing(Context& context, SemIR::NodeId expr_id)
+    -> SemIR::NodeId {
+  if (GetExpressionCategory(context.semantics_ir(), expr_id) ==
+      SemIR::ExpressionCategory::Initializing) {
+    return FinalizeTemporary(context, expr_id, /*discarded=*/false);
+  }
+  return expr_id;
+}
+
+// Creates and adds a node to perform element access into an aggregate.
+template <typename AccessNodeT, typename NodeBlockT>
+static auto MakeElemAccessNode(Context& context, Parse::Node parse_node,
+                               SemIR::NodeId aggregate_id,
+                               SemIR::TypeId elem_type_id, NodeBlockT& block,
+                               std::size_t i) {
+  if constexpr (std::is_same_v<AccessNodeT, SemIR::Node::ArrayIndex>) {
+    // TODO: Add a new node kind for indexing an array at a constant index
+    // so that we don't need an integer literal node here, and remove this
+    // special case.
+    auto index_id = block.AddNode(SemIR::Node::IntegerLiteral::Make(
+        parse_node, context.CanonicalizeType(SemIR::NodeId::BuiltinIntegerType),
+        context.semantics_ir().AddIntegerLiteral(llvm::APInt(32, i))));
+    return block.AddNode(
+        AccessNodeT::Make(parse_node, elem_type_id, aggregate_id, index_id));
+  } else {
+    return block.AddNode(AccessNodeT::Make(
+        parse_node, elem_type_id, aggregate_id, SemIR::MemberIndex(i)));
+  }
+}
+
+// Converts an element of one aggregate so that it can be used as an element of
+// another aggregate.
+//
+// For the source: `src_id` is the source aggregate, `src_elem_type` is the
+// element type, `i` is the index, and `SourceAccessNodeT` is the kind of node
+// used to access the source element.
+//
+// For the target: `kind` is the kind of conversion or initialization,
+// `target_elem_type` is the element type. For initialization, `target_id` is
+// the destination, `target_block` is a pending block for target location
+// calculations that will be spliced as the return slot of the initializer if
+// necessary, `i` is the index, and `TargetAccessNodeT` is the kind of node
+// used to access the destination element.
+template <typename SourceAccessNodeT, typename TargetAccessNodeT>
+static auto ConvertAggregateElement(
+    Context& context, Parse::Node parse_node, SemIR::NodeId src_id,
+    SemIR::TypeId src_elem_type,
+    llvm::ArrayRef<SemIR::NodeId> src_literal_elems,
+    ConversionTarget::Kind kind, SemIR::NodeId target_id,
+    SemIR::TypeId target_elem_type, PendingBlock* target_block, std::size_t i) {
+  // Compute the location of the source element. This goes into the current code
+  // block, not into the target block.
+  // TODO: Ideally we would discard this node if it's unused.
+  auto src_elem_id =
+      !src_literal_elems.empty()
+          ? src_literal_elems[i]
+          : MakeElemAccessNode<SourceAccessNodeT>(context, parse_node, src_id,
+                                                  src_elem_type, context, i);
+
+  // If we're performing a conversion rather than an initialization, we won't
+  // have or need a target.
+  ConversionTarget target = {.kind = kind, .type_id = target_elem_type};
+  if (!target.is_initializer()) {
+    return Convert(context, parse_node, src_elem_id, target);
+  }
+
+  // Compute the location of the target element and initialize it.
+  PendingBlock::DiscardUnusedNodesScope scope(target_block);
+  target.init_block = target_block;
+  target.init_id = MakeElemAccessNode<TargetAccessNodeT>(
+      context, parse_node, target_id, target_elem_type, *target_block, i);
+  return Convert(context, parse_node, src_elem_id, target);
+}
+
+namespace {
+// A handle to a new block that may be modified, with copy-on-write semantics.
+//
+// The constructor is given the ID of an existing block that provides the
+// initial contents of the new block. The new block is lazily allocated; if no
+// modifications have been made, the `id()` function will return the original
+// block ID.
+//
+// This is intended to avoid an unnecessary block allocation in the case where
+// the new block ends up being exactly the same as the original block.
+class CopyOnWriteBlock {
+ public:
+  // Constructs the block. If `source_id` is valid, it is used as the initial
+  // value of the block. Otherwise, uninitialized storage for `size` elements
+  // is allocated.
+  CopyOnWriteBlock(SemIR::File& file, SemIR::NodeBlockId source_id, size_t size)
+      : file_(file), source_id_(source_id) {
+    if (!source_id_.is_valid()) {
+      id_ = file_.AddUninitializedNodeBlock(size);
+    }
+  }
+
+  auto id() -> SemIR::NodeBlockId const { return id_; }
+
+  auto Set(int i, SemIR::NodeId value) -> void {
+    if (source_id_.is_valid() && file_.GetNodeBlock(id_)[i] == value) {
+      return;
+    }
+    if (id_ == source_id_) {
+      id_ = file_.AddNodeBlock(file_.GetNodeBlock(source_id_));
+    }
+    file_.GetNodeBlock(id_)[i] = value;
+  }
+
+ private:
+  SemIR::File& file_;
+  SemIR::NodeBlockId source_id_;
+  SemIR::NodeBlockId id_ = source_id_;
+};
+}  // namespace
+
+// Performs a conversion from a tuple to an array type. Does not perform a
+// final conversion to the requested expression category.
+static auto ConvertTupleToArray(Context& context, SemIR::Node tuple_type,
+                                SemIR::Node array_type, SemIR::NodeId value_id,
+                                ConversionTarget target) -> SemIR::NodeId {
+  auto& semantics_ir = context.semantics_ir();
+  auto [array_bound_id, element_type_id] = array_type.GetAsArrayType();
+  auto tuple_elem_types_id = tuple_type.GetAsTupleType();
+  const auto& tuple_elem_types = semantics_ir.GetTypeBlock(tuple_elem_types_id);
+
+  auto value = semantics_ir.GetNode(value_id);
+
+  // If we're initializing from a tuple literal, we will use its elements
+  // directly. Otherwise, materialize a temporary if needed and index into the
+  // result.
+  llvm::ArrayRef<SemIR::NodeId> literal_elems;
+  if (value.kind() == SemIR::NodeKind::TupleLiteral) {
+    literal_elems = semantics_ir.GetNodeBlock(value.GetAsTupleLiteral());
+  } else {
+    value_id = MaterializeIfInitializing(context, value_id);
+  }
+
+  // Check that the tuple is the right size.
+  uint64_t array_bound = semantics_ir.GetArrayBoundValue(array_bound_id);
+  if (tuple_elem_types.size() != array_bound) {
+    CARBON_DIAGNOSTIC(
+        ArrayInitFromLiteralArgCountMismatch, Error,
+        "Cannot initialize array of {0} element(s) from {1} initializer(s).",
+        uint64_t, size_t);
+    CARBON_DIAGNOSTIC(ArrayInitFromExpressionArgCountMismatch, Error,
+                      "Cannot initialize array of {0} element(s) from tuple "
+                      "with {1} element(s).",
+                      uint64_t, size_t);
+    context.emitter().Emit(value.parse_node(),
+                           literal_elems.empty()
+                               ? ArrayInitFromExpressionArgCountMismatch
+                               : ArrayInitFromLiteralArgCountMismatch,
+                           array_bound, tuple_elem_types.size());
+    return SemIR::NodeId::BuiltinError;
+  }
+
+  PendingBlock target_block_storage(context);
+  PendingBlock* target_block =
+      target.init_block ? target.init_block : &target_block_storage;
+
+  // Arrays are always initialized in-place. Allocate a temporary as the
+  // destination for the array initialization if we weren't given one.
+  SemIR::NodeId return_slot_id = target.init_id;
+  if (!target.init_id.is_valid()) {
+    return_slot_id = target_block->AddNode(SemIR::Node::TemporaryStorage::Make(
+        value.parse_node(), target.type_id));
+  }
+
+  // Initialize each element of the array from the corresponding element of the
+  // tuple.
+  // TODO: Annotate diagnostics coming from here with the array element index,
+  // if initializing from a tuple literal.
+  llvm::SmallVector<SemIR::NodeId> inits;
+  inits.reserve(array_bound + 1);
+  for (auto [i, src_type_id] : llvm::enumerate(tuple_elem_types)) {
+    // TODO: This call recurses back into conversion. Switch to an iterative
+    // approach.
+    auto init_id = ConvertAggregateElement<SemIR::Node::TupleAccess,
+                                           SemIR::Node::ArrayIndex>(
+        context, value.parse_node(), value_id, src_type_id, literal_elems,
+        ConversionTarget::FullInitializer, return_slot_id, element_type_id,
+        target_block, i);
+    if (init_id == SemIR::NodeId::BuiltinError) {
+      return SemIR::NodeId::BuiltinError;
+    }
+    inits.push_back(init_id);
+  }
+
+  // The last element of the refs block contains the return slot for the array
+  // initialization. Flush the temporary here if we didn't insert it earlier.
+  target_block->InsertHere();
+  inits.push_back(return_slot_id);
+
+  return context.AddNode(
+      SemIR::Node::ArrayInit::Make(value.parse_node(), target.type_id, value_id,
+                                   semantics_ir.AddNodeBlock(inits)));
+}
+
+// Performs a conversion from a tuple to a tuple type. Does not perform a
+// final conversion to the requested expression category.
+static auto ConvertTupleToTuple(Context& context, SemIR::Node src_type,
+                                SemIR::Node dest_type, SemIR::NodeId value_id,
+                                ConversionTarget target) -> SemIR::NodeId {
+  auto& semantics_ir = context.semantics_ir();
+  auto src_elem_types = semantics_ir.GetTypeBlock(src_type.GetAsTupleType());
+  auto dest_elem_types = semantics_ir.GetTypeBlock(dest_type.GetAsTupleType());
+
+  auto value = semantics_ir.GetNode(value_id);
+
+  // If we're initializing from a tuple literal, we will use its elements
+  // directly. Otherwise, materialize a temporary if needed and index into the
+  // result.
+  llvm::ArrayRef<SemIR::NodeId> literal_elems;
+  if (value.kind() == SemIR::NodeKind::TupleLiteral) {
+    literal_elems = semantics_ir.GetNodeBlock(value.GetAsTupleLiteral());
+  } else {
+    value_id = MaterializeIfInitializing(context, value_id);
+  }
+
+  // Check that the tuples are the same size.
+  if (src_elem_types.size() != dest_elem_types.size()) {
+    CARBON_DIAGNOSTIC(TupleInitElementCountMismatch, Error,
+                      "Cannot initialize tuple of {0} element(s) from tuple "
+                      "with {1} element(s).",
+                      size_t, size_t);
+    context.emitter().Emit(value.parse_node(), TupleInitElementCountMismatch,
+                           dest_elem_types.size(), src_elem_types.size());
+    return SemIR::NodeId::BuiltinError;
+  }
+
+  // If we're forming an initializer, then we want an initializer for each
+  // element. Otherwise, we want a value representation for each element.
+  // Perform a final destination store if we're performing an in-place
+  // initialization.
+  bool is_init = target.is_initializer();
+  ConversionTarget::Kind inner_kind =
+      !is_init ? ConversionTarget::Value
+      : SemIR::GetInitializingRepresentation(semantics_ir, target.type_id)
+                  .kind == SemIR::InitializingRepresentation::InPlace
+          ? ConversionTarget::FullInitializer
+          : ConversionTarget::Initializer;
+
+  // Initialize each element of the destination from the corresponding element
+  // of the source.
+  // TODO: Annotate diagnostics coming from here with the element index.
+  CopyOnWriteBlock new_block(semantics_ir,
+                             value.kind() == SemIR::NodeKind::TupleLiteral
+                                 ? value.GetAsTupleLiteral()
+                                 : SemIR::NodeBlockId::Invalid,
+                             src_elem_types.size());
+  for (auto [i, src_type_id, dest_type_id] :
+       llvm::enumerate(src_elem_types, dest_elem_types)) {
+    // TODO: This call recurses back into conversion. Switch to an iterative
+    // approach.
+    auto init_id = ConvertAggregateElement<SemIR::Node::TupleAccess,
+                                           SemIR::Node::TupleAccess>(
+        context, value.parse_node(), value_id, src_type_id, literal_elems,
+        inner_kind, target.init_id, dest_type_id, target.init_block, i);
+    if (init_id == SemIR::NodeId::BuiltinError) {
+      return SemIR::NodeId::BuiltinError;
+    }
+    new_block.Set(i, init_id);
+  }
+
+  return context.AddNode(
+      is_init
+          ? SemIR::Node::TupleInit::Make(value.parse_node(), target.type_id,
+                                         value_id, new_block.id())
+          : SemIR::Node::TupleValue::Make(value.parse_node(), target.type_id,
+                                          value_id, new_block.id()));
+}
+
+// Performs a conversion from a struct to a struct type. Does not perform a
+// final conversion to the requested expression category.
+static auto ConvertStructToStruct(Context& context, SemIR::Node src_type,
+                                  SemIR::Node dest_type, SemIR::NodeId value_id,
+                                  ConversionTarget target) -> SemIR::NodeId {
+  auto& semantics_ir = context.semantics_ir();
+  auto src_elem_fields = semantics_ir.GetNodeBlock(src_type.GetAsStructType());
+  auto dest_elem_fields =
+      semantics_ir.GetNodeBlock(dest_type.GetAsStructType());
+
+  auto value = semantics_ir.GetNode(value_id);
+
+  // If we're initializing from a struct literal, we will use its elements
+  // directly. Otherwise, materialize a temporary if needed and index into the
+  // result.
+  llvm::ArrayRef<SemIR::NodeId> literal_elems;
+  if (value.kind() == SemIR::NodeKind::StructLiteral) {
+    literal_elems = semantics_ir.GetNodeBlock(value.GetAsStructLiteral());
+  } else {
+    value_id = MaterializeIfInitializing(context, value_id);
+  }
+
+  // Check that the structs are the same size.
+  // TODO: Check the field names are the same up to permutation, compute the
+  // permutation, and use it below.
+  if (src_elem_fields.size() != dest_elem_fields.size()) {
+    CARBON_DIAGNOSTIC(StructInitElementCountMismatch, Error,
+                      "Cannot initialize struct of {0} element(s) from struct "
+                      "with {1} element(s).",
+                      size_t, size_t);
+    context.emitter().Emit(value.parse_node(), StructInitElementCountMismatch,
+                           dest_elem_fields.size(), src_elem_fields.size());
+    return SemIR::NodeId::BuiltinError;
+  }
+
+  // If we're forming an initializer, then we want an initializer for each
+  // element. Otherwise, we want a value representation for each element.
+  // Perform a final destination store if we're performing an in-place
+  // initialization.
+  bool is_init = target.is_initializer();
+  ConversionTarget::Kind inner_kind =
+      !is_init ? ConversionTarget::Value
+      : SemIR::GetInitializingRepresentation(semantics_ir, target.type_id)
+                  .kind == SemIR::InitializingRepresentation::InPlace
+          ? ConversionTarget::FullInitializer
+          : ConversionTarget::Initializer;
+
+  // Initialize each element of the destination from the corresponding element
+  // of the source.
+  // TODO: Annotate diagnostics coming from here with the element index.
+  CopyOnWriteBlock new_block(semantics_ir,
+                             value.kind() == SemIR::NodeKind::StructLiteral
+                                 ? value.GetAsStructLiteral()
+                                 : SemIR::NodeBlockId::Invalid,
+                             src_elem_fields.size());
+  for (auto [i, src_field_id, dest_field_id] :
+       llvm::enumerate(src_elem_fields, dest_elem_fields)) {
+    auto [src_name_id, src_type_id] =
+        semantics_ir.GetNode(src_field_id).GetAsStructTypeField();
+    auto [dest_name_id, dest_type_id] =
+        semantics_ir.GetNode(dest_field_id).GetAsStructTypeField();
+    if (src_name_id != dest_name_id) {
+      CARBON_DIAGNOSTIC(
+          StructInitFieldNameMismatch, Error,
+          "Mismatched names for field {0} in struct initialization: "
+          "source has field name `{1}`, destination has field name `{2}`.",
+          size_t, llvm::StringRef, llvm::StringRef);
+      context.emitter().Emit(value.parse_node(), StructInitFieldNameMismatch,
+                             i + 1, semantics_ir.GetString(src_name_id),
+                             semantics_ir.GetString(dest_name_id));
+      return SemIR::NodeId::BuiltinError;
+    }
+
+    // TODO: This call recurses back into conversion. Switch to an iterative
+    // approach.
+    auto init_id = ConvertAggregateElement<SemIR::Node::StructAccess,
+                                           SemIR::Node::StructAccess>(
+        context, value.parse_node(), value_id, src_type_id, literal_elems,
+        inner_kind, target.init_id, dest_type_id, target.init_block, i);
+    if (init_id == SemIR::NodeId::BuiltinError) {
+      return SemIR::NodeId::BuiltinError;
+    }
+    new_block.Set(i, init_id);
+  }
+
+  return context.AddNode(
+      is_init
+          ? SemIR::Node::StructInit::Make(value.parse_node(), target.type_id,
+                                          value_id, new_block.id())
+          : SemIR::Node::StructValue::Make(value.parse_node(), target.type_id,
+                                           value_id, new_block.id()));
+}
+
+// Returns whether `category` is a valid expression category to produce as a
+// result of a conversion with kind `target_kind`, or at most needs a temporary
+// to be materialized.
+static bool IsValidExpressionCategoryForConversionTarget(
+    SemIR::ExpressionCategory category, ConversionTarget::Kind target_kind) {
+  switch (target_kind) {
+    case ConversionTarget::Value:
+      return category == SemIR::ExpressionCategory::Value;
+    case ConversionTarget::ValueOrReference:
+    case ConversionTarget::Discarded:
+      return category == SemIR::ExpressionCategory::Value ||
+             category == SemIR::ExpressionCategory::DurableReference ||
+             category == SemIR::ExpressionCategory::EphemeralReference ||
+             category == SemIR::ExpressionCategory::Initializing;
+    case ConversionTarget::Initializer:
+    case ConversionTarget::FullInitializer:
+      return category == SemIR::ExpressionCategory::Initializing;
+  }
+}
+
+static auto PerformBuiltinConversion(Context& context, Parse::Node parse_node,
+                                     SemIR::NodeId value_id,
+                                     ConversionTarget target) -> SemIR::NodeId {
+  auto& semantics_ir = context.semantics_ir();
+  auto value = semantics_ir.GetNode(value_id);
+  auto value_type_id = value.type_id();
+  auto target_type_node = semantics_ir.GetNode(
+      semantics_ir.GetTypeAllowBuiltinTypes(target.type_id));
+
+  // Various forms of implicit conversion are supported as builtin conversions,
+  // either in addition to or instead of `impl`s of `ImplicitAs` in the Carbon
+  // prelude. There are a few reasons we need to perform some of these
+  // conversions as builtins:
+  //
+  // 1) Conversions from struct and tuple *literals* have special rules that
+  //    cannot be implemented by invoking `ImplicitAs`. Specifically, we must
+  //    recurse into the elements of the literal before performing
+  //    initialization in order to avoid unnecessary conversions between
+  //    expression categories that would be performed by `ImplicitAs.Convert`.
+  // 2) (Not implemented yet) Conversion of a facet to a facet type depends on
+  //    the value of the facet, not only its type, and therefore cannot be
+  //    modeled by `ImplicitAs`.
+  // 3) Some of these conversions are used while checking the library
+  //    definition of `ImplicitAs` itself or implementations of it.
+  //
+  // We also expect to see better performance by avoiding an `impl` lookup for
+  // common conversions.
+  //
+  // TODO: We should provide a debugging flag to turn off as many of these
+  // builtin conversions as we can so that we can test that they do the same
+  // thing as the library implementations.
+  //
+  // The builtin conversions that correspond to `impl`s in the library all
+  // correspond to `final impl`s, so we don't need to worry about `ImplicitAs`
+  // being specialized in any of these cases.
+
+  // If the value is already of the right kind and expression category, there's
+  // nothing to do. Performing a conversion would decompose and rebuild tuples
+  // and structs, so it's important that we bail out early in this case.
+  if (value_type_id == target.type_id &&
+      IsValidExpressionCategoryForConversionTarget(
+          SemIR::GetExpressionCategory(semantics_ir, value_id), target.kind)) {
+    return value_id;
+  }
+
+  // A tuple (T1, T2, ..., Tn) converts to (U1, U2, ..., Un) if each Ti
+  // converts to Ui.
+  if (target_type_node.kind() == SemIR::NodeKind::TupleType) {
+    auto value_type_node = semantics_ir.GetNode(
+        semantics_ir.GetTypeAllowBuiltinTypes(value_type_id));
+    if (value_type_node.kind() == SemIR::NodeKind::TupleType) {
+      return ConvertTupleToTuple(context, value_type_node, target_type_node,
+                                 value_id, target);
+    }
+  }
+
+  // A struct {.f_1: T_1, .f_2: T_2, ..., .f_n: T_n} converts to
+  // {.f_p(1): U_p(1), .f_p(2): U_p(2), ..., .f_p(n): U_p(n)} if
+  // (p(1), ..., p(n)) is a permutation of (1, ..., n) and each Ti converts
+  // to Ui.
+  if (target_type_node.kind() == SemIR::NodeKind::StructType) {
+    auto value_type_node = semantics_ir.GetNode(
+        semantics_ir.GetTypeAllowBuiltinTypes(value_type_id));
+    if (value_type_node.kind() == SemIR::NodeKind::StructType) {
+      return ConvertStructToStruct(context, value_type_node, target_type_node,
+                                   value_id, target);
+    }
+  }
+
+  // A tuple (T1, T2, ..., Tn) converts to [T; n] if each Ti converts to T.
+  if (target_type_node.kind() == SemIR::NodeKind::ArrayType) {
+    auto value_type_node = semantics_ir.GetNode(
+        semantics_ir.GetTypeAllowBuiltinTypes(value_type_id));
+    if (value_type_node.kind() == SemIR::NodeKind::TupleType) {
+      return ConvertTupleToArray(context, value_type_node, target_type_node,
+                                 value_id, target);
+    }
+  }
+
+  if (target.type_id == SemIR::TypeId::TypeType) {
+    // A tuple of types converts to type `type`.
+    // TODO: This should apply even for non-literal tuples.
+    if (value.kind() == SemIR::NodeKind::TupleLiteral) {
+      auto tuple_block_id = value.GetAsTupleLiteral();
+      llvm::SmallVector<SemIR::TypeId> type_ids;
+      // If it is empty tuple type, we don't fetch anything.
+      if (tuple_block_id != SemIR::NodeBlockId::Empty) {
+        const auto& tuple_block = semantics_ir.GetNodeBlock(tuple_block_id);
+        for (auto tuple_node_id : tuple_block) {
+          // TODO: This call recurses back into conversion. Switch to an
+          // iterative approach.
+          type_ids.push_back(
+              ExpressionAsType(context, parse_node, tuple_node_id));
+        }
+      }
+      auto tuple_type_id =
+          context.CanonicalizeTupleType(parse_node, std::move(type_ids));
+      return semantics_ir.GetTypeAllowBuiltinTypes(tuple_type_id);
+    }
+
+    // `{}` converts to `{} as type`.
+    // TODO: This conversion should also be performed for a non-literal value
+    // of type `{}`.
+    if (value.kind() == SemIR::NodeKind::StructLiteral &&
+        value.GetAsStructLiteral() == SemIR::NodeBlockId::Empty) {
+      value_id = semantics_ir.GetTypeAllowBuiltinTypes(value_type_id);
+    }
+  }
+
+  // No builtin conversion applies.
+  return value_id;
+}
+
+auto Convert(Context& context, Parse::Node parse_node, SemIR::NodeId expr_id,
+             ConversionTarget target) -> SemIR::NodeId {
+  auto& semantics_ir = context.semantics_ir();
+  auto orig_expr_id = expr_id;
+
+  // Start by making sure both sides are valid. If any part is invalid, the
+  // result is invalid and we shouldn't error.
+  if (expr_id == SemIR::NodeId::BuiltinError) {
+    return expr_id;
+  }
+  if (semantics_ir.GetNode(expr_id).type_id() == SemIR::TypeId::Error ||
+      target.type_id == SemIR::TypeId::Error) {
+    return SemIR::NodeId::BuiltinError;
+  }
+
+  if (SemIR::GetExpressionCategory(semantics_ir, expr_id) ==
+      SemIR::ExpressionCategory::NotExpression) {
+    // TODO: We currently encounter this for use of namespaces and functions.
+    // We should provide a better diagnostic for inappropriate use of
+    // namespace names, and allow use of functions as values.
+    CARBON_DIAGNOSTIC(UseOfNonExpressionAsValue, Error,
+                      "Expression cannot be used as a value.");
+    context.emitter().Emit(semantics_ir.GetNode(expr_id).parse_node(),
+                           UseOfNonExpressionAsValue);
+    return SemIR::NodeId::BuiltinError;
+  }
+
+  // Check whether any builtin conversion applies.
+  expr_id = PerformBuiltinConversion(context, parse_node, expr_id, target);
+  if (expr_id == SemIR::NodeId::BuiltinError) {
+    return expr_id;
+  }
+
+  // If the types don't match at this point, we can't perform the conversion.
+  // TODO: Look for an ImplicitAs impl.
+  SemIR::Node expr = semantics_ir.GetNode(expr_id);
+  if (expr.type_id() != target.type_id) {
+    CARBON_DIAGNOSTIC(ImplicitAsConversionFailure, Error,
+                      "Cannot implicitly convert from `{0}` to `{1}`.",
+                      std::string, std::string);
+    context.emitter()
+        .Build(parse_node, ImplicitAsConversionFailure,
+               semantics_ir.StringifyType(expr.type_id()),
+               semantics_ir.StringifyType(target.type_id))
+        .Emit();
+    return SemIR::NodeId::BuiltinError;
+  }
+
+  // Now perform any necessary value category conversions.
+  switch (SemIR::GetExpressionCategory(semantics_ir, expr_id)) {
+    case SemIR::ExpressionCategory::NotExpression:
+    case SemIR::ExpressionCategory::Mixed:
+      CARBON_FATAL() << "Unexpected expression " << expr
+                     << " after builtin conversions";
+
+    case SemIR::ExpressionCategory::Initializing:
+      if (target.is_initializer()) {
+        if (orig_expr_id == expr_id) {
+          // Don't fill in the return slot if we created the expression through
+          // a conversion. In that case, we will have created it with the
+          // target already set.
+          // TODO: Find a better way to track whether we need to do this.
+          MarkInitializerFor(semantics_ir, expr_id, target.init_id,
+                             *target.init_block);
+        }
+        break;
+      }
+
+      // Commit to using a temporary for this initializing expression.
+      // TODO: Don't create a temporary if the initializing representation
+      // is already a value representation.
+      expr_id = FinalizeTemporary(context, expr_id,
+                                  target.kind == ConversionTarget::Discarded);
+      // We now have an ephemeral reference.
+      [[fallthrough]];
+
+    case SemIR::ExpressionCategory::DurableReference:
+    case SemIR::ExpressionCategory::EphemeralReference: {
+      // If we have a reference and don't want one, form a value binding.
+      if (target.kind != ConversionTarget::ValueOrReference &&
+          target.kind != ConversionTarget::Discarded) {
+        // TODO: Support types with custom value representations.
+        expr_id = context.AddNode(SemIR::Node::BindValue::Make(
+            expr.parse_node(), expr.type_id(), expr_id));
+      }
+      break;
+    }
+
+    case SemIR::ExpressionCategory::Value:
+      break;
+  }
+
+  // Perform a final destination store, if necessary.
+  if (target.kind == ConversionTarget::FullInitializer) {
+    if (auto init_rep =
+            SemIR::GetInitializingRepresentation(semantics_ir, target.type_id);
+        init_rep.kind == SemIR::InitializingRepresentation::ByCopy) {
+      target.init_block->InsertHere();
+      expr_id = context.AddNode(SemIR::Node::InitializeFrom::Make(
+          parse_node, target.type_id, expr_id, target.init_id));
+    }
+  }
+
+  return expr_id;
+}
+
+auto Initialize(Context& context, Parse::Node parse_node,
+                SemIR::NodeId target_id, SemIR::NodeId value_id)
+    -> SemIR::NodeId {
+  PendingBlock target_block(context);
+  return Convert(
+      context, parse_node, value_id,
+      {.kind = ConversionTarget::Initializer,
+       .type_id = context.semantics_ir().GetNode(target_id).type_id(),
+       .init_id = target_id,
+       .init_block = &target_block});
+}
+
+auto ConvertToValueExpression(Context& context, SemIR::NodeId expr_id)
+    -> SemIR::NodeId {
+  auto expr = context.semantics_ir().GetNode(expr_id);
+  return Convert(context, expr.parse_node(), expr_id,
+                 {.kind = ConversionTarget::Value, .type_id = expr.type_id()});
+}
+
+auto ConvertToValueOrReferenceExpression(Context& context,
+                                         SemIR::NodeId expr_id)
+    -> SemIR::NodeId {
+  auto expr = context.semantics_ir().GetNode(expr_id);
+  return Convert(
+      context, expr.parse_node(), expr_id,
+      {.kind = ConversionTarget::ValueOrReference, .type_id = expr.type_id()});
+}
+
+auto ConvertToValueOfType(Context& context, Parse::Node parse_node,
+                          SemIR::NodeId value_id, SemIR::TypeId type_id)
+    -> SemIR::NodeId {
+  return Convert(context, parse_node, value_id,
+                 {.kind = ConversionTarget::Value, .type_id = type_id});
+}
+
+auto ConvertToBoolValue(Context& context, Parse::Node parse_node,
+                        SemIR::NodeId value_id) -> SemIR::NodeId {
+  return ConvertToValueOfType(
+      context, parse_node, value_id,
+      context.CanonicalizeType(SemIR::NodeId::BuiltinBoolType));
+}
+
+auto ConvertCallArgs(Context& context, Parse::Node call_parse_node,
+                     SemIR::NodeBlockId arg_refs_id,
+                     Parse::Node param_parse_node,
+                     SemIR::NodeBlockId param_refs_id, bool has_return_slot)
+    -> bool {
+  // If both arguments and parameters are empty, return quickly. Otherwise,
+  // we'll fetch both so that errors are consistent.
+  if (arg_refs_id == SemIR::NodeBlockId::Empty &&
+      param_refs_id == SemIR::NodeBlockId::Empty) {
+    return true;
+  }
+
+  auto arg_refs = context.semantics_ir().GetNodeBlock(arg_refs_id);
+  auto param_refs = context.semantics_ir().GetNodeBlock(param_refs_id);
+
+  if (has_return_slot) {
+    // There's no entry in the parameter block for the return slot, so ignore
+    // the corresponding entry in the argument block.
+    // TODO: Consider adding the return slot to the parameter list.
+    CARBON_CHECK(!arg_refs.empty()) << "missing return slot";
+    arg_refs = arg_refs.drop_back();
+  }
+
+  // If sizes mismatch, fail early.
+  if (arg_refs.size() != param_refs.size()) {
+    CARBON_DIAGNOSTIC(CallArgCountMismatch, Error,
+                      "{0} argument(s) passed to function expecting "
+                      "{1} argument(s).",
+                      int, int);
+    CARBON_DIAGNOSTIC(InCallToFunction, Note,
+                      "Calling function declared here.");
+    context.emitter()
+        .Build(call_parse_node, CallArgCountMismatch, arg_refs.size(),
+               param_refs.size())
+        .Note(param_parse_node, InCallToFunction)
+        .Emit();
+    return false;
+  }
+
+  if (param_refs.empty()) {
+    return true;
+  }
+
+  int diag_param_index;
+  DiagnosticAnnotationScope annotate_diagnostics(
+      &context.emitter(), [&](auto& builder) {
+        CARBON_DIAGNOSTIC(
+            InCallToFunctionParam, Note,
+            "Initializing parameter {0} of function declared here.", int);
+        builder.Note(param_parse_node, InCallToFunctionParam,
+                     diag_param_index + 1);
+      });
+
+  // Check type conversions per-element.
+  for (auto [i, value_id, param_ref] : llvm::enumerate(arg_refs, param_refs)) {
+    diag_param_index = i;
+
+    auto as_type_id = context.semantics_ir().GetNode(param_ref).type_id();
+    // TODO: Convert to the proper expression category. For now, we assume
+    // parameters are all `let` bindings.
+    value_id =
+        ConvertToValueOfType(context, call_parse_node, value_id, as_type_id);
+    if (value_id == SemIR::NodeId::BuiltinError) {
+      return false;
+    }
+    arg_refs[i] = value_id;
+  }
+
+  return true;
+}
+
+auto ExpressionAsType(Context& context, Parse::Node parse_node,
+                      SemIR::NodeId value_id) -> SemIR::TypeId {
+  return context.CanonicalizeType(ConvertToValueOfType(
+      context, parse_node, value_id, SemIR::TypeId::TypeType));
+}
+
+}  // namespace Carbon::Check

+ 93 - 0
toolchain/check/convert.h

@@ -0,0 +1,93 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef CARBON_TOOLCHAIN_CHECK_CONVERT_H_
+#define CARBON_TOOLCHAIN_CHECK_CONVERT_H_
+
+#include "toolchain/check/context.h"
+#include "toolchain/check/pending_block.h"
+#include "toolchain/parse/tree.h"
+#include "toolchain/sem_ir/node.h"
+
+namespace Carbon::Check {
+
+// Description of the target of a conversion.
+struct ConversionTarget {
+  enum Kind {
+    // Convert to a value of type `type`.
+    Value,
+    // Convert to either a value or a reference of type `type`.
+    ValueOrReference,
+    // The result of the conversion is discarded. It can't be an initializing
+    // expression, but can be anything else.
+    Discarded,
+    // Convert to an initializer for the object denoted by `init_id`.
+    Initializer,
+    // Convert to an initializer for the object denoted by `init_id`,
+    // including a final destination store if needed.
+    FullInitializer,
+    Last = FullInitializer
+  };
+  // The kind of the target for this conversion.
+  Kind kind;
+  // The target type for the conversion.
+  SemIR::TypeId type_id;
+  // For an initializer, the object being initialized.
+  SemIR::NodeId init_id = SemIR::NodeId::Invalid;
+  // For an initializer, a block of pending instructions that are needed to
+  // form the value of `target_id`, and that can be discarded if no
+  // initialization is needed.
+  PendingBlock* init_block = nullptr;
+
+  // Are we converting this value into an initializer for an object?
+  bool is_initializer() const {
+    return kind == Initializer || kind == FullInitializer;
+  }
+};
+
+// Convert a value to another type and expression category.
+auto Convert(Context& context, Parse::Node parse_node, SemIR::NodeId value_id,
+             ConversionTarget target) -> SemIR::NodeId;
+
+// Performs initialization of `target_id` from `value_id`. Returns the
+// possibly-converted initializing expression, which should be assigned to the
+// target using a suitable node for the kind of initialization.
+auto Initialize(Context& context, Parse::Node parse_node,
+                SemIR::NodeId target_id, SemIR::NodeId value_id)
+    -> SemIR::NodeId;
+
+// Convert the given expression to a value expression of the same type.
+auto ConvertToValueExpression(Context& context, SemIR::NodeId expr_id)
+    -> SemIR::NodeId;
+
+// Convert the given expression to a value or reference expression of the same
+// type.
+auto ConvertToValueOrReferenceExpression(Context& context,
+                                         SemIR::NodeId expr_id)
+    -> SemIR::NodeId;
+
+// Converts `value_id` to a value expression of type `type_id`.
+auto ConvertToValueOfType(Context& context, Parse::Node parse_node,
+                          SemIR::NodeId value_id, SemIR::TypeId type_id)
+    -> SemIR::NodeId;
+
+// Converts `value_id` to a value expression of type `bool`.
+auto ConvertToBoolValue(Context& context, Parse::Node parse_node,
+                        SemIR::NodeId value_id) -> SemIR::NodeId;
+
+// Implicitly converts a set of arguments to match the parameter types in a
+// function call.
+auto ConvertCallArgs(Context& context, Parse::Node call_parse_node,
+                     SemIR::NodeBlockId arg_refs_id,
+                     Parse::Node param_parse_node,
+                     SemIR::NodeBlockId param_refs_id, bool has_return_slot)
+    -> bool;
+
+// Converts an expression for use as a type.
+auto ExpressionAsType(Context& context, Parse::Node parse_node,
+                      SemIR::NodeId value_id) -> SemIR::TypeId;
+
+}  // namespace Carbon::Check
+
+#endif  // CARBON_TOOLCHAIN_CHECK_CONVERT_H_

+ 2 - 1
toolchain/check/handle_array.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/parse/node_kind.h"
 #include "toolchain/sem_ir/node.h"
 #include "toolchain/sem_ir/node_kind.h"
@@ -43,7 +44,7 @@ auto HandleArrayExpression(Context& context, Parse::Node parse_node) -> bool {
           parse_node,
           SemIR::Node::ArrayType::Make(
               parse_node, SemIR::TypeId::TypeType, bound_node_id,
-              context.ExpressionAsType(parse_node, element_type_node_id)));
+              ExpressionAsType(context, parse_node, element_type_node_id)));
       return true;
     }
   }

+ 4 - 3
toolchain/check/handle_call_expression.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 
 namespace Carbon::Check {
@@ -46,9 +47,9 @@ auto HandleCallExpression(Context& context, Parse::Node parse_node) -> bool {
 
   // Convert the arguments to match the parameters.
   auto refs_id = context.ParamOrArgPop();
-  if (!context.ImplicitAsForArgs(call_expr_parse_node, refs_id,
-                                 name_node.parse_node(), callable.param_refs_id,
-                                 callable.return_slot_id.is_valid())) {
+  if (!ConvertCallArgs(context, call_expr_parse_node, refs_id,
+                       name_node.parse_node(), callable.param_refs_id,
+                       callable.return_slot_id.is_valid())) {
     context.node_stack().Push(parse_node, SemIR::NodeId::BuiltinError);
     return true;
   }

+ 2 - 1
toolchain/check/handle_function.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 
 namespace Carbon::Check {
 
@@ -133,7 +134,7 @@ auto HandleReturnType(Context& context, Parse::Node parse_node) -> bool {
   // Propagate the type expression.
   auto [type_parse_node, type_node_id] =
       context.node_stack().PopExpressionWithParseNode();
-  auto type_id = context.ExpressionAsType(type_parse_node, type_node_id);
+  auto type_id = ExpressionAsType(context, type_parse_node, type_node_id);
   // TODO: Use a dedicated node rather than VarStorage here.
   context.AddNodeAndPush(
       parse_node,

+ 4 - 3
toolchain/check/handle_if_expression.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 
 namespace Carbon::Check {
 
@@ -13,7 +14,7 @@ auto HandleIfExpressionIf(Context& context, Parse::Node parse_node) -> bool {
   auto cond_value_id = context.node_stack().PopExpression();
 
   // Convert the condition to `bool`, and branch on it.
-  cond_value_id = context.ConvertToBoolValue(if_node, cond_value_id);
+  cond_value_id = ConvertToBoolValue(context, if_node, cond_value_id);
   auto then_block_id =
       context.AddDominatedBlockAndBranchIf(if_node, cond_value_id);
   auto else_block_id = context.AddDominatedBlockAndBranch(if_node);
@@ -33,7 +34,7 @@ auto HandleIfExpressionThen(Context& context, Parse::Node parse_node) -> bool {
       context.node_stack().Peek<Parse::NodeKind::IfExpressionIf>();
 
   // Convert the first operand to a value.
-  then_value_id = context.ConvertToValueExpression(then_value_id);
+  then_value_id = ConvertToValueExpression(context, then_value_id);
 
   // Start emitting the `else` block.
   context.node_block_stack().Push(else_block_id);
@@ -58,7 +59,7 @@ auto HandleIfExpressionElse(Context& context, Parse::Node parse_node) -> bool {
   // TODO: Find a common type, and convert both operands to it instead.
   auto result_type_id = context.semantics_ir().GetNode(then_value_id).type_id();
   else_value_id =
-      context.ConvertToValueOfType(else_node, else_value_id, result_type_id);
+      ConvertToValueOfType(context, else_node, else_value_id, result_type_id);
 
   // Create a resumption block and branches to it.
   auto chosen_value_id = context.AddConvergenceBlockWithArgAndPush(

+ 2 - 1
toolchain/check/handle_if_statement.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 
 namespace Carbon::Check {
@@ -15,7 +16,7 @@ auto HandleIfConditionStart(Context& /*context*/, Parse::Node /*parse_node*/)
 auto HandleIfCondition(Context& context, Parse::Node parse_node) -> bool {
   // Convert the condition to `bool`.
   auto cond_value_id = context.node_stack().PopExpression();
-  cond_value_id = context.ConvertToBoolValue(parse_node, cond_value_id);
+  cond_value_id = ConvertToBoolValue(context, parse_node, cond_value_id);
 
   // Create the then block and the else block, and branch to the right one. If
   // there is no `else`, the then block will terminate with a branch to the

+ 4 - 3
toolchain/check/handle_index.cpp

@@ -4,6 +4,7 @@
 
 #include "llvm/ADT/APSInt.h"
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 #include "toolchain/sem_ir/node_kind.h"
 
@@ -42,7 +43,7 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
   auto index_node = context.semantics_ir().GetNode(index_node_id);
   auto operand_node_id = context.node_stack().PopExpression();
   operand_node_id =
-      context.ConvertToValueOrReferenceExpression(operand_node_id);
+      ConvertToValueOrReferenceExpression(context, operand_node_id);
   auto operand_node = context.semantics_ir().GetNode(operand_node_id);
   auto operand_type_id = operand_node.type_id();
   auto operand_type_node = context.semantics_ir().GetNode(
@@ -59,8 +60,8 @@ auto HandleIndexExpression(Context& context, Parse::Node parse_node) -> bool {
               context.semantics_ir().GetArrayBoundValue(bound_id))) {
         index_node_id = SemIR::NodeId::BuiltinError;
       }
-      auto cast_index_id = context.ConvertToValueOfType(
-          index_node.parse_node(), index_node_id,
+      auto cast_index_id = ConvertToValueOfType(
+          context, index_node.parse_node(), index_node_id,
           context.CanonicalizeType(SemIR::NodeId::BuiltinIntegerType));
       context.AddNodeAndPush(parse_node, SemIR::Node::ArrayIndex::Make(
                                              parse_node, element_type_id,

+ 2 - 1
toolchain/check/handle_name.cpp

@@ -4,6 +4,7 @@
 
 #include "llvm/ADT/STLExtras.h"
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 
 namespace Carbon::Check {
@@ -25,7 +26,7 @@ auto HandleMemberAccessExpression(Context& context, Parse::Node parse_node)
   }
 
   // Materialize a temporary for the base expression if necessary.
-  base_id = context.ConvertToValueOrReferenceExpression(base_id);
+  base_id = ConvertToValueOrReferenceExpression(context, base_id);
 
   auto base_type = context.semantics_ir().GetNode(
       context.semantics_ir().GetTypeAllowBuiltinTypes(base.type_id()));

+ 12 - 10
toolchain/check/handle_operator.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 
 namespace Carbon::Check {
 
@@ -16,9 +17,10 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
     case Lex::TokenKind::Plus:
       // TODO: This should search for a compatible interface. For now, it's a
       // very trivial check of validity on the operation.
-      lhs_id = context.ConvertToValueOfType(
-          parse_node, lhs_id, context.semantics_ir().GetNode(rhs_id).type_id());
-      rhs_id = context.ConvertToValueExpression(rhs_id);
+      lhs_id = ConvertToValueOfType(
+          context, parse_node, lhs_id,
+          context.semantics_ir().GetNode(rhs_id).type_id());
+      rhs_id = ConvertToValueExpression(context, rhs_id);
 
       context.AddNodeAndPush(
           parse_node,
@@ -32,7 +34,7 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
       // The first operand is wrapped in a ShortCircuitOperand, which we
       // already handled by creating a RHS block and a resumption block, which
       // are the current block and its enclosing block.
-      rhs_id = context.ConvertToBoolValue(parse_node, rhs_id);
+      rhs_id = ConvertToBoolValue(context, parse_node, rhs_id);
 
       // When the second operand is evaluated, the result of `and` and `or` is
       // its value.
@@ -60,7 +62,7 @@ auto HandleInfixOperator(Context& context, Parse::Node parse_node) -> bool {
       }
       // TODO: Destroy the old value before reinitializing. This will require
       // building the destruction code before we build the RHS subexpression.
-      rhs_id = context.Initialize(parse_node, lhs_id, rhs_id);
+      rhs_id = Initialize(context, parse_node, lhs_id, rhs_id);
       context.AddNode(SemIR::Node::Assign::Make(parse_node, lhs_id, rhs_id));
       // We model assignment as an expression, so we need to push a value for
       // it, even though it doesn't produce a value.
@@ -81,7 +83,7 @@ auto HandlePostfixOperator(Context& context, Parse::Node parse_node) -> bool {
   auto token = context.parse_tree().node_token(parse_node);
   switch (auto token_kind = context.tokens().GetKind(token)) {
     case Lex::TokenKind::Star: {
-      auto inner_type_id = context.ExpressionAsType(parse_node, value_id);
+      auto inner_type_id = ExpressionAsType(context, parse_node, value_id);
       context.AddNodeAndPush(
           parse_node, SemIR::Node::PointerType::Make(
                           parse_node, SemIR::TypeId::TypeType, inner_type_id));
@@ -138,7 +140,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
                           "additional effect.");
         context.emitter().Emit(parse_node, RepeatedConst);
       }
-      auto inner_type_id = context.ExpressionAsType(parse_node, value_id);
+      auto inner_type_id = ExpressionAsType(context, parse_node, value_id);
       context.AddNodeAndPush(
           parse_node, SemIR::Node::ConstType::Make(
                           parse_node, SemIR::TypeId::TypeType, inner_type_id));
@@ -146,7 +148,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
     }
 
     case Lex::TokenKind::Not:
-      value_id = context.ConvertToBoolValue(parse_node, value_id);
+      value_id = ConvertToBoolValue(context, parse_node, value_id);
       context.AddNodeAndPush(
           parse_node,
           SemIR::Node::UnaryOperatorNot::Make(
@@ -179,7 +181,7 @@ auto HandlePrefixOperator(Context& context, Parse::Node parse_node) -> bool {
         }
         builder.Emit();
       }
-      value_id = context.ConvertToValueExpression(value_id);
+      value_id = ConvertToValueExpression(context, value_id);
       context.AddNodeAndPush(
           parse_node,
           SemIR::Node::Dereference::Make(parse_node, result_type_id, value_id));
@@ -195,7 +197,7 @@ auto HandleShortCircuitOperand(Context& context, Parse::Node parse_node)
     -> bool {
   // Convert the condition to `bool`.
   auto cond_value_id = context.node_stack().PopExpression();
-  cond_value_id = context.ConvertToBoolValue(parse_node, cond_value_id);
+  cond_value_id = ConvertToBoolValue(context, parse_node, cond_value_id);
   auto bool_type_id = context.semantics_ir().GetNode(cond_value_id).type_id();
 
   // Compute the branch value: the condition for `and`, inverted for `or`.

+ 2 - 1
toolchain/check/handle_pattern_binding.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 
 namespace Carbon::Check {
@@ -19,7 +20,7 @@ auto HandleGenericPatternBinding(Context& context, Parse::Node parse_node)
 auto HandlePatternBinding(Context& context, Parse::Node parse_node) -> bool {
   auto [type_node, parsed_type_id] =
       context.node_stack().PopExpressionWithParseNode();
-  auto cast_type_id = context.ExpressionAsType(type_node, parsed_type_id);
+  auto cast_type_id = ExpressionAsType(context, type_node, parsed_type_id);
 
   // Get the name.
   auto [name_node, name_id] =

+ 18 - 4
toolchain/check/handle_statement.cpp

@@ -3,13 +3,27 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 
 namespace Carbon::Check {
 
+// TODO: Find a better home for this. We'll likely need it for more than just
+// expression statements.
+static auto HandleDiscardedExpression(Context& context, SemIR::NodeId expr_id)
+    -> void {
+  // If we discard an initializing expression, convert it to a value or
+  // reference so that it has something to initialize.
+  auto expr = context.semantics_ir().GetNode(expr_id);
+  Convert(context, expr.parse_node(), expr_id,
+          {.kind = ConversionTarget::Discarded, .type_id = expr.type_id()});
+
+  // TODO: This will eventually need to do some "do not discard" analysis.
+}
+
 auto HandleExpressionStatement(Context& context, Parse::Node /*parse_node*/)
     -> bool {
-  context.HandleDiscardedExpression(context.node_stack().PopExpression());
+  HandleDiscardedExpression(context, context.node_stack().PopExpression());
   return true;
 }
 
@@ -52,10 +66,10 @@ auto HandleReturnStatement(Context& context, Parse::Node parse_node) -> bool {
           .Note(fn_node.parse_node(), ReturnStatementImplicitNote)
           .Emit();
     } else if (callable.return_slot_id.is_valid()) {
-      arg = context.Initialize(parse_node, callable.return_slot_id, arg);
+      arg = Initialize(context, parse_node, callable.return_slot_id, arg);
     } else {
-      arg = context.ConvertToValueOfType(parse_node, arg,
-                                         callable.return_type_id);
+      arg = ConvertToValueOfType(context, parse_node, arg,
+                                 callable.return_type_id);
     }
 
     context.AddNode(SemIR::Node::ReturnExpression::Make(parse_node, arg));

+ 2 - 1
toolchain/check/handle_struct.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 
 namespace Carbon::Check {
 
@@ -22,7 +23,7 @@ auto HandleStructFieldDesignator(Context& context, Parse::Node /*parse_node*/)
 
 auto HandleStructFieldType(Context& context, Parse::Node parse_node) -> bool {
   auto [type_node, type_id] = context.node_stack().PopExpressionWithParseNode();
-  SemIR::TypeId cast_type_id = context.ExpressionAsType(type_node, type_id);
+  SemIR::TypeId cast_type_id = ExpressionAsType(context, type_node, type_id);
 
   auto [name_node, name_id] =
       context.node_stack().PopWithParseNode<Parse::NodeKind::Name>();

+ 2 - 1
toolchain/check/handle_variable.cpp

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
 #include "toolchain/check/context.h"
+#include "toolchain/check/convert.h"
 #include "toolchain/sem_ir/node.h"
 
 namespace Carbon::Check {
@@ -28,7 +29,7 @@ auto HandleVariableDeclaration(Context& context, Parse::Node parse_node)
   context.AddNameToLookup(var.parse_node(), name_id, var_id);
   // If there was an initializer, assign it to storage.
   if (has_init) {
-    init_id = context.Initialize(parse_node, var_id, init_id);
+    init_id = Initialize(context, parse_node, var_id, init_id);
     // TODO: Consider using different node kinds for assignment versus
     // initialization.
     context.AddNode(SemIR::Node::Assign::Make(parse_node, var_id, init_id));

+ 94 - 0
toolchain/check/pending_block.h

@@ -0,0 +1,94 @@
+// 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
+
+#ifndef CARBON_TOOLCHAIN_CHECK_PENDING_BLOCK_H_
+#define CARBON_TOOLCHAIN_CHECK_PENDING_BLOCK_H_
+
+#include "llvm/ADT/SmallVector.h"
+#include "toolchain/check/context.h"
+
+namespace Carbon::Check {
+
+// A block of code that contains pending instructions that might be needed but
+// that haven't been inserted yet.
+class PendingBlock {
+ public:
+  PendingBlock(Context& context) : context_(context) {}
+
+  PendingBlock(const PendingBlock&) = delete;
+  PendingBlock& operator=(const PendingBlock&) = delete;
+
+  // A scope in which we will tentatively add nodes to a pending block. If we
+  // leave the scope without inserting or merging the block, nodes added after
+  // this point will be removed again.
+  class DiscardUnusedNodesScope {
+   public:
+    // If `block` is not null, enters the scope. If `block` is null, this object
+    // has no effect.
+    DiscardUnusedNodesScope(PendingBlock* block)
+        : block_(block), size_(block ? block->nodes_.size() : 0) {}
+    ~DiscardUnusedNodesScope() {
+      if (block_ && block_->nodes_.size() > size_) {
+        block_->nodes_.truncate(size_);
+      }
+    }
+
+   private:
+    PendingBlock* block_;
+    size_t size_;
+  };
+
+  auto AddNode(SemIR::Node node) -> SemIR::NodeId {
+    auto node_id = context_.semantics_ir().AddNodeInNoBlock(node);
+    nodes_.push_back(node_id);
+    return node_id;
+  }
+
+  // Insert the pending block of code at the current position.
+  auto InsertHere() -> void {
+    for (auto id : nodes_) {
+      context_.node_block_stack().AddNodeId(id);
+    }
+    nodes_.clear();
+  }
+
+  // Replace the node at target_id with the nodes in this block. The new value
+  // for target_id should be value_id.
+  auto MergeReplacing(SemIR::NodeId target_id, SemIR::NodeId value_id) -> void {
+    auto value = context_.semantics_ir().GetNode(value_id);
+
+    // There are three cases here:
+
+    if (nodes_.empty()) {
+      // 1) The block is empty. Replace `target_id` with an empty splice
+      // pointing at `value_id`.
+      context_.semantics_ir().ReplaceNode(
+          target_id,
+          SemIR::Node::SpliceBlock::Make(value.parse_node(), value.type_id(),
+                                         SemIR::NodeBlockId::Empty, value_id));
+    } else if (nodes_.size() == 1 && nodes_[0] == value_id) {
+      // 2) The block is {value_id}. Replace `target_id` with the node referred
+      // to by `value_id`. This is intended to be the common case.
+      context_.semantics_ir().ReplaceNode(target_id, value);
+    } else {
+      // 3) Anything else: splice it into the IR, replacing `target_id`.
+      context_.semantics_ir().ReplaceNode(
+          target_id,
+          SemIR::Node::SpliceBlock::Make(
+              value.parse_node(), value.type_id(),
+              context_.semantics_ir().AddNodeBlock(nodes_), value_id));
+    }
+
+    // Prepare to stash more pending instructions.
+    nodes_.clear();
+  }
+
+ private:
+  Context& context_;
+  llvm::SmallVector<SemIR::NodeId> nodes_;
+};
+
+}  // namespace Carbon::Check
+
+#endif  // CARBON_TOOLCHAIN_CHECK_PENDING_BLOCK_H_

+ 3 - 4
toolchain/check/testdata/array/array_in_place.carbon

@@ -24,19 +24,18 @@ fn G() {
 // CHECK:STDOUT:   %.loc10_29: type = array_type %.loc10_28, (i32, i32, i32)
 // CHECK:STDOUT:   %v: ref [(i32, i32, i32); 2] = var "v"
 // CHECK:STDOUT:   %.loc10_42.3: ref (i32, i32, i32) = splice_block %.loc10_42.2 {
-// CHECK:STDOUT:     %.loc10_7: ref [(i32, i32, i32); 2] = splice_block %v {}
 // CHECK:STDOUT:     %.loc10_42.1: i32 = int_literal 0
-// CHECK:STDOUT:     %.loc10_42.2: ref (i32, i32, i32) = array_index %.loc10_7, %.loc10_42.1
+// CHECK:STDOUT:     %.loc10_42.2: ref (i32, i32, i32) = array_index %v, %.loc10_42.1
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %.loc10_35: init (i32, i32, i32) = call @F() to %.loc10_42.3
 // CHECK:STDOUT:   %.loc10_42.6: ref (i32, i32, i32) = splice_block %.loc10_42.5 {
 // CHECK:STDOUT:     %.loc10_42.4: i32 = int_literal 1
-// CHECK:STDOUT:     %.loc10_42.5: ref (i32, i32, i32) = array_index %.loc10_7, %.loc10_42.4
+// CHECK:STDOUT:     %.loc10_42.5: ref (i32, i32, i32) = array_index %v, %.loc10_42.4
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %.loc10_40: init (i32, i32, i32) = call @F() to %.loc10_42.6
 // CHECK:STDOUT:   %.loc10_42.7: type = tuple_type ((i32, i32, i32), (i32, i32, i32))
 // CHECK:STDOUT:   %.loc10_42.8: ((i32, i32, i32), (i32, i32, i32)) = tuple_literal (%.loc10_35, %.loc10_40)
-// CHECK:STDOUT:   %.loc10_42.9: init [(i32, i32, i32); 2] = array_init %.loc10_42.8, (%.loc10_35, %.loc10_40) to %.loc10_7
+// CHECK:STDOUT:   %.loc10_42.9: init [(i32, i32, i32); 2] = array_init %.loc10_42.8, (%.loc10_35, %.loc10_40) to %v
 // CHECK:STDOUT:   assign %v, %.loc10_42.9
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 2 - 3
toolchain/check/testdata/array/assign_return_value.carbon

@@ -33,11 +33,10 @@ fn Run() {
 // CHECK:STDOUT:   %.loc10_22.3: ref (i32,) = temporary %.loc10_22.2, %.loc10_22.1
 // CHECK:STDOUT:   %.loc10_22.4: ref i32 = tuple_access %.loc10_22.3, member0
 // CHECK:STDOUT:   %.loc10_22.5: i32 = bind_value %.loc10_22.4
-// CHECK:STDOUT:   %.loc10_7: ref [i32; 1] = splice_block %t {}
 // CHECK:STDOUT:   %.loc10_22.6: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc10_22.7: ref i32 = array_index %.loc10_7, %.loc10_22.6
+// CHECK:STDOUT:   %.loc10_22.7: ref i32 = array_index %t, %.loc10_22.6
 // CHECK:STDOUT:   %.loc10_22.8: init i32 = initialize_from %.loc10_22.5 to %.loc10_22.7
-// CHECK:STDOUT:   %.loc10_22.9: init [i32; 1] = array_init %.loc10_22.3, (%.loc10_22.8) to %.loc10_7
+// CHECK:STDOUT:   %.loc10_22.9: init [i32; 1] = array_init %.loc10_22.3, (%.loc10_22.8) to %t
 // CHECK:STDOUT:   assign %t, %.loc10_22.9
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 13 - 14
toolchain/check/testdata/array/assign_var.carbon

@@ -15,34 +15,33 @@ var b: [i32; 3] = a;
 // CHECK:STDOUT:   %.loc7_27: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc7_30: i32 = int_literal 2
 // CHECK:STDOUT:   %.loc7_33: i32 = int_literal 3
-// CHECK:STDOUT:   %.loc7_34: (i32, i32, i32) = tuple_literal (%.loc7_27, %.loc7_30, %.loc7_33)
-// CHECK:STDOUT:   %.loc7_35.1: ref i32 = tuple_access %a, member0
-// CHECK:STDOUT:   %.loc7_35.2: init i32 = initialize_from %.loc7_27 to %.loc7_35.1
-// CHECK:STDOUT:   %.loc7_35.3: ref i32 = tuple_access %a, member1
-// CHECK:STDOUT:   %.loc7_35.4: init i32 = initialize_from %.loc7_30 to %.loc7_35.3
-// CHECK:STDOUT:   %.loc7_35.5: ref i32 = tuple_access %a, member2
-// CHECK:STDOUT:   %.loc7_35.6: init i32 = initialize_from %.loc7_33 to %.loc7_35.5
-// CHECK:STDOUT:   %.loc7_35.7: init (i32, i32, i32) = tuple_init %.loc7_34, (%.loc7_35.2, %.loc7_35.4, %.loc7_35.6)
-// CHECK:STDOUT:   assign %a, %.loc7_35.7
+// CHECK:STDOUT:   %.loc7_34.1: (i32, i32, i32) = tuple_literal (%.loc7_27, %.loc7_30, %.loc7_33)
+// CHECK:STDOUT:   %.loc7_34.2: ref i32 = tuple_access %a, member0
+// CHECK:STDOUT:   %.loc7_34.3: init i32 = initialize_from %.loc7_27 to %.loc7_34.2
+// CHECK:STDOUT:   %.loc7_34.4: ref i32 = tuple_access %a, member1
+// CHECK:STDOUT:   %.loc7_34.5: init i32 = initialize_from %.loc7_30 to %.loc7_34.4
+// CHECK:STDOUT:   %.loc7_34.6: ref i32 = tuple_access %a, member2
+// CHECK:STDOUT:   %.loc7_34.7: init i32 = initialize_from %.loc7_33 to %.loc7_34.6
+// CHECK:STDOUT:   %.loc7_34.8: init (i32, i32, i32) = tuple_init %.loc7_34.1, (%.loc7_34.3, %.loc7_34.5, %.loc7_34.7)
+// CHECK:STDOUT:   assign %a, %.loc7_34.8
 // CHECK:STDOUT:   %.loc8_14: i32 = int_literal 3
 // CHECK:STDOUT:   %.loc8_15: type = array_type %.loc8_14, i32
 // CHECK:STDOUT:   %b: ref [i32; 3] = var "b"
 // CHECK:STDOUT:   %.loc7_5.1: ref i32 = tuple_access %a, member0
 // CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
-// CHECK:STDOUT:   %.loc8_5: ref [i32; 3] = splice_block %b {}
 // CHECK:STDOUT:   %.loc7_5.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_5.4: ref i32 = array_index %.loc8_5, %.loc7_5.3
+// CHECK:STDOUT:   %.loc7_5.4: ref i32 = array_index %b, %.loc7_5.3
 // CHECK:STDOUT:   %.loc7_5.5: init i32 = initialize_from %.loc7_5.2 to %.loc7_5.4
 // CHECK:STDOUT:   %.loc7_5.6: ref i32 = tuple_access %a, member1
 // CHECK:STDOUT:   %.loc7_5.7: i32 = bind_value %.loc7_5.6
 // CHECK:STDOUT:   %.loc7_5.8: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc7_5.9: ref i32 = array_index %.loc8_5, %.loc7_5.8
+// CHECK:STDOUT:   %.loc7_5.9: ref i32 = array_index %b, %.loc7_5.8
 // CHECK:STDOUT:   %.loc7_5.10: init i32 = initialize_from %.loc7_5.7 to %.loc7_5.9
 // CHECK:STDOUT:   %.loc7_5.11: ref i32 = tuple_access %a, member2
 // CHECK:STDOUT:   %.loc7_5.12: i32 = bind_value %.loc7_5.11
 // CHECK:STDOUT:   %.loc7_5.13: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc7_5.14: ref i32 = array_index %.loc8_5, %.loc7_5.13
+// CHECK:STDOUT:   %.loc7_5.14: ref i32 = array_index %b, %.loc7_5.13
 // CHECK:STDOUT:   %.loc7_5.15: init i32 = initialize_from %.loc7_5.12 to %.loc7_5.14
-// CHECK:STDOUT:   %.loc7_5.16: init [i32; 3] = array_init %a, (%.loc7_5.5, %.loc7_5.10, %.loc7_5.15) to %.loc8_5
+// CHECK:STDOUT:   %.loc7_5.16: init [i32; 3] = array_init %a, (%.loc7_5.5, %.loc7_5.10, %.loc7_5.15) to %b
 // CHECK:STDOUT:   assign %b, %.loc7_5.16
 // CHECK:STDOUT: }

+ 18 - 21
toolchain/check/testdata/array/base.carbon

@@ -15,11 +15,10 @@ var c: [(); 5] = ((), (), (), (), (),);
 // CHECK:STDOUT:   %.loc7_20: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc7_22.1: type = tuple_type (i32)
 // CHECK:STDOUT:   %.loc7_22.2: (i32,) = tuple_literal (%.loc7_20)
-// CHECK:STDOUT:   %.loc7_5: ref [i32; 1] = splice_block %a {}
 // CHECK:STDOUT:   %.loc7_22.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_22.4: ref i32 = array_index %.loc7_5, %.loc7_22.3
+// CHECK:STDOUT:   %.loc7_22.4: ref i32 = array_index %a, %.loc7_22.3
 // CHECK:STDOUT:   %.loc7_22.5: init i32 = initialize_from %.loc7_20 to %.loc7_22.4
-// CHECK:STDOUT:   %.loc7_22.6: init [i32; 1] = array_init %.loc7_22.2, (%.loc7_22.5) to %.loc7_5
+// CHECK:STDOUT:   %.loc7_22.6: init [i32; 1] = array_init %.loc7_22.2, (%.loc7_22.5) to %a
 // CHECK:STDOUT:   assign %a, %.loc7_22.6
 // CHECK:STDOUT:   %.loc8_14: i32 = int_literal 2
 // CHECK:STDOUT:   %.loc8_15: type = array_type %.loc8_14, f64
@@ -28,33 +27,31 @@ var c: [(); 5] = ((), (), (), (), (),);
 // CHECK:STDOUT:   %.loc8_26: f64 = real_literal 22e-1
 // CHECK:STDOUT:   %.loc8_30.1: type = tuple_type (f64, f64)
 // CHECK:STDOUT:   %.loc8_30.2: (f64, f64) = tuple_literal (%.loc8_20, %.loc8_26)
-// CHECK:STDOUT:   %.loc8_5: ref [f64; 2] = splice_block %b {}
 // CHECK:STDOUT:   %.loc8_30.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc8_30.4: ref f64 = array_index %.loc8_5, %.loc8_30.3
+// CHECK:STDOUT:   %.loc8_30.4: ref f64 = array_index %b, %.loc8_30.3
 // CHECK:STDOUT:   %.loc8_30.5: init f64 = initialize_from %.loc8_20 to %.loc8_30.4
 // CHECK:STDOUT:   %.loc8_30.6: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc8_30.7: ref f64 = array_index %.loc8_5, %.loc8_30.6
+// CHECK:STDOUT:   %.loc8_30.7: ref f64 = array_index %b, %.loc8_30.6
 // CHECK:STDOUT:   %.loc8_30.8: init f64 = initialize_from %.loc8_26 to %.loc8_30.7
-// CHECK:STDOUT:   %.loc8_30.9: init [f64; 2] = array_init %.loc8_30.2, (%.loc8_30.5, %.loc8_30.8) to %.loc8_5
+// CHECK:STDOUT:   %.loc8_30.9: init [f64; 2] = array_init %.loc8_30.2, (%.loc8_30.5, %.loc8_30.8) to %b
 // CHECK:STDOUT:   assign %b, %.loc8_30.9
 // CHECK:STDOUT:   %.loc9_10.1: type = tuple_type ()
 // CHECK:STDOUT:   %.loc9_10.2: () = tuple_literal ()
 // CHECK:STDOUT:   %.loc9_13: i32 = int_literal 5
 // CHECK:STDOUT:   %.loc9_14: type = array_type %.loc9_13, ()
 // CHECK:STDOUT:   %c: ref [(); 5] = var "c"
-// CHECK:STDOUT:   %.loc9_20: () = tuple_literal ()
-// CHECK:STDOUT:   %.loc9_24: () = tuple_literal ()
-// CHECK:STDOUT:   %.loc9_28: () = tuple_literal ()
-// CHECK:STDOUT:   %.loc9_32: () = tuple_literal ()
-// CHECK:STDOUT:   %.loc9_36: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc9_20.1: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc9_24.1: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc9_28.1: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc9_32.1: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc9_36.1: () = tuple_literal ()
 // CHECK:STDOUT:   %.loc9_38.1: type = tuple_type ((), (), (), (), ())
-// CHECK:STDOUT:   %.loc9_38.2: ((), (), (), (), ()) = tuple_literal (%.loc9_20, %.loc9_24, %.loc9_28, %.loc9_32, %.loc9_36)
-// CHECK:STDOUT:   %.loc9_38.3: init () = tuple_init %.loc9_20, ()
-// CHECK:STDOUT:   %.loc9_38.4: init () = tuple_init %.loc9_24, ()
-// CHECK:STDOUT:   %.loc9_38.5: init () = tuple_init %.loc9_28, ()
-// CHECK:STDOUT:   %.loc9_38.6: init () = tuple_init %.loc9_32, ()
-// CHECK:STDOUT:   %.loc9_38.7: init () = tuple_init %.loc9_36, ()
-// CHECK:STDOUT:   %.loc9_5: ref [(); 5] = splice_block %c {}
-// CHECK:STDOUT:   %.loc9_38.8: init [(); 5] = array_init %.loc9_38.2, (%.loc9_38.3, %.loc9_38.4, %.loc9_38.5, %.loc9_38.6, %.loc9_38.7) to %.loc9_5
-// CHECK:STDOUT:   assign %c, %.loc9_38.8
+// CHECK:STDOUT:   %.loc9_38.2: ((), (), (), (), ()) = tuple_literal (%.loc9_20.1, %.loc9_24.1, %.loc9_28.1, %.loc9_32.1, %.loc9_36.1)
+// CHECK:STDOUT:   %.loc9_20.2: init () = tuple_init %.loc9_20.1, ()
+// CHECK:STDOUT:   %.loc9_24.2: init () = tuple_init %.loc9_24.1, ()
+// CHECK:STDOUT:   %.loc9_28.2: init () = tuple_init %.loc9_28.1, ()
+// CHECK:STDOUT:   %.loc9_32.2: init () = tuple_init %.loc9_32.1, ()
+// CHECK:STDOUT:   %.loc9_36.2: init () = tuple_init %.loc9_36.1, ()
+// CHECK:STDOUT:   %.loc9_38.3: init [(); 5] = array_init %.loc9_38.2, (%.loc9_20.2, %.loc9_24.2, %.loc9_28.2, %.loc9_32.2, %.loc9_36.2) to %c
+// CHECK:STDOUT:   assign %c, %.loc9_38.3
 // CHECK:STDOUT: }

+ 7 - 9
toolchain/check/testdata/array/fail_type_mismatch.carbon

@@ -35,10 +35,9 @@ var d: [i32; 3] = t2;
 // CHECK:STDOUT:   %.loc10_32: String = string_literal "World"
 // CHECK:STDOUT:   %.loc10_39.1: type = tuple_type (i32, String, String)
 // CHECK:STDOUT:   %.loc10_39.2: (i32, String, String) = tuple_literal (%.loc10_20, %.loc10_23, %.loc10_32)
-// CHECK:STDOUT:   %.loc10_39.3: ref [i32; 3] = temporary_storage
-// CHECK:STDOUT:   %.loc10_39.4: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc10_39.5: ref i32 = array_index %.loc10_39.3, %.loc10_39.4
-// CHECK:STDOUT:   %.loc10_39.6: init i32 = initialize_from %.loc10_20 to %.loc10_39.5
+// CHECK:STDOUT:   %.loc10_39.3: i32 = int_literal 0
+// CHECK:STDOUT:   %.loc10_39.4: ref i32 = array_index %a, %.loc10_39.3
+// CHECK:STDOUT:   %.loc10_39.5: init i32 = initialize_from %.loc10_20 to %.loc10_39.4
 // CHECK:STDOUT:   assign %a, <error>
 // CHECK:STDOUT:   %.loc15_29.1: type = tuple_type (type, type, type)
 // CHECK:STDOUT:   %.loc15_29.2: (type, type, type) = tuple_literal (i32, String, String)
@@ -48,11 +47,10 @@ var d: [i32; 3] = t2;
 // CHECK:STDOUT:   %b: ref [i32; 3] = var "b"
 // CHECK:STDOUT:   %.loc15_5.1: ref i32 = tuple_access %t1, member0
 // CHECK:STDOUT:   %.loc15_5.2: i32 = bind_value %.loc15_5.1
-// CHECK:STDOUT:   %.loc15_5.3: ref [i32; 3] = temporary_storage
-// CHECK:STDOUT:   %.loc15_5.4: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc15_5.5: ref i32 = array_index %.loc15_5.3, %.loc15_5.4
-// CHECK:STDOUT:   %.loc15_5.6: init i32 = initialize_from %.loc15_5.2 to %.loc15_5.5
-// CHECK:STDOUT:   %.loc15_5.7: ref String = tuple_access %t1, member1
+// CHECK:STDOUT:   %.loc15_5.3: i32 = int_literal 0
+// CHECK:STDOUT:   %.loc15_5.4: ref i32 = array_index %b, %.loc15_5.3
+// CHECK:STDOUT:   %.loc15_5.5: init i32 = initialize_from %.loc15_5.2 to %.loc15_5.4
+// CHECK:STDOUT:   %.loc15_5.6: ref String = tuple_access %t1, member1
 // CHECK:STDOUT:   assign %b, <error>
 // CHECK:STDOUT:   %.loc21_14: i32 = int_literal 3
 // CHECK:STDOUT:   %.loc21_15: type = array_type %.loc21_14, i32

+ 10 - 11
toolchain/check/testdata/array/nine_elements.carbon

@@ -21,34 +21,33 @@ var a: [i32; 9] = (1, 2, 3, 4, 5, 6, 7, 8, 9);
 // CHECK:STDOUT:   %.loc7_44: i32 = int_literal 9
 // CHECK:STDOUT:   %.loc7_45.1: type = tuple_type (i32, i32, i32, i32, i32, i32, i32, i32, i32)
 // CHECK:STDOUT:   %.loc7_45.2: (i32, i32, i32, i32, i32, i32, i32, i32, i32) = tuple_literal (%.loc7_20, %.loc7_23, %.loc7_26, %.loc7_29, %.loc7_32, %.loc7_35, %.loc7_38, %.loc7_41, %.loc7_44)
-// CHECK:STDOUT:   %.loc7_5: ref [i32; 9] = splice_block %a {}
 // CHECK:STDOUT:   %.loc7_45.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_45.4: ref i32 = array_index %.loc7_5, %.loc7_45.3
+// CHECK:STDOUT:   %.loc7_45.4: ref i32 = array_index %a, %.loc7_45.3
 // CHECK:STDOUT:   %.loc7_45.5: init i32 = initialize_from %.loc7_20 to %.loc7_45.4
 // CHECK:STDOUT:   %.loc7_45.6: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc7_45.7: ref i32 = array_index %.loc7_5, %.loc7_45.6
+// CHECK:STDOUT:   %.loc7_45.7: ref i32 = array_index %a, %.loc7_45.6
 // CHECK:STDOUT:   %.loc7_45.8: init i32 = initialize_from %.loc7_23 to %.loc7_45.7
 // CHECK:STDOUT:   %.loc7_45.9: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc7_45.10: ref i32 = array_index %.loc7_5, %.loc7_45.9
+// CHECK:STDOUT:   %.loc7_45.10: ref i32 = array_index %a, %.loc7_45.9
 // CHECK:STDOUT:   %.loc7_45.11: init i32 = initialize_from %.loc7_26 to %.loc7_45.10
 // CHECK:STDOUT:   %.loc7_45.12: i32 = int_literal 3
-// CHECK:STDOUT:   %.loc7_45.13: ref i32 = array_index %.loc7_5, %.loc7_45.12
+// CHECK:STDOUT:   %.loc7_45.13: ref i32 = array_index %a, %.loc7_45.12
 // CHECK:STDOUT:   %.loc7_45.14: init i32 = initialize_from %.loc7_29 to %.loc7_45.13
 // CHECK:STDOUT:   %.loc7_45.15: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc7_45.16: ref i32 = array_index %.loc7_5, %.loc7_45.15
+// CHECK:STDOUT:   %.loc7_45.16: ref i32 = array_index %a, %.loc7_45.15
 // CHECK:STDOUT:   %.loc7_45.17: init i32 = initialize_from %.loc7_32 to %.loc7_45.16
 // CHECK:STDOUT:   %.loc7_45.18: i32 = int_literal 5
-// CHECK:STDOUT:   %.loc7_45.19: ref i32 = array_index %.loc7_5, %.loc7_45.18
+// CHECK:STDOUT:   %.loc7_45.19: ref i32 = array_index %a, %.loc7_45.18
 // CHECK:STDOUT:   %.loc7_45.20: init i32 = initialize_from %.loc7_35 to %.loc7_45.19
 // CHECK:STDOUT:   %.loc7_45.21: i32 = int_literal 6
-// CHECK:STDOUT:   %.loc7_45.22: ref i32 = array_index %.loc7_5, %.loc7_45.21
+// CHECK:STDOUT:   %.loc7_45.22: ref i32 = array_index %a, %.loc7_45.21
 // CHECK:STDOUT:   %.loc7_45.23: init i32 = initialize_from %.loc7_38 to %.loc7_45.22
 // CHECK:STDOUT:   %.loc7_45.24: i32 = int_literal 7
-// CHECK:STDOUT:   %.loc7_45.25: ref i32 = array_index %.loc7_5, %.loc7_45.24
+// CHECK:STDOUT:   %.loc7_45.25: ref i32 = array_index %a, %.loc7_45.24
 // CHECK:STDOUT:   %.loc7_45.26: init i32 = initialize_from %.loc7_41 to %.loc7_45.25
 // CHECK:STDOUT:   %.loc7_45.27: i32 = int_literal 8
-// CHECK:STDOUT:   %.loc7_45.28: ref i32 = array_index %.loc7_5, %.loc7_45.27
+// CHECK:STDOUT:   %.loc7_45.28: ref i32 = array_index %a, %.loc7_45.27
 // CHECK:STDOUT:   %.loc7_45.29: init i32 = initialize_from %.loc7_44 to %.loc7_45.28
-// CHECK:STDOUT:   %.loc7_45.30: init [i32; 9] = array_init %.loc7_45.2, (%.loc7_45.5, %.loc7_45.8, %.loc7_45.11, %.loc7_45.14, %.loc7_45.17, %.loc7_45.20, %.loc7_45.23, %.loc7_45.26, %.loc7_45.29) to %.loc7_5
+// CHECK:STDOUT:   %.loc7_45.30: init [i32; 9] = array_init %.loc7_45.2, (%.loc7_45.5, %.loc7_45.8, %.loc7_45.11, %.loc7_45.14, %.loc7_45.17, %.loc7_45.20, %.loc7_45.23, %.loc7_45.26, %.loc7_45.29) to %a
 // CHECK:STDOUT:   assign %a, %.loc7_45.30
 // CHECK:STDOUT: }

+ 14 - 16
toolchain/check/testdata/basics/numeric_literals.carbon

@@ -41,23 +41,22 @@ fn F() {
 // CHECK:STDOUT:   %.loc15: i32 = int_literal 39999999999999999993
 // CHECK:STDOUT:   %.loc16_3.1: type = tuple_type (i32, i32, i32, i32, i32)
 // CHECK:STDOUT:   %.loc16_3.2: (i32, i32, i32, i32, i32) = tuple_literal (%.loc11, %.loc12, %.loc13, %.loc14, %.loc15)
-// CHECK:STDOUT:   %.loc10_7: ref [i32; 5] = splice_block %ints {}
 // CHECK:STDOUT:   %.loc16_3.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc16_3.4: ref i32 = array_index %.loc10_7, %.loc16_3.3
+// CHECK:STDOUT:   %.loc16_3.4: ref i32 = array_index %ints, %.loc16_3.3
 // CHECK:STDOUT:   %.loc16_3.5: init i32 = initialize_from %.loc11 to %.loc16_3.4
 // CHECK:STDOUT:   %.loc16_3.6: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc16_3.7: ref i32 = array_index %.loc10_7, %.loc16_3.6
+// CHECK:STDOUT:   %.loc16_3.7: ref i32 = array_index %ints, %.loc16_3.6
 // CHECK:STDOUT:   %.loc16_3.8: init i32 = initialize_from %.loc12 to %.loc16_3.7
 // CHECK:STDOUT:   %.loc16_3.9: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc16_3.10: ref i32 = array_index %.loc10_7, %.loc16_3.9
+// CHECK:STDOUT:   %.loc16_3.10: ref i32 = array_index %ints, %.loc16_3.9
 // CHECK:STDOUT:   %.loc16_3.11: init i32 = initialize_from %.loc13 to %.loc16_3.10
 // CHECK:STDOUT:   %.loc16_3.12: i32 = int_literal 3
-// CHECK:STDOUT:   %.loc16_3.13: ref i32 = array_index %.loc10_7, %.loc16_3.12
+// CHECK:STDOUT:   %.loc16_3.13: ref i32 = array_index %ints, %.loc16_3.12
 // CHECK:STDOUT:   %.loc16_3.14: init i32 = initialize_from %.loc14 to %.loc16_3.13
 // CHECK:STDOUT:   %.loc16_3.15: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc16_3.16: ref i32 = array_index %.loc10_7, %.loc16_3.15
+// CHECK:STDOUT:   %.loc16_3.16: ref i32 = array_index %ints, %.loc16_3.15
 // CHECK:STDOUT:   %.loc16_3.17: init i32 = initialize_from %.loc15 to %.loc16_3.16
-// CHECK:STDOUT:   %.loc16_3.18: init [i32; 5] = array_init %.loc16_3.2, (%.loc16_3.5, %.loc16_3.8, %.loc16_3.11, %.loc16_3.14, %.loc16_3.17) to %.loc10_7
+// CHECK:STDOUT:   %.loc16_3.18: init [i32; 5] = array_init %.loc16_3.2, (%.loc16_3.5, %.loc16_3.8, %.loc16_3.11, %.loc16_3.14, %.loc16_3.17) to %ints
 // CHECK:STDOUT:   assign %ints, %.loc16_3.18
 // CHECK:STDOUT:   %.loc17_21: i32 = int_literal 7
 // CHECK:STDOUT:   %.loc17_22: type = array_type %.loc17_21, f64
@@ -71,29 +70,28 @@ fn F() {
 // CHECK:STDOUT:   %.loc24: f64 = real_literal 399999999999999999930e39999999999999999992
 // CHECK:STDOUT:   %.loc25_3.1: type = tuple_type (f64, f64, f64, f64, f64, f64, f64)
 // CHECK:STDOUT:   %.loc25_3.2: (f64, f64, f64, f64, f64, f64, f64) = tuple_literal (%.loc18, %.loc19, %.loc20, %.loc21, %.loc22, %.loc23, %.loc24)
-// CHECK:STDOUT:   %.loc17_7: ref [f64; 7] = splice_block %floats {}
 // CHECK:STDOUT:   %.loc25_3.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc25_3.4: ref f64 = array_index %.loc17_7, %.loc25_3.3
+// CHECK:STDOUT:   %.loc25_3.4: ref f64 = array_index %floats, %.loc25_3.3
 // CHECK:STDOUT:   %.loc25_3.5: init f64 = initialize_from %.loc18 to %.loc25_3.4
 // CHECK:STDOUT:   %.loc25_3.6: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc25_3.7: ref f64 = array_index %.loc17_7, %.loc25_3.6
+// CHECK:STDOUT:   %.loc25_3.7: ref f64 = array_index %floats, %.loc25_3.6
 // CHECK:STDOUT:   %.loc25_3.8: init f64 = initialize_from %.loc19 to %.loc25_3.7
 // CHECK:STDOUT:   %.loc25_3.9: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc25_3.10: ref f64 = array_index %.loc17_7, %.loc25_3.9
+// CHECK:STDOUT:   %.loc25_3.10: ref f64 = array_index %floats, %.loc25_3.9
 // CHECK:STDOUT:   %.loc25_3.11: init f64 = initialize_from %.loc20 to %.loc25_3.10
 // CHECK:STDOUT:   %.loc25_3.12: i32 = int_literal 3
-// CHECK:STDOUT:   %.loc25_3.13: ref f64 = array_index %.loc17_7, %.loc25_3.12
+// CHECK:STDOUT:   %.loc25_3.13: ref f64 = array_index %floats, %.loc25_3.12
 // CHECK:STDOUT:   %.loc25_3.14: init f64 = initialize_from %.loc21 to %.loc25_3.13
 // CHECK:STDOUT:   %.loc25_3.15: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc25_3.16: ref f64 = array_index %.loc17_7, %.loc25_3.15
+// CHECK:STDOUT:   %.loc25_3.16: ref f64 = array_index %floats, %.loc25_3.15
 // CHECK:STDOUT:   %.loc25_3.17: init f64 = initialize_from %.loc22 to %.loc25_3.16
 // CHECK:STDOUT:   %.loc25_3.18: i32 = int_literal 5
-// CHECK:STDOUT:   %.loc25_3.19: ref f64 = array_index %.loc17_7, %.loc25_3.18
+// CHECK:STDOUT:   %.loc25_3.19: ref f64 = array_index %floats, %.loc25_3.18
 // CHECK:STDOUT:   %.loc25_3.20: init f64 = initialize_from %.loc23 to %.loc25_3.19
 // CHECK:STDOUT:   %.loc25_3.21: i32 = int_literal 6
-// CHECK:STDOUT:   %.loc25_3.22: ref f64 = array_index %.loc17_7, %.loc25_3.21
+// CHECK:STDOUT:   %.loc25_3.22: ref f64 = array_index %floats, %.loc25_3.21
 // CHECK:STDOUT:   %.loc25_3.23: init f64 = initialize_from %.loc24 to %.loc25_3.22
-// CHECK:STDOUT:   %.loc25_3.24: init [f64; 7] = array_init %.loc25_3.2, (%.loc25_3.5, %.loc25_3.8, %.loc25_3.11, %.loc25_3.14, %.loc25_3.17, %.loc25_3.20, %.loc25_3.23) to %.loc17_7
+// CHECK:STDOUT:   %.loc25_3.24: init [f64; 7] = array_init %.loc25_3.2, (%.loc25_3.5, %.loc25_3.8, %.loc25_3.11, %.loc25_3.14, %.loc25_3.17, %.loc25_3.20, %.loc25_3.23) to %floats
 // CHECK:STDOUT:   assign %floats, %.loc25_3.24
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 7 - 7
toolchain/check/testdata/basics/raw_and_textual_ir.carbon

@@ -114,11 +114,11 @@ fn Foo(n: i32) -> (i32, f64) {
 // CHECK:STDOUT:   %.loc12_15: i32 = int_literal 2
 // CHECK:STDOUT:   %.loc12_13: i32 = add %n, %.loc12_15
 // CHECK:STDOUT:   %.loc12_18: f64 = real_literal 34e-1
-// CHECK:STDOUT:   %.loc12_21: (i32, f64) = tuple_literal (%.loc12_13, %.loc12_18)
-// CHECK:STDOUT:   %.loc12_22.1: ref i32 = tuple_access %return, member0
-// CHECK:STDOUT:   %.loc12_22.2: init i32 = initialize_from %.loc12_13 to %.loc12_22.1
-// CHECK:STDOUT:   %.loc12_22.3: ref f64 = tuple_access %return, member1
-// CHECK:STDOUT:   %.loc12_22.4: init f64 = initialize_from %.loc12_18 to %.loc12_22.3
-// CHECK:STDOUT:   %.loc12_22.5: init (i32, f64) = tuple_init %.loc12_21, (%.loc12_22.2, %.loc12_22.4)
-// CHECK:STDOUT:   return %.loc12_22.5
+// CHECK:STDOUT:   %.loc12_21.1: (i32, f64) = tuple_literal (%.loc12_13, %.loc12_18)
+// CHECK:STDOUT:   %.loc12_21.2: ref i32 = tuple_access %return, member0
+// CHECK:STDOUT:   %.loc12_21.3: init i32 = initialize_from %.loc12_13 to %.loc12_21.2
+// CHECK:STDOUT:   %.loc12_21.4: ref f64 = tuple_access %return, member1
+// CHECK:STDOUT:   %.loc12_21.5: init f64 = initialize_from %.loc12_18 to %.loc12_21.4
+// CHECK:STDOUT:   %.loc12_21.6: init (i32, f64) = tuple_init %.loc12_21.1, (%.loc12_21.3, %.loc12_21.5)
+// CHECK:STDOUT:   return %.loc12_21.6
 // CHECK:STDOUT: }

+ 7 - 7
toolchain/check/testdata/basics/textual_ir.carbon

@@ -21,11 +21,11 @@ fn Foo(n: i32) -> (i32, f64) {
 // CHECK:STDOUT:   %.loc12_15: i32 = int_literal 2
 // CHECK:STDOUT:   %.loc12_13: i32 = add %n, %.loc12_15
 // CHECK:STDOUT:   %.loc12_18: f64 = real_literal 34e-1
-// CHECK:STDOUT:   %.loc12_21: (i32, f64) = tuple_literal (%.loc12_13, %.loc12_18)
-// CHECK:STDOUT:   %.loc12_22.1: ref i32 = tuple_access %return, member0
-// CHECK:STDOUT:   %.loc12_22.2: init i32 = initialize_from %.loc12_13 to %.loc12_22.1
-// CHECK:STDOUT:   %.loc12_22.3: ref f64 = tuple_access %return, member1
-// CHECK:STDOUT:   %.loc12_22.4: init f64 = initialize_from %.loc12_18 to %.loc12_22.3
-// CHECK:STDOUT:   %.loc12_22.5: init (i32, f64) = tuple_init %.loc12_21, (%.loc12_22.2, %.loc12_22.4)
-// CHECK:STDOUT:   return %.loc12_22.5
+// CHECK:STDOUT:   %.loc12_21.1: (i32, f64) = tuple_literal (%.loc12_13, %.loc12_18)
+// CHECK:STDOUT:   %.loc12_21.2: ref i32 = tuple_access %return, member0
+// CHECK:STDOUT:   %.loc12_21.3: init i32 = initialize_from %.loc12_13 to %.loc12_21.2
+// CHECK:STDOUT:   %.loc12_21.4: ref f64 = tuple_access %return, member1
+// CHECK:STDOUT:   %.loc12_21.5: init f64 = initialize_from %.loc12_18 to %.loc12_21.4
+// CHECK:STDOUT:   %.loc12_21.6: init (i32, f64) = tuple_init %.loc12_21.1, (%.loc12_21.3, %.loc12_21.5)
+// CHECK:STDOUT:   return %.loc12_21.6
 // CHECK:STDOUT: }

+ 57 - 0
toolchain/check/testdata/if_expression/struct.carbon

@@ -0,0 +1,57 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn G(s: {.a: i32, .b: i32});
+
+fn F(cond: bool) {
+  var a: {.a: i32, .b: i32} = {.a = 1, .b = 2};
+  G(if cond then a else a);
+}
+
+// CHECK:STDOUT: file "struct.carbon" {
+// CHECK:STDOUT:   %.loc7 = fn_decl @G
+// CHECK:STDOUT:   %.loc9 = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G(%s: {.a: i32, .b: i32});
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F(%cond: bool) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc10_27: type = struct_type {.a: i32, .b: i32}
+// CHECK:STDOUT:   %a: ref {.a: i32, .b: i32} = var "a"
+// CHECK:STDOUT:   %.loc10_37: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc10_45: i32 = int_literal 2
+// CHECK:STDOUT:   %.loc10_46.1: {.a: i32, .b: i32} = struct_literal (%.loc10_37, %.loc10_45)
+// CHECK:STDOUT:   %.loc10_46.2: ref i32 = struct_access %a, member0
+// CHECK:STDOUT:   %.loc10_46.3: init i32 = initialize_from %.loc10_37 to %.loc10_46.2
+// CHECK:STDOUT:   %.loc10_46.4: ref i32 = struct_access %a, member1
+// CHECK:STDOUT:   %.loc10_46.5: init i32 = initialize_from %.loc10_45 to %.loc10_46.4
+// CHECK:STDOUT:   %.loc10_46.6: init {.a: i32, .b: i32} = struct_init %.loc10_46.1, (%.loc10_46.3, %.loc10_46.5)
+// CHECK:STDOUT:   assign %a, %.loc10_46.6
+// CHECK:STDOUT:   if %cond br !if.expr.then else br !if.expr.else
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.expr.then:
+// CHECK:STDOUT:   %.loc10_7.1: ref i32 = struct_access %a, member0
+// CHECK:STDOUT:   %.loc10_7.2: i32 = bind_value %.loc10_7.1
+// CHECK:STDOUT:   %.loc10_7.3: ref i32 = struct_access %a, member1
+// CHECK:STDOUT:   %.loc10_7.4: i32 = bind_value %.loc10_7.3
+// CHECK:STDOUT:   %.loc10_7.5: {.a: i32, .b: i32} = struct_value %a, (%.loc10_7.2, %.loc10_7.4)
+// CHECK:STDOUT:   br !if.expr.result(%.loc10_7.5)
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.expr.else:
+// CHECK:STDOUT:   %.loc10_7.6: ref i32 = struct_access %a, member0
+// CHECK:STDOUT:   %.loc10_7.7: i32 = bind_value %.loc10_7.6
+// CHECK:STDOUT:   %.loc10_7.8: ref i32 = struct_access %a, member1
+// CHECK:STDOUT:   %.loc10_7.9: i32 = bind_value %.loc10_7.8
+// CHECK:STDOUT:   %.loc10_7.10: {.a: i32, .b: i32} = struct_value %a, (%.loc10_7.7, %.loc10_7.9)
+// CHECK:STDOUT:   br !if.expr.result(%.loc10_7.10)
+// CHECK:STDOUT:
+// CHECK:STDOUT: !if.expr.result:
+// CHECK:STDOUT:   %.loc11_5: {.a: i32, .b: i32} = block_arg !if.expr.result
+// CHECK:STDOUT:   %.loc11_4.1: type = tuple_type ()
+// CHECK:STDOUT:   %.loc11_4.2: init () = call @G(%.loc11_5)
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }

+ 3 - 4
toolchain/check/testdata/index/array_element_access.carbon

@@ -17,14 +17,13 @@ var d: i32 = a[b];
 // CHECK:STDOUT:   %.loc7_24: i32 = int_literal 24
 // CHECK:STDOUT:   %.loc7_26.1: type = tuple_type (i32, i32)
 // CHECK:STDOUT:   %.loc7_26.2: (i32, i32) = tuple_literal (%.loc7_20, %.loc7_24)
-// CHECK:STDOUT:   %.loc7_5: ref [i32; 2] = splice_block %a {}
 // CHECK:STDOUT:   %.loc7_26.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_26.4: ref i32 = array_index %.loc7_5, %.loc7_26.3
+// CHECK:STDOUT:   %.loc7_26.4: ref i32 = array_index %a, %.loc7_26.3
 // CHECK:STDOUT:   %.loc7_26.5: init i32 = initialize_from %.loc7_20 to %.loc7_26.4
 // CHECK:STDOUT:   %.loc7_26.6: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc7_26.7: ref i32 = array_index %.loc7_5, %.loc7_26.6
+// CHECK:STDOUT:   %.loc7_26.7: ref i32 = array_index %a, %.loc7_26.6
 // CHECK:STDOUT:   %.loc7_26.8: init i32 = initialize_from %.loc7_24 to %.loc7_26.7
-// CHECK:STDOUT:   %.loc7_26.9: init [i32; 2] = array_init %.loc7_26.2, (%.loc7_26.5, %.loc7_26.8) to %.loc7_5
+// CHECK:STDOUT:   %.loc7_26.9: init [i32; 2] = array_init %.loc7_26.2, (%.loc7_26.5, %.loc7_26.8) to %a
 // CHECK:STDOUT:   assign %a, %.loc7_26.9
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc8_14: i32 = int_literal 1

+ 2 - 3
toolchain/check/testdata/index/fail_array_large_index.carbon

@@ -17,11 +17,10 @@ var b: i32 = a[0xFFFFFFFFFFFFFFFFF];
 // CHECK:STDOUT:   %.loc7_20: i32 = int_literal 12
 // CHECK:STDOUT:   %.loc7_23.1: type = tuple_type (i32)
 // CHECK:STDOUT:   %.loc7_23.2: (i32,) = tuple_literal (%.loc7_20)
-// CHECK:STDOUT:   %.loc7_5: ref [i32; 1] = splice_block %a {}
 // CHECK:STDOUT:   %.loc7_23.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_23.4: ref i32 = array_index %.loc7_5, %.loc7_23.3
+// CHECK:STDOUT:   %.loc7_23.4: ref i32 = array_index %a, %.loc7_23.3
 // CHECK:STDOUT:   %.loc7_23.5: init i32 = initialize_from %.loc7_20 to %.loc7_23.4
-// CHECK:STDOUT:   %.loc7_23.6: init [i32; 1] = array_init %.loc7_23.2, (%.loc7_23.5) to %.loc7_5
+// CHECK:STDOUT:   %.loc7_23.6: init [i32; 1] = array_init %.loc7_23.2, (%.loc7_23.5) to %a
 // CHECK:STDOUT:   assign %a, %.loc7_23.6
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc11_16: i32 = int_literal 295147905179352825855

+ 2 - 3
toolchain/check/testdata/index/fail_array_non_int_indexing.carbon

@@ -17,11 +17,10 @@ var b: i32 = a[2.6];
 // CHECK:STDOUT:   %.loc7_20: i32 = int_literal 12
 // CHECK:STDOUT:   %.loc7_23.1: type = tuple_type (i32)
 // CHECK:STDOUT:   %.loc7_23.2: (i32,) = tuple_literal (%.loc7_20)
-// CHECK:STDOUT:   %.loc7_5: ref [i32; 1] = splice_block %a {}
 // CHECK:STDOUT:   %.loc7_23.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_23.4: ref i32 = array_index %.loc7_5, %.loc7_23.3
+// CHECK:STDOUT:   %.loc7_23.4: ref i32 = array_index %a, %.loc7_23.3
 // CHECK:STDOUT:   %.loc7_23.5: init i32 = initialize_from %.loc7_20 to %.loc7_23.4
-// CHECK:STDOUT:   %.loc7_23.6: init [i32; 1] = array_init %.loc7_23.2, (%.loc7_23.5) to %.loc7_5
+// CHECK:STDOUT:   %.loc7_23.6: init [i32; 1] = array_init %.loc7_23.2, (%.loc7_23.5) to %a
 // CHECK:STDOUT:   assign %a, %.loc7_23.6
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc11_16: f64 = real_literal 26e-1

+ 2 - 3
toolchain/check/testdata/index/fail_array_out_of_bound_access.carbon

@@ -17,11 +17,10 @@ var b: i32 = a[2];
 // CHECK:STDOUT:   %.loc7_20: i32 = int_literal 12
 // CHECK:STDOUT:   %.loc7_23.1: type = tuple_type (i32)
 // CHECK:STDOUT:   %.loc7_23.2: (i32,) = tuple_literal (%.loc7_20)
-// CHECK:STDOUT:   %.loc7_5: ref [i32; 1] = splice_block %a {}
 // CHECK:STDOUT:   %.loc7_23.3: i32 = int_literal 0
-// CHECK:STDOUT:   %.loc7_23.4: ref i32 = array_index %.loc7_5, %.loc7_23.3
+// CHECK:STDOUT:   %.loc7_23.4: ref i32 = array_index %a, %.loc7_23.3
 // CHECK:STDOUT:   %.loc7_23.5: init i32 = initialize_from %.loc7_20 to %.loc7_23.4
-// CHECK:STDOUT:   %.loc7_23.6: init [i32; 1] = array_init %.loc7_23.2, (%.loc7_23.5) to %.loc7_5
+// CHECK:STDOUT:   %.loc7_23.6: init [i32; 1] = array_init %.loc7_23.2, (%.loc7_23.5) to %a
 // CHECK:STDOUT:   assign %a, %.loc7_23.6
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc11_16: i32 = int_literal 2

+ 7 - 7
toolchain/check/testdata/index/fail_non_deterministic_type.carbon

@@ -18,13 +18,13 @@ var c: i32 = a[b];
 // CHECK:STDOUT:   %a: ref (i32, i32) = var "a"
 // CHECK:STDOUT:   %.loc7_22: i32 = int_literal 2
 // CHECK:STDOUT:   %.loc7_25: i32 = int_literal 3
-// CHECK:STDOUT:   %.loc7_26: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_25)
-// CHECK:STDOUT:   %.loc7_27.1: ref i32 = tuple_access %a, member0
-// CHECK:STDOUT:   %.loc7_27.2: init i32 = initialize_from %.loc7_22 to %.loc7_27.1
-// CHECK:STDOUT:   %.loc7_27.3: ref i32 = tuple_access %a, member1
-// CHECK:STDOUT:   %.loc7_27.4: init i32 = initialize_from %.loc7_25 to %.loc7_27.3
-// CHECK:STDOUT:   %.loc7_27.5: init (i32, i32) = tuple_init %.loc7_26, (%.loc7_27.2, %.loc7_27.4)
-// CHECK:STDOUT:   assign %a, %.loc7_27.5
+// CHECK:STDOUT:   %.loc7_26.1: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_25)
+// CHECK:STDOUT:   %.loc7_26.2: ref i32 = tuple_access %a, member0
+// CHECK:STDOUT:   %.loc7_26.3: init i32 = initialize_from %.loc7_22 to %.loc7_26.2
+// CHECK:STDOUT:   %.loc7_26.4: ref i32 = tuple_access %a, member1
+// CHECK:STDOUT:   %.loc7_26.5: init i32 = initialize_from %.loc7_25 to %.loc7_26.4
+// CHECK:STDOUT:   %.loc7_26.6: init (i32, i32) = tuple_init %.loc7_26.1, (%.loc7_26.3, %.loc7_26.5)
+// CHECK:STDOUT:   assign %a, %.loc7_26.6
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc8: i32 = int_literal 0
 // CHECK:STDOUT:   assign %b, %.loc8

+ 7 - 5
toolchain/check/testdata/index/fail_tuple_large_index.carbon

@@ -17,13 +17,15 @@ var c: i32 = b[0xFFFFFFFFFFFFFFFFF];
 // CHECK:STDOUT:   %.loc7_13.3: type = tuple_type (i32)
 // CHECK:STDOUT:   %a: ref (i32,) = var "a"
 // CHECK:STDOUT:   %.loc7_18: i32 = int_literal 12
-// CHECK:STDOUT:   %.loc7_21: (i32,) = tuple_literal (%.loc7_18)
-// CHECK:STDOUT:   %.loc7_22: init (i32,) = tuple_init %.loc7_21, (%.loc7_18)
-// CHECK:STDOUT:   assign %a, %.loc7_22
+// CHECK:STDOUT:   %.loc7_21.1: (i32,) = tuple_literal (%.loc7_18)
+// CHECK:STDOUT:   %.loc7_21.2: init (i32,) = tuple_init %.loc7_21.1, (%.loc7_18)
+// CHECK:STDOUT:   assign %a, %.loc7_21.2
 // CHECK:STDOUT:   %.loc8: (type,) = tuple_literal (i32)
 // CHECK:STDOUT:   %b: ref (i32,) = var "b"
-// CHECK:STDOUT:   %.loc7_5: (i32,) = bind_value %a
-// CHECK:STDOUT:   assign %b, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = tuple_access %a, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: init (i32,) = tuple_init %a, (%.loc7_5.2)
+// CHECK:STDOUT:   assign %b, %.loc7_5.3
 // CHECK:STDOUT:   %c: ref i32 = var "c"
 // CHECK:STDOUT:   %.loc12_16: i32 = int_literal 295147905179352825855
 // CHECK:STDOUT:   %.loc12_35: ref <error> = tuple_index %b, <error>

+ 7 - 7
toolchain/check/testdata/index/fail_tuple_non_int_indexing.carbon

@@ -17,13 +17,13 @@ var b: i32 = a[2.6];
 // CHECK:STDOUT:   %a: ref (i32, i32) = var "a"
 // CHECK:STDOUT:   %.loc7_22: i32 = int_literal 12
 // CHECK:STDOUT:   %.loc7_26: i32 = int_literal 6
-// CHECK:STDOUT:   %.loc7_27: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_26)
-// CHECK:STDOUT:   %.loc7_28.1: ref i32 = tuple_access %a, member0
-// CHECK:STDOUT:   %.loc7_28.2: init i32 = initialize_from %.loc7_22 to %.loc7_28.1
-// CHECK:STDOUT:   %.loc7_28.3: ref i32 = tuple_access %a, member1
-// CHECK:STDOUT:   %.loc7_28.4: init i32 = initialize_from %.loc7_26 to %.loc7_28.3
-// CHECK:STDOUT:   %.loc7_28.5: init (i32, i32) = tuple_init %.loc7_27, (%.loc7_28.2, %.loc7_28.4)
-// CHECK:STDOUT:   assign %a, %.loc7_28.5
+// CHECK:STDOUT:   %.loc7_27.1: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_26)
+// CHECK:STDOUT:   %.loc7_27.2: ref i32 = tuple_access %a, member0
+// CHECK:STDOUT:   %.loc7_27.3: init i32 = initialize_from %.loc7_22 to %.loc7_27.2
+// CHECK:STDOUT:   %.loc7_27.4: ref i32 = tuple_access %a, member1
+// CHECK:STDOUT:   %.loc7_27.5: init i32 = initialize_from %.loc7_26 to %.loc7_27.4
+// CHECK:STDOUT:   %.loc7_27.6: init (i32, i32) = tuple_init %.loc7_27.1, (%.loc7_27.3, %.loc7_27.5)
+// CHECK:STDOUT:   assign %a, %.loc7_27.6
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc11_16: f64 = real_literal 26e-1
 // CHECK:STDOUT:   %.loc11_19: ref <error> = tuple_index %a, <error>

+ 7 - 7
toolchain/check/testdata/index/fail_tuple_out_of_bound_access.carbon

@@ -17,13 +17,13 @@ var b: i32 = a[2];
 // CHECK:STDOUT:   %a: ref (i32, i32) = var "a"
 // CHECK:STDOUT:   %.loc7_22: i32 = int_literal 12
 // CHECK:STDOUT:   %.loc7_26: i32 = int_literal 6
-// CHECK:STDOUT:   %.loc7_27: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_26)
-// CHECK:STDOUT:   %.loc7_28.1: ref i32 = tuple_access %a, member0
-// CHECK:STDOUT:   %.loc7_28.2: init i32 = initialize_from %.loc7_22 to %.loc7_28.1
-// CHECK:STDOUT:   %.loc7_28.3: ref i32 = tuple_access %a, member1
-// CHECK:STDOUT:   %.loc7_28.4: init i32 = initialize_from %.loc7_26 to %.loc7_28.3
-// CHECK:STDOUT:   %.loc7_28.5: init (i32, i32) = tuple_init %.loc7_27, (%.loc7_28.2, %.loc7_28.4)
-// CHECK:STDOUT:   assign %a, %.loc7_28.5
+// CHECK:STDOUT:   %.loc7_27.1: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_26)
+// CHECK:STDOUT:   %.loc7_27.2: ref i32 = tuple_access %a, member0
+// CHECK:STDOUT:   %.loc7_27.3: init i32 = initialize_from %.loc7_22 to %.loc7_27.2
+// CHECK:STDOUT:   %.loc7_27.4: ref i32 = tuple_access %a, member1
+// CHECK:STDOUT:   %.loc7_27.5: init i32 = initialize_from %.loc7_26 to %.loc7_27.4
+// CHECK:STDOUT:   %.loc7_27.6: init (i32, i32) = tuple_init %.loc7_27.1, (%.loc7_27.3, %.loc7_27.5)
+// CHECK:STDOUT:   assign %a, %.loc7_27.6
 // CHECK:STDOUT:   %b: ref i32 = var "b"
 // CHECK:STDOUT:   %.loc11_16: i32 = int_literal 2
 // CHECK:STDOUT:   %.loc11_17: ref <error> = tuple_index %a, <error>

+ 7 - 5
toolchain/check/testdata/index/tuple_element_access.carbon

@@ -14,13 +14,15 @@ var c: i32 = b[0];
 // CHECK:STDOUT:   %.loc7_13.3: type = tuple_type (i32)
 // CHECK:STDOUT:   %a: ref (i32,) = var "a"
 // CHECK:STDOUT:   %.loc7_18: i32 = int_literal 12
-// CHECK:STDOUT:   %.loc7_21: (i32,) = tuple_literal (%.loc7_18)
-// CHECK:STDOUT:   %.loc7_22: init (i32,) = tuple_init %.loc7_21, (%.loc7_18)
-// CHECK:STDOUT:   assign %a, %.loc7_22
+// CHECK:STDOUT:   %.loc7_21.1: (i32,) = tuple_literal (%.loc7_18)
+// CHECK:STDOUT:   %.loc7_21.2: init (i32,) = tuple_init %.loc7_21.1, (%.loc7_18)
+// CHECK:STDOUT:   assign %a, %.loc7_21.2
 // CHECK:STDOUT:   %.loc8: (type,) = tuple_literal (i32)
 // CHECK:STDOUT:   %b: ref (i32,) = var "b"
-// CHECK:STDOUT:   %.loc7_5: (i32,) = bind_value %a
-// CHECK:STDOUT:   assign %b, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = tuple_access %a, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: init (i32,) = tuple_init %a, (%.loc7_5.2)
+// CHECK:STDOUT:   assign %b, %.loc7_5.3
 // CHECK:STDOUT:   %c: ref i32 = var "c"
 // CHECK:STDOUT:   %.loc9_16: i32 = int_literal 0
 // CHECK:STDOUT:   %.loc9_17.1: ref i32 = tuple_index %b, %.loc9_16

+ 14 - 14
toolchain/check/testdata/operators/assignment.carbon

@@ -39,13 +39,13 @@ fn Main() {
 // CHECK:STDOUT:   %b: ref (i32, i32) = var "b"
 // CHECK:STDOUT:   %.loc11_24: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc11_27: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc11_28: (i32, i32) = tuple_literal (%.loc11_24, %.loc11_27)
-// CHECK:STDOUT:   %.loc11_29.1: ref i32 = tuple_access %b, member0
-// CHECK:STDOUT:   %.loc11_29.2: init i32 = initialize_from %.loc11_24 to %.loc11_29.1
-// CHECK:STDOUT:   %.loc11_29.3: ref i32 = tuple_access %b, member1
-// CHECK:STDOUT:   %.loc11_29.4: init i32 = initialize_from %.loc11_27 to %.loc11_29.3
-// CHECK:STDOUT:   %.loc11_29.5: init (i32, i32) = tuple_init %.loc11_28, (%.loc11_29.2, %.loc11_29.4)
-// CHECK:STDOUT:   assign %b, %.loc11_29.5
+// CHECK:STDOUT:   %.loc11_28.1: (i32, i32) = tuple_literal (%.loc11_24, %.loc11_27)
+// CHECK:STDOUT:   %.loc11_28.2: ref i32 = tuple_access %b, member0
+// CHECK:STDOUT:   %.loc11_28.3: init i32 = initialize_from %.loc11_24 to %.loc11_28.2
+// CHECK:STDOUT:   %.loc11_28.4: ref i32 = tuple_access %b, member1
+// CHECK:STDOUT:   %.loc11_28.5: init i32 = initialize_from %.loc11_27 to %.loc11_28.4
+// CHECK:STDOUT:   %.loc11_28.6: init (i32, i32) = tuple_init %.loc11_28.1, (%.loc11_28.3, %.loc11_28.5)
+// CHECK:STDOUT:   assign %b, %.loc11_28.6
 // CHECK:STDOUT:   %.loc12_5: i32 = int_literal 0
 // CHECK:STDOUT:   %.loc12_6: ref i32 = tuple_index %b, %.loc12_5
 // CHECK:STDOUT:   %.loc12_10: i32 = int_literal 3
@@ -58,13 +58,13 @@ fn Main() {
 // CHECK:STDOUT:   %c: ref {.a: i32, .b: i32} = var "c"
 // CHECK:STDOUT:   %.loc15_37: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc15_45: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc15_46: {.a: i32, .b: i32} = struct_literal (%.loc15_37, %.loc15_45)
-// CHECK:STDOUT:   %.loc15_47.1: ref i32 = struct_access %c, member0
-// CHECK:STDOUT:   %.loc15_47.2: init i32 = initialize_from %.loc15_37 to %.loc15_47.1
-// CHECK:STDOUT:   %.loc15_47.3: ref i32 = struct_access %c, member1
-// CHECK:STDOUT:   %.loc15_47.4: init i32 = initialize_from %.loc15_45 to %.loc15_47.3
-// CHECK:STDOUT:   %.loc15_47.5: init {.a: i32, .b: i32} = struct_init %.loc15_46, (%.loc15_47.2, %.loc15_47.4)
-// CHECK:STDOUT:   assign %c, %.loc15_47.5
+// CHECK:STDOUT:   %.loc15_46.1: {.a: i32, .b: i32} = struct_literal (%.loc15_37, %.loc15_45)
+// CHECK:STDOUT:   %.loc15_46.2: ref i32 = struct_access %c, member0
+// CHECK:STDOUT:   %.loc15_46.3: init i32 = initialize_from %.loc15_37 to %.loc15_46.2
+// CHECK:STDOUT:   %.loc15_46.4: ref i32 = struct_access %c, member1
+// CHECK:STDOUT:   %.loc15_46.5: init i32 = initialize_from %.loc15_45 to %.loc15_46.4
+// CHECK:STDOUT:   %.loc15_46.6: init {.a: i32, .b: i32} = struct_init %.loc15_46.1, (%.loc15_46.3, %.loc15_46.5)
+// CHECK:STDOUT:   assign %c, %.loc15_46.6
 // CHECK:STDOUT:   %.loc16_4: ref i32 = struct_access %c, member0
 // CHECK:STDOUT:   %.loc16_9: i32 = int_literal 3
 // CHECK:STDOUT:   assign %.loc16_4, %.loc16_9

+ 21 - 21
toolchain/check/testdata/operators/fail_assigment_to_non_assignable.carbon

@@ -66,13 +66,13 @@ fn Main() {
 // CHECK:STDOUT:   %.loc21_8.2: (i32, i32) = tuple_literal (%.loc21_4, %.loc21_7)
 // CHECK:STDOUT:   %.loc21_13: i32 = int_literal 3
 // CHECK:STDOUT:   %.loc21_16: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc21_17: (i32, i32) = tuple_literal (%.loc21_13, %.loc21_16)
-// CHECK:STDOUT:   %.loc21_10.1: i32 = tuple_access %.loc21_8.2, member0
-// CHECK:STDOUT:   %.loc21_10.2: init i32 = initialize_from %.loc21_13 to %.loc21_10.1
-// CHECK:STDOUT:   %.loc21_10.3: i32 = tuple_access %.loc21_8.2, member1
-// CHECK:STDOUT:   %.loc21_10.4: init i32 = initialize_from %.loc21_16 to %.loc21_10.3
-// CHECK:STDOUT:   %.loc21_10.5: init (i32, i32) = tuple_init %.loc21_17, (%.loc21_10.2, %.loc21_10.4)
-// CHECK:STDOUT:   assign %.loc21_8.2, %.loc21_10.5
+// CHECK:STDOUT:   %.loc21_17.1: (i32, i32) = tuple_literal (%.loc21_13, %.loc21_16)
+// CHECK:STDOUT:   %.loc21_17.2: i32 = tuple_access %.loc21_8.2, member0
+// CHECK:STDOUT:   %.loc21_17.3: init i32 = initialize_from %.loc21_13 to %.loc21_17.2
+// CHECK:STDOUT:   %.loc21_17.4: i32 = tuple_access %.loc21_8.2, member1
+// CHECK:STDOUT:   %.loc21_17.5: init i32 = initialize_from %.loc21_16 to %.loc21_17.4
+// CHECK:STDOUT:   %.loc21_17.6: init (i32, i32) = tuple_init %.loc21_17.1, (%.loc21_17.3, %.loc21_17.5)
+// CHECK:STDOUT:   assign %.loc21_8.2, %.loc21_17.6
 // CHECK:STDOUT:   %.loc21_8.3: (i32, i32) = tuple_value %.loc21_8.2, (%.loc21_4, %.loc21_7)
 // CHECK:STDOUT:   %n: ref i32 = var "n"
 // CHECK:STDOUT:   %.loc22_16: i32 = int_literal 0
@@ -80,13 +80,13 @@ fn Main() {
 // CHECK:STDOUT:   %.loc26_8.1: (i32, i32) = tuple_literal (%n, %n)
 // CHECK:STDOUT:   %.loc26_13: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc26_16: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc26_17: (i32, i32) = tuple_literal (%.loc26_13, %.loc26_16)
-// CHECK:STDOUT:   %.loc26_10.1: i32 = tuple_access %.loc26_8.1, member0
-// CHECK:STDOUT:   %.loc26_10.2: init i32 = initialize_from %.loc26_13 to %.loc26_10.1
-// CHECK:STDOUT:   %.loc26_10.3: i32 = tuple_access %.loc26_8.1, member1
-// CHECK:STDOUT:   %.loc26_10.4: init i32 = initialize_from %.loc26_16 to %.loc26_10.3
-// CHECK:STDOUT:   %.loc26_10.5: init (i32, i32) = tuple_init %.loc26_17, (%.loc26_10.2, %.loc26_10.4)
-// CHECK:STDOUT:   assign %.loc26_8.1, %.loc26_10.5
+// CHECK:STDOUT:   %.loc26_17.1: (i32, i32) = tuple_literal (%.loc26_13, %.loc26_16)
+// CHECK:STDOUT:   %.loc26_17.2: i32 = tuple_access %.loc26_8.1, member0
+// CHECK:STDOUT:   %.loc26_17.3: init i32 = initialize_from %.loc26_13 to %.loc26_17.2
+// CHECK:STDOUT:   %.loc26_17.4: i32 = tuple_access %.loc26_8.1, member1
+// CHECK:STDOUT:   %.loc26_17.5: init i32 = initialize_from %.loc26_16 to %.loc26_17.4
+// CHECK:STDOUT:   %.loc26_17.6: init (i32, i32) = tuple_init %.loc26_17.1, (%.loc26_17.3, %.loc26_17.5)
+// CHECK:STDOUT:   assign %.loc26_8.1, %.loc26_17.6
 // CHECK:STDOUT:   %.loc22_7.1: i32 = bind_value %n
 // CHECK:STDOUT:   %.loc22_7.2: i32 = bind_value %n
 // CHECK:STDOUT:   %.loc26_8.2: (i32, i32) = tuple_value %.loc26_8.1, (%.loc22_7.1, %.loc22_7.2)
@@ -98,13 +98,13 @@ fn Main() {
 // CHECK:STDOUT:   %.loc34_18.2: {.x: i32, .y: i32} = struct_literal (%.loc34_9, %.loc34_17)
 // CHECK:STDOUT:   %.loc34_28: i32 = int_literal 3
 // CHECK:STDOUT:   %.loc34_36: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc34_37: {.x: i32, .y: i32} = struct_literal (%.loc34_28, %.loc34_36)
-// CHECK:STDOUT:   %.loc34_20.1: i32 = struct_access %.loc34_18.2, member0
-// CHECK:STDOUT:   %.loc34_20.2: init i32 = initialize_from %.loc34_28 to %.loc34_20.1
-// CHECK:STDOUT:   %.loc34_20.3: i32 = struct_access %.loc34_18.2, member1
-// CHECK:STDOUT:   %.loc34_20.4: init i32 = initialize_from %.loc34_36 to %.loc34_20.3
-// CHECK:STDOUT:   %.loc34_20.5: init {.x: i32, .y: i32} = struct_init %.loc34_37, (%.loc34_20.2, %.loc34_20.4)
-// CHECK:STDOUT:   assign %.loc34_18.2, %.loc34_20.5
+// CHECK:STDOUT:   %.loc34_37.1: {.x: i32, .y: i32} = struct_literal (%.loc34_28, %.loc34_36)
+// CHECK:STDOUT:   %.loc34_37.2: i32 = struct_access %.loc34_18.2, member0
+// CHECK:STDOUT:   %.loc34_37.3: init i32 = initialize_from %.loc34_28 to %.loc34_37.2
+// CHECK:STDOUT:   %.loc34_37.4: i32 = struct_access %.loc34_18.2, member1
+// CHECK:STDOUT:   %.loc34_37.5: init i32 = initialize_from %.loc34_36 to %.loc34_37.4
+// CHECK:STDOUT:   %.loc34_37.6: init {.x: i32, .y: i32} = struct_init %.loc34_37.1, (%.loc34_37.3, %.loc34_37.5)
+// CHECK:STDOUT:   assign %.loc34_18.2, %.loc34_37.6
 // CHECK:STDOUT:   %.loc34_18.3: {.x: i32, .y: i32} = struct_value %.loc34_18.2, (%.loc34_9, %.loc34_17)
 // CHECK:STDOUT:   %.loc38_7: bool = bool_literal true
 // CHECK:STDOUT:   if %.loc38_7 br !if.expr.then.loc38 else br !if.expr.else.loc38

+ 14 - 14
toolchain/check/testdata/pointer/address_of_lvalue.carbon

@@ -26,13 +26,13 @@ fn F() {
 // CHECK:STDOUT:   %s: ref {.a: i32, .b: i32} = var "s"
 // CHECK:STDOUT:   %.loc8_37: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc8_45: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc8_46: {.a: i32, .b: i32} = struct_literal (%.loc8_37, %.loc8_45)
-// CHECK:STDOUT:   %.loc8_47.1: ref i32 = struct_access %s, member0
-// CHECK:STDOUT:   %.loc8_47.2: init i32 = initialize_from %.loc8_37 to %.loc8_47.1
-// CHECK:STDOUT:   %.loc8_47.3: ref i32 = struct_access %s, member1
-// CHECK:STDOUT:   %.loc8_47.4: init i32 = initialize_from %.loc8_45 to %.loc8_47.3
-// CHECK:STDOUT:   %.loc8_47.5: init {.a: i32, .b: i32} = struct_init %.loc8_46, (%.loc8_47.2, %.loc8_47.4)
-// CHECK:STDOUT:   assign %s, %.loc8_47.5
+// CHECK:STDOUT:   %.loc8_46.1: {.a: i32, .b: i32} = struct_literal (%.loc8_37, %.loc8_45)
+// CHECK:STDOUT:   %.loc8_46.2: ref i32 = struct_access %s, member0
+// CHECK:STDOUT:   %.loc8_46.3: init i32 = initialize_from %.loc8_37 to %.loc8_46.2
+// CHECK:STDOUT:   %.loc8_46.4: ref i32 = struct_access %s, member1
+// CHECK:STDOUT:   %.loc8_46.5: init i32 = initialize_from %.loc8_45 to %.loc8_46.4
+// CHECK:STDOUT:   %.loc8_46.6: init {.a: i32, .b: i32} = struct_init %.loc8_46.1, (%.loc8_46.3, %.loc8_46.5)
+// CHECK:STDOUT:   assign %s, %.loc8_46.6
 // CHECK:STDOUT:   %.loc10_27: type = struct_type {.a: i32, .b: i32}
 // CHECK:STDOUT:   %.loc10_28: type = ptr_type {.a: i32, .b: i32}
 // CHECK:STDOUT:   %p: ref {.a: i32, .b: i32}* = var "p"
@@ -54,13 +54,13 @@ fn F() {
 // CHECK:STDOUT:   %t: ref (i32, i32) = var "t"
 // CHECK:STDOUT:   %.loc14_24: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc14_27: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc14_28: (i32, i32) = tuple_literal (%.loc14_24, %.loc14_27)
-// CHECK:STDOUT:   %.loc14_29.1: ref i32 = tuple_access %t, member0
-// CHECK:STDOUT:   %.loc14_29.2: init i32 = initialize_from %.loc14_24 to %.loc14_29.1
-// CHECK:STDOUT:   %.loc14_29.3: ref i32 = tuple_access %t, member1
-// CHECK:STDOUT:   %.loc14_29.4: init i32 = initialize_from %.loc14_27 to %.loc14_29.3
-// CHECK:STDOUT:   %.loc14_29.5: init (i32, i32) = tuple_init %.loc14_28, (%.loc14_29.2, %.loc14_29.4)
-// CHECK:STDOUT:   assign %t, %.loc14_29.5
+// CHECK:STDOUT:   %.loc14_28.1: (i32, i32) = tuple_literal (%.loc14_24, %.loc14_27)
+// CHECK:STDOUT:   %.loc14_28.2: ref i32 = tuple_access %t, member0
+// CHECK:STDOUT:   %.loc14_28.3: init i32 = initialize_from %.loc14_24 to %.loc14_28.2
+// CHECK:STDOUT:   %.loc14_28.4: ref i32 = tuple_access %t, member1
+// CHECK:STDOUT:   %.loc14_28.5: init i32 = initialize_from %.loc14_27 to %.loc14_28.4
+// CHECK:STDOUT:   %.loc14_28.6: init (i32, i32) = tuple_init %.loc14_28.1, (%.loc14_28.3, %.loc14_28.5)
+// CHECK:STDOUT:   assign %t, %.loc14_28.6
 // CHECK:STDOUT:   %.loc15_14: type = ptr_type i32
 // CHECK:STDOUT:   %t0: ref i32* = var "t0"
 // CHECK:STDOUT:   %.loc15_21: i32 = int_literal 0

+ 7 - 7
toolchain/check/testdata/return/tuple.carbon

@@ -17,11 +17,11 @@ fn Main() -> (i32, i32) {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %.loc9_11: i32 = int_literal 15
 // CHECK:STDOUT:   %.loc9_15: i32 = int_literal 35
-// CHECK:STDOUT:   %.loc9_17: (i32, i32) = tuple_literal (%.loc9_11, %.loc9_15)
-// CHECK:STDOUT:   %.loc9_18.1: ref i32 = tuple_access %return, member0
-// CHECK:STDOUT:   %.loc9_18.2: init i32 = initialize_from %.loc9_11 to %.loc9_18.1
-// CHECK:STDOUT:   %.loc9_18.3: ref i32 = tuple_access %return, member1
-// CHECK:STDOUT:   %.loc9_18.4: init i32 = initialize_from %.loc9_15 to %.loc9_18.3
-// CHECK:STDOUT:   %.loc9_18.5: init (i32, i32) = tuple_init %.loc9_17, (%.loc9_18.2, %.loc9_18.4)
-// CHECK:STDOUT:   return %.loc9_18.5
+// CHECK:STDOUT:   %.loc9_17.1: (i32, i32) = tuple_literal (%.loc9_11, %.loc9_15)
+// CHECK:STDOUT:   %.loc9_17.2: ref i32 = tuple_access %return, member0
+// CHECK:STDOUT:   %.loc9_17.3: init i32 = initialize_from %.loc9_11 to %.loc9_17.2
+// CHECK:STDOUT:   %.loc9_17.4: ref i32 = tuple_access %return, member1
+// CHECK:STDOUT:   %.loc9_17.5: init i32 = initialize_from %.loc9_15 to %.loc9_17.4
+// CHECK:STDOUT:   %.loc9_17.6: init (i32, i32) = tuple_init %.loc9_17.1, (%.loc9_17.3, %.loc9_17.5)
+// CHECK:STDOUT:   return %.loc9_17.6
 // CHECK:STDOUT: }

+ 4 - 4
toolchain/check/testdata/struct/empty.carbon

@@ -11,11 +11,11 @@ var y: {} = x;
 // CHECK:STDOUT:   %.loc7_9.1: type = struct_type {}
 // CHECK:STDOUT:   %.loc7_9.2: {} = struct_literal ()
 // CHECK:STDOUT:   %x: ref {} = var "x"
-// CHECK:STDOUT:   %.loc7_14: {} = struct_literal ()
-// CHECK:STDOUT:   %.loc7_15: init {} = struct_init %.loc7_14, ()
-// CHECK:STDOUT:   assign %x, %.loc7_15
+// CHECK:STDOUT:   %.loc7_14.1: {} = struct_literal ()
+// CHECK:STDOUT:   %.loc7_14.2: init {} = struct_init %.loc7_14.1, ()
+// CHECK:STDOUT:   assign %x, %.loc7_14.2
 // CHECK:STDOUT:   %.loc8: {} = struct_literal ()
 // CHECK:STDOUT:   %y: ref {} = var "y"
-// CHECK:STDOUT:   %.loc7_5: {} = bind_value %x
+// CHECK:STDOUT:   %.loc7_5: init {} = struct_init %x, ()
 // CHECK:STDOUT:   assign %y, %.loc7_5
 // CHECK:STDOUT: }

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

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_empty.carbon:[[@LINE+3]]:22: ERROR: Cannot implicitly convert from `{} as type` to `{.a: i32}`.
+// CHECK:STDERR: fail_assign_empty.carbon:[[@LINE+3]]:21: ERROR: Cannot initialize struct of 1 element(s) from struct with 0 element(s).
 // CHECK:STDERR: var x: {.a: i32} = {};
-// CHECK:STDERR:                      ^
+// CHECK:STDERR:                     ^
 var x: {.a: i32} = {};
 
 // CHECK:STDOUT: file "fail_assign_empty.carbon" {

+ 2 - 2
toolchain/check/testdata/struct/fail_assign_nested.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_nested.carbon:[[@LINE+3]]:28: ERROR: Cannot implicitly convert from `{.b: {}}` to `{.a: {}}`.
+// CHECK:STDERR: fail_assign_nested.carbon:[[@LINE+3]]:27: ERROR: Mismatched names for field 1 in struct initialization: source has field name `b`, destination has field name `a`.
 // CHECK:STDERR: var x: {.a: {}} = {.b = {}};
-// CHECK:STDERR:                            ^
+// CHECK:STDERR:                           ^
 var x: {.a: {}} = {.b = {}};
 
 // CHECK:STDOUT: file "fail_assign_nested.carbon" {

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

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_to_empty.carbon:[[@LINE+3]]:21: ERROR: Cannot implicitly convert from `{.a: i32}` to `{} as type`.
+// CHECK:STDERR: fail_assign_to_empty.carbon:[[@LINE+3]]:20: ERROR: Cannot initialize struct of 0 element(s) from struct with 1 element(s).
 // CHECK:STDERR: var x: {} = {.a = 1};
-// CHECK:STDERR:                     ^
+// CHECK:STDERR:                    ^
 var x: {} = {.a = 1};
 
 // CHECK:STDOUT: file "fail_assign_to_empty.carbon" {

+ 2 - 2
toolchain/check/testdata/struct/fail_field_name_mismatch.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_field_name_mismatch.carbon:[[@LINE+3]]:28: ERROR: Cannot implicitly convert from `{.b: i32}` to `{.a: i32}`.
+// CHECK:STDERR: fail_field_name_mismatch.carbon:[[@LINE+3]]:27: ERROR: Mismatched names for field 1 in struct initialization: source has field name `b`, destination has field name `a`.
 // CHECK:STDERR: var x: {.a: i32} = {.b = 1};
-// CHECK:STDERR:                            ^
+// CHECK:STDERR:                           ^
 var x: {.a: i32} = {.b = 1};
 
 // CHECK:STDOUT: file "fail_field_name_mismatch.carbon" {

+ 2 - 2
toolchain/check/testdata/struct/fail_field_type_mismatch.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_field_type_mismatch.carbon:[[@LINE+3]]:30: ERROR: Cannot implicitly convert from `{.b: f64}` to `{.a: i32}`.
+// CHECK:STDERR: fail_field_type_mismatch.carbon:[[@LINE+3]]:29: ERROR: Mismatched names for field 1 in struct initialization: source has field name `b`, destination has field name `a`.
 // CHECK:STDERR: var x: {.a: i32} = {.b = 1.0};
-// CHECK:STDERR:                              ^
+// CHECK:STDERR:                             ^
 var x: {.a: i32} = {.b = 1.0};
 
 // CHECK:STDOUT: file "fail_field_type_mismatch.carbon" {

+ 3 - 3
toolchain/check/testdata/struct/fail_member_access_type.carbon

@@ -14,9 +14,9 @@ var y: i32 = x.b;
 // CHECK:STDOUT:   %.loc7_16: type = struct_type {.a: f64}
 // CHECK:STDOUT:   %x: ref {.a: f64} = var "x"
 // CHECK:STDOUT:   %.loc7_26: f64 = real_literal 40e-1
-// CHECK:STDOUT:   %.loc7_29: {.a: f64} = struct_literal (%.loc7_26)
-// CHECK:STDOUT:   %.loc7_30: init {.a: f64} = struct_init %.loc7_29, (%.loc7_26)
-// CHECK:STDOUT:   assign %x, %.loc7_30
+// CHECK:STDOUT:   %.loc7_29.1: {.a: f64} = struct_literal (%.loc7_26)
+// CHECK:STDOUT:   %.loc7_29.2: init {.a: f64} = struct_init %.loc7_29.1, (%.loc7_26)
+// CHECK:STDOUT:   assign %x, %.loc7_29.2
 // CHECK:STDOUT:   %y: ref i32 = var "y"
 // CHECK:STDOUT:   assign %y, <error>
 // CHECK:STDOUT: }

+ 3 - 3
toolchain/check/testdata/struct/fail_non_member_access.carbon

@@ -14,9 +14,9 @@ var y: i32 = x.b;
 // CHECK:STDOUT:   %.loc7_16: type = struct_type {.a: i32}
 // CHECK:STDOUT:   %x: ref {.a: i32} = var "x"
 // CHECK:STDOUT:   %.loc7_26: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc7_27: {.a: i32} = struct_literal (%.loc7_26)
-// CHECK:STDOUT:   %.loc7_28: init {.a: i32} = struct_init %.loc7_27, (%.loc7_26)
-// CHECK:STDOUT:   assign %x, %.loc7_28
+// CHECK:STDOUT:   %.loc7_27.1: {.a: i32} = struct_literal (%.loc7_26)
+// CHECK:STDOUT:   %.loc7_27.2: init {.a: i32} = struct_init %.loc7_27.1, (%.loc7_26)
+// CHECK:STDOUT:   assign %x, %.loc7_27.2
 // CHECK:STDOUT:   %y: ref i32 = var "y"
 // CHECK:STDOUT:   assign %y, <error>
 // CHECK:STDOUT: }

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

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_too_few_values.carbon:[[@LINE+3]]:37: ERROR: Cannot implicitly convert from `{.a: i32}` to `{.a: i32, .b: i32}`.
+// CHECK:STDERR: fail_too_few_values.carbon:[[@LINE+3]]:36: ERROR: Cannot initialize struct of 2 element(s) from struct with 1 element(s).
 // CHECK:STDERR: var x: {.a: i32, .b: i32} = {.a = 1};
-// CHECK:STDERR:                                     ^
+// CHECK:STDERR:                                    ^
 var x: {.a: i32, .b: i32} = {.a = 1};
 
 // CHECK:STDOUT: file "fail_too_few_values.carbon" {

+ 40 - 0
toolchain/check/testdata/struct/literal_member_access.carbon

@@ -0,0 +1,40 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn G() -> {.x: i32, .y: i32, .z: i32};
+
+fn F() -> i32 {
+  return {.a = 1, .b = G(), .c = 3}.b.y;
+}
+
+// CHECK:STDOUT: file "literal_member_access.carbon" {
+// CHECK:STDOUT:   %.loc7 = fn_decl @G
+// CHECK:STDOUT:   %.loc9 = fn_decl @F
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @G() -> %return: {.x: i32, .y: i32, .z: i32};
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @F() -> i32 {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %.loc10_16: i32 = int_literal 1
+// CHECK:STDOUT:   %.loc10_25.1: ref {.x: i32, .y: i32, .z: i32} = temporary_storage
+// CHECK:STDOUT:   %.loc10_25.2: init {.x: i32, .y: i32, .z: i32} = call @G() to %.loc10_25.1
+// CHECK:STDOUT:   %.loc10_34: i32 = int_literal 3
+// CHECK:STDOUT:   %.loc10_35.1: type = struct_type {.a: i32, .b: {.x: i32, .y: i32, .z: i32}, .c: i32}
+// CHECK:STDOUT:   %.loc10_35.2: {.a: i32, .b: {.x: i32, .y: i32, .z: i32}, .c: i32} = struct_literal (%.loc10_16, %.loc10_25.2, %.loc10_34)
+// CHECK:STDOUT:   %.loc10_25.3: ref {.x: i32, .y: i32, .z: i32} = temporary %.loc10_25.1, %.loc10_25.2
+// CHECK:STDOUT:   %.loc10_25.4: ref i32 = struct_access %.loc10_25.3, member0
+// CHECK:STDOUT:   %.loc10_25.5: i32 = bind_value %.loc10_25.4
+// CHECK:STDOUT:   %.loc10_25.6: ref i32 = struct_access %.loc10_25.3, member1
+// CHECK:STDOUT:   %.loc10_25.7: i32 = bind_value %.loc10_25.6
+// CHECK:STDOUT:   %.loc10_25.8: ref i32 = struct_access %.loc10_25.3, member2
+// CHECK:STDOUT:   %.loc10_25.9: i32 = bind_value %.loc10_25.8
+// CHECK:STDOUT:   %.loc10_25.10: {.x: i32, .y: i32, .z: i32} = struct_value %.loc10_25.3, (%.loc10_25.5, %.loc10_25.7, %.loc10_25.9)
+// CHECK:STDOUT:   %.loc10_35.3: {.a: i32, .b: {.x: i32, .y: i32, .z: i32}, .c: i32} = struct_value %.loc10_35.2, (%.loc10_16, %.loc10_25.10, %.loc10_34)
+// CHECK:STDOUT:   %.loc10_36: {.x: i32, .y: i32, .z: i32} = struct_access %.loc10_35.3, member1
+// CHECK:STDOUT:   %.loc10_38: i32 = struct_access %.loc10_36, member1
+// CHECK:STDOUT:   return %.loc10_38
+// CHECK:STDOUT: }

+ 7 - 7
toolchain/check/testdata/struct/member_access.carbon

@@ -13,13 +13,13 @@ var z: i32 = y;
 // CHECK:STDOUT:   %x: ref {.a: f64, .b: i32} = var "x"
 // CHECK:STDOUT:   %.loc7_35: f64 = real_literal 0e-1
 // CHECK:STDOUT:   %.loc7_45: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc7_46: {.a: f64, .b: i32} = struct_literal (%.loc7_35, %.loc7_45)
-// CHECK:STDOUT:   %.loc7_47.1: ref f64 = struct_access %x, member0
-// CHECK:STDOUT:   %.loc7_47.2: init f64 = initialize_from %.loc7_35 to %.loc7_47.1
-// CHECK:STDOUT:   %.loc7_47.3: ref i32 = struct_access %x, member1
-// CHECK:STDOUT:   %.loc7_47.4: init i32 = initialize_from %.loc7_45 to %.loc7_47.3
-// CHECK:STDOUT:   %.loc7_47.5: init {.a: f64, .b: i32} = struct_init %.loc7_46, (%.loc7_47.2, %.loc7_47.4)
-// CHECK:STDOUT:   assign %x, %.loc7_47.5
+// CHECK:STDOUT:   %.loc7_46.1: {.a: f64, .b: i32} = struct_literal (%.loc7_35, %.loc7_45)
+// CHECK:STDOUT:   %.loc7_46.2: ref f64 = struct_access %x, member0
+// CHECK:STDOUT:   %.loc7_46.3: init f64 = initialize_from %.loc7_35 to %.loc7_46.2
+// CHECK:STDOUT:   %.loc7_46.4: ref i32 = struct_access %x, member1
+// CHECK:STDOUT:   %.loc7_46.5: init i32 = initialize_from %.loc7_45 to %.loc7_46.4
+// CHECK:STDOUT:   %.loc7_46.6: init {.a: f64, .b: i32} = struct_init %.loc7_46.1, (%.loc7_46.3, %.loc7_46.5)
+// CHECK:STDOUT:   assign %x, %.loc7_46.6
 // CHECK:STDOUT:   %y: ref i32 = var "y"
 // CHECK:STDOUT:   %.loc8_15.1: ref i32 = struct_access %x, member1
 // CHECK:STDOUT:   %.loc8_15.2: i32 = bind_value %.loc8_15.1

+ 7 - 7
toolchain/check/testdata/struct/nested_struct_in_place.carbon

@@ -23,12 +23,12 @@ fn G() {
 // CHECK:STDOUT:   %.loc10_50: (type, type, type) = tuple_literal (i32, i32, i32)
 // CHECK:STDOUT:   %.loc10_51: type = struct_type {.a: (i32, i32, i32), .b: (i32, i32, i32)}
 // CHECK:STDOUT:   %v: ref {.a: (i32, i32, i32), .b: (i32, i32, i32)} = var "v"
-// CHECK:STDOUT:   %.loc10_75.1: ref (i32, i32, i32) = struct_access %v, member0
-// CHECK:STDOUT:   %.loc10_62: init (i32, i32, i32) = call @F() to %.loc10_75.1
-// CHECK:STDOUT:   %.loc10_75.2: ref (i32, i32, i32) = struct_access %v, member1
-// CHECK:STDOUT:   %.loc10_72: init (i32, i32, i32) = call @F() to %.loc10_75.2
-// CHECK:STDOUT:   %.loc10_74: {.a: (i32, i32, i32), .b: (i32, i32, i32)} = struct_literal (%.loc10_62, %.loc10_72)
-// CHECK:STDOUT:   %.loc10_75.3: init {.a: (i32, i32, i32), .b: (i32, i32, i32)} = struct_init %.loc10_74, (%.loc10_62, %.loc10_72)
-// CHECK:STDOUT:   assign %v, %.loc10_75.3
+// CHECK:STDOUT:   %.loc10_74.1: ref (i32, i32, i32) = struct_access %v, member0
+// CHECK:STDOUT:   %.loc10_62: init (i32, i32, i32) = call @F() to %.loc10_74.1
+// CHECK:STDOUT:   %.loc10_74.2: ref (i32, i32, i32) = struct_access %v, member1
+// CHECK:STDOUT:   %.loc10_72: init (i32, i32, i32) = call @F() to %.loc10_74.2
+// CHECK:STDOUT:   %.loc10_74.3: {.a: (i32, i32, i32), .b: (i32, i32, i32)} = struct_literal (%.loc10_62, %.loc10_72)
+// CHECK:STDOUT:   %.loc10_74.4: init {.a: (i32, i32, i32), .b: (i32, i32, i32)} = struct_init %.loc10_74.3, (%.loc10_62, %.loc10_72)
+// CHECK:STDOUT:   assign %v, %.loc10_74.4
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 7 - 5
toolchain/check/testdata/struct/one_entry.carbon

@@ -11,11 +11,13 @@ var y: {.a: i32} = x;
 // CHECK:STDOUT:   %.loc7_16: type = struct_type {.a: i32}
 // CHECK:STDOUT:   %x: ref {.a: i32} = var "x"
 // CHECK:STDOUT:   %.loc7_26: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc7_27: {.a: i32} = struct_literal (%.loc7_26)
-// CHECK:STDOUT:   %.loc7_28: init {.a: i32} = struct_init %.loc7_27, (%.loc7_26)
-// CHECK:STDOUT:   assign %x, %.loc7_28
+// CHECK:STDOUT:   %.loc7_27.1: {.a: i32} = struct_literal (%.loc7_26)
+// CHECK:STDOUT:   %.loc7_27.2: init {.a: i32} = struct_init %.loc7_27.1, (%.loc7_26)
+// CHECK:STDOUT:   assign %x, %.loc7_27.2
 // CHECK:STDOUT:   %.loc8: type = struct_type {.a: i32}
 // CHECK:STDOUT:   %y: ref {.a: i32} = var "y"
-// CHECK:STDOUT:   %.loc7_5: {.a: i32} = bind_value %x
-// CHECK:STDOUT:   assign %y, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = struct_access %x, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: init {.a: i32} = struct_init %x, (%.loc7_5.2)
+// CHECK:STDOUT:   assign %y, %.loc7_5.3
 // CHECK:STDOUT: }

+ 21 - 11
toolchain/check/testdata/struct/tuple_as_element.carbon

@@ -15,18 +15,28 @@ var y: {.a: i32, .b: (i32,)} = x;
 // CHECK:STDOUT:   %x: ref {.a: i32, .b: (i32,)} = var "x"
 // CHECK:STDOUT:   %.loc7_38: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc7_47: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc7_49: (i32,) = tuple_literal (%.loc7_47)
-// CHECK:STDOUT:   %.loc7_50: {.a: i32, .b: (i32,)} = struct_literal (%.loc7_38, %.loc7_49)
-// CHECK:STDOUT:   %.loc7_51.1: ref i32 = struct_access %x, member0
-// CHECK:STDOUT:   %.loc7_51.2: init i32 = initialize_from %.loc7_38 to %.loc7_51.1
-// CHECK:STDOUT:   %.loc7_51.3: init (i32,) = tuple_init %.loc7_49, (%.loc7_47)
-// CHECK:STDOUT:   %.loc7_51.4: ref (i32,) = struct_access %x, member1
-// CHECK:STDOUT:   %.loc7_51.5: init (i32,) = initialize_from %.loc7_51.3 to %.loc7_51.4
-// CHECK:STDOUT:   %.loc7_51.6: init {.a: i32, .b: (i32,)} = struct_init %.loc7_50, (%.loc7_51.2, %.loc7_51.5)
-// CHECK:STDOUT:   assign %x, %.loc7_51.6
+// CHECK:STDOUT:   %.loc7_49.1: (i32,) = tuple_literal (%.loc7_47)
+// CHECK:STDOUT:   %.loc7_50.1: {.a: i32, .b: (i32,)} = struct_literal (%.loc7_38, %.loc7_49.1)
+// CHECK:STDOUT:   %.loc7_50.2: ref i32 = struct_access %x, member0
+// CHECK:STDOUT:   %.loc7_50.3: init i32 = initialize_from %.loc7_38 to %.loc7_50.2
+// CHECK:STDOUT:   %.loc7_49.2: init (i32,) = tuple_init %.loc7_49.1, (%.loc7_47)
+// CHECK:STDOUT:   %.loc7_50.4: ref (i32,) = struct_access %x, member1
+// CHECK:STDOUT:   %.loc7_50.5: init (i32,) = initialize_from %.loc7_49.2 to %.loc7_50.4
+// CHECK:STDOUT:   %.loc7_50.6: init {.a: i32, .b: (i32,)} = struct_init %.loc7_50.1, (%.loc7_50.3, %.loc7_50.5)
+// CHECK:STDOUT:   assign %x, %.loc7_50.6
 // CHECK:STDOUT:   %.loc8_27: (type,) = tuple_literal (i32)
 // CHECK:STDOUT:   %.loc8_28: type = struct_type {.a: i32, .b: (i32,)}
 // CHECK:STDOUT:   %y: ref {.a: i32, .b: (i32,)} = var "y"
-// CHECK:STDOUT:   %.loc7_5: {.a: i32, .b: (i32,)} = bind_value %x
-// CHECK:STDOUT:   assign %y, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = struct_access %x, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: ref i32 = struct_access %y, member0
+// CHECK:STDOUT:   %.loc7_5.4: init i32 = initialize_from %.loc7_5.2 to %.loc7_5.3
+// CHECK:STDOUT:   %.loc7_5.5: ref (i32,) = struct_access %x, member1
+// CHECK:STDOUT:   %.loc7_5.6: ref i32 = tuple_access %.loc7_5.5, member0
+// CHECK:STDOUT:   %.loc7_5.7: i32 = bind_value %.loc7_5.6
+// CHECK:STDOUT:   %.loc7_5.8: init (i32,) = tuple_init %.loc7_5.5, (%.loc7_5.7)
+// CHECK:STDOUT:   %.loc7_5.9: ref (i32,) = struct_access %y, member1
+// CHECK:STDOUT:   %.loc7_5.10: init (i32,) = initialize_from %.loc7_5.8 to %.loc7_5.9
+// CHECK:STDOUT:   %.loc7_5.11: init {.a: i32, .b: (i32,)} = struct_init %x, (%.loc7_5.4, %.loc7_5.10)
+// CHECK:STDOUT:   assign %y, %.loc7_5.11
 // CHECK:STDOUT: }

+ 17 - 9
toolchain/check/testdata/struct/two_entries.carbon

@@ -12,15 +12,23 @@ var y: {.a: i32, .b: i32} = x;
 // CHECK:STDOUT:   %x: ref {.a: i32, .b: i32} = var "x"
 // CHECK:STDOUT:   %.loc7_35: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc7_43: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc7_44: {.a: i32, .b: i32} = struct_literal (%.loc7_35, %.loc7_43)
-// CHECK:STDOUT:   %.loc7_45.1: ref i32 = struct_access %x, member0
-// CHECK:STDOUT:   %.loc7_45.2: init i32 = initialize_from %.loc7_35 to %.loc7_45.1
-// CHECK:STDOUT:   %.loc7_45.3: ref i32 = struct_access %x, member1
-// CHECK:STDOUT:   %.loc7_45.4: init i32 = initialize_from %.loc7_43 to %.loc7_45.3
-// CHECK:STDOUT:   %.loc7_45.5: init {.a: i32, .b: i32} = struct_init %.loc7_44, (%.loc7_45.2, %.loc7_45.4)
-// CHECK:STDOUT:   assign %x, %.loc7_45.5
+// CHECK:STDOUT:   %.loc7_44.1: {.a: i32, .b: i32} = struct_literal (%.loc7_35, %.loc7_43)
+// CHECK:STDOUT:   %.loc7_44.2: ref i32 = struct_access %x, member0
+// CHECK:STDOUT:   %.loc7_44.3: init i32 = initialize_from %.loc7_35 to %.loc7_44.2
+// CHECK:STDOUT:   %.loc7_44.4: ref i32 = struct_access %x, member1
+// CHECK:STDOUT:   %.loc7_44.5: init i32 = initialize_from %.loc7_43 to %.loc7_44.4
+// CHECK:STDOUT:   %.loc7_44.6: init {.a: i32, .b: i32} = struct_init %.loc7_44.1, (%.loc7_44.3, %.loc7_44.5)
+// CHECK:STDOUT:   assign %x, %.loc7_44.6
 // CHECK:STDOUT:   %.loc8: type = struct_type {.a: i32, .b: i32}
 // CHECK:STDOUT:   %y: ref {.a: i32, .b: i32} = var "y"
-// CHECK:STDOUT:   %.loc7_5: {.a: i32, .b: i32} = bind_value %x
-// CHECK:STDOUT:   assign %y, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = struct_access %x, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: ref i32 = struct_access %y, member0
+// CHECK:STDOUT:   %.loc7_5.4: init i32 = initialize_from %.loc7_5.2 to %.loc7_5.3
+// CHECK:STDOUT:   %.loc7_5.5: ref i32 = struct_access %x, member1
+// CHECK:STDOUT:   %.loc7_5.6: i32 = bind_value %.loc7_5.5
+// CHECK:STDOUT:   %.loc7_5.7: ref i32 = struct_access %y, member1
+// CHECK:STDOUT:   %.loc7_5.8: init i32 = initialize_from %.loc7_5.6 to %.loc7_5.7
+// CHECK:STDOUT:   %.loc7_5.9: init {.a: i32, .b: i32} = struct_init %x, (%.loc7_5.4, %.loc7_5.8)
+// CHECK:STDOUT:   assign %y, %.loc7_5.9
 // CHECK:STDOUT: }

+ 4 - 4
toolchain/check/testdata/tuples/empty.carbon

@@ -11,11 +11,11 @@ var y: () = x;
 // CHECK:STDOUT:   %.loc7_9.1: type = tuple_type ()
 // CHECK:STDOUT:   %.loc7_9.2: () = tuple_literal ()
 // CHECK:STDOUT:   %x: ref () = var "x"
-// CHECK:STDOUT:   %.loc7_14: () = tuple_literal ()
-// CHECK:STDOUT:   %.loc7_15: init () = tuple_init %.loc7_14, ()
-// CHECK:STDOUT:   assign %x, %.loc7_15
+// CHECK:STDOUT:   %.loc7_14.1: () = tuple_literal ()
+// CHECK:STDOUT:   %.loc7_14.2: init () = tuple_init %.loc7_14.1, ()
+// CHECK:STDOUT:   assign %x, %.loc7_14.2
 // CHECK:STDOUT:   %.loc8: () = tuple_literal ()
 // CHECK:STDOUT:   %y: ref () = var "y"
-// CHECK:STDOUT:   %.loc7_5: () = bind_value %x
+// CHECK:STDOUT:   %.loc7_5: init () = tuple_init %x, ()
 // CHECK:STDOUT:   assign %y, %.loc7_5
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/tuples/fail_assign_empty.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_empty.carbon:[[@LINE+3]]:19: ERROR: Cannot implicitly convert from `() as type` to `(i32,) as type`.
+// CHECK:STDERR: fail_assign_empty.carbon:[[@LINE+3]]:18: ERROR: Cannot initialize tuple of 1 element(s) from tuple with 0 element(s).
 // CHECK:STDERR: var x: (i32,) = ();
-// CHECK:STDERR:                   ^
+// CHECK:STDERR:                  ^
 var x: (i32,) = ();
 
 // CHECK:STDOUT: file "fail_assign_empty.carbon" {

+ 4 - 4
toolchain/check/testdata/tuples/fail_assign_nested.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_assign_nested.carbon:[[@LINE+3]]:57: ERROR: Cannot implicitly convert from `((i32, i32, i32), (i32, i32, i32)) as type` to `((i32, i32), (i32, i32)) as type`.
+// CHECK:STDERR: fail_assign_nested.carbon:[[@LINE+3]]:44: ERROR: Cannot initialize tuple of 2 element(s) from tuple with 3 element(s).
 // CHECK:STDERR: var x: ((i32, i32), (i32, i32)) = ((1, 2, 3), (4, 5, 6));
-// CHECK:STDERR:                                                         ^
+// CHECK:STDERR:                                            ^
 var x: ((i32, i32), (i32, i32)) = ((1, 2, 3), (4, 5, 6));
 
 // CHECK:STDOUT: file "fail_assign_nested.carbon" {
@@ -15,8 +15,8 @@ var x: ((i32, i32), (i32, i32)) = ((1, 2, 3), (4, 5, 6));
 // CHECK:STDOUT:   %.loc10_30: (type, type) = tuple_literal (i32, i32)
 // CHECK:STDOUT:   %.loc10_31.1: type = tuple_type ((type, type), (type, type))
 // CHECK:STDOUT:   %.loc10_31.2: ((type, type), (type, type)) = tuple_literal (%.loc10_18.2, %.loc10_30)
-// CHECK:STDOUT:   %.loc10_18.3: type = tuple_type (i32, i32)
-// CHECK:STDOUT:   %.loc10_31.3: type = tuple_type ((i32, i32), (i32, i32))
+// CHECK:STDOUT:   %.loc10_31.3: type = tuple_type (i32, i32)
+// CHECK:STDOUT:   %.loc10_31.4: type = tuple_type ((i32, i32), (i32, i32))
 // CHECK:STDOUT:   %x: ref ((i32, i32), (i32, i32)) = var "x"
 // CHECK:STDOUT:   %.loc10_37: i32 = int_literal 1
 // CHECK:STDOUT:   %.loc10_40: i32 = int_literal 2

+ 4 - 2
toolchain/check/testdata/tuples/fail_element_type_mismatch.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_element_type_mismatch.carbon:[[@LINE+3]]:31: ERROR: Cannot implicitly convert from `(i32, f64) as type` to `(i32, i32) as type`.
+// CHECK:STDERR: fail_element_type_mismatch.carbon:[[@LINE+3]]:30: ERROR: Cannot implicitly convert from `f64` to `i32`.
 // CHECK:STDERR: var x: (i32, i32) = (2, 65.89);
-// CHECK:STDERR:                               ^
+// CHECK:STDERR:                              ^
 var x: (i32, i32) = (2, 65.89);
 
 // CHECK:STDOUT: file "fail_element_type_mismatch.carbon" {
@@ -18,5 +18,7 @@ var x: (i32, i32) = (2, 65.89);
 // CHECK:STDOUT:   %.loc10_25: f64 = real_literal 6589e-2
 // CHECK:STDOUT:   %.loc10_30.1: type = tuple_type (i32, f64)
 // CHECK:STDOUT:   %.loc10_30.2: (i32, f64) = tuple_literal (%.loc10_22, %.loc10_25)
+// CHECK:STDOUT:   %.loc10_30.3: ref i32 = tuple_access %x, member0
+// CHECK:STDOUT:   %.loc10_30.4: init i32 = initialize_from %.loc10_22 to %.loc10_30.3
 // CHECK:STDOUT:   assign %x, <error>
 // CHECK:STDOUT: }

+ 2 - 2
toolchain/check/testdata/tuples/fail_too_few_element.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_too_few_element.carbon:[[@LINE+3]]:26: ERROR: Cannot implicitly convert from `(i32,) as type` to `(i32, i32) as type`.
+// CHECK:STDERR: fail_too_few_element.carbon:[[@LINE+3]]:25: ERROR: Cannot initialize tuple of 2 element(s) from tuple with 1 element(s).
 // CHECK:STDERR: var x: (i32, i32) = (2, );
-// CHECK:STDERR:                          ^
+// CHECK:STDERR:                         ^
 var x: (i32, i32) = (2, );
 
 // CHECK:STDOUT: file "fail_too_few_element.carbon" {

+ 2 - 2
toolchain/check/testdata/tuples/fail_type_assign.carbon

@@ -4,9 +4,9 @@
 //
 // AUTOUPDATE
 
-// CHECK:STDERR: fail_type_assign.carbon:[[@LINE+3]]:25: ERROR: Cannot implicitly convert from `(type,) as type` to `(i32,) as type`.
+// CHECK:STDERR: fail_type_assign.carbon:[[@LINE+3]]:24: ERROR: Cannot implicitly convert from `type` to `i32`.
 // CHECK:STDERR: var x: (i32, ) = (i32, );
-// CHECK:STDERR:                         ^
+// CHECK:STDERR:                        ^
 var x: (i32, ) = (i32, );
 
 // CHECK:STDOUT: file "fail_type_assign.carbon" {

+ 14 - 14
toolchain/check/testdata/tuples/nested_tuple.carbon

@@ -11,22 +11,22 @@ var x: ((i32, i32), i32) = ((12, 76), 6);
 // CHECK:STDOUT:   %.loc7_18.2: (type, type) = tuple_literal (i32, i32)
 // CHECK:STDOUT:   %.loc7_24.1: type = tuple_type ((type, type), type)
 // CHECK:STDOUT:   %.loc7_24.2: ((type, type), type) = tuple_literal (%.loc7_18.2, i32)
-// CHECK:STDOUT:   %.loc7_18.3: type = tuple_type (i32, i32)
-// CHECK:STDOUT:   %.loc7_24.3: type = tuple_type ((i32, i32), i32)
+// CHECK:STDOUT:   %.loc7_24.3: type = tuple_type (i32, i32)
+// CHECK:STDOUT:   %.loc7_24.4: type = tuple_type ((i32, i32), i32)
 // CHECK:STDOUT:   %x: ref ((i32, i32), i32) = var "x"
 // CHECK:STDOUT:   %.loc7_30: i32 = int_literal 12
 // CHECK:STDOUT:   %.loc7_34: i32 = int_literal 76
-// CHECK:STDOUT:   %.loc7_36: (i32, i32) = tuple_literal (%.loc7_30, %.loc7_34)
+// CHECK:STDOUT:   %.loc7_36.1: (i32, i32) = tuple_literal (%.loc7_30, %.loc7_34)
 // CHECK:STDOUT:   %.loc7_39: i32 = int_literal 6
-// CHECK:STDOUT:   %.loc7_40: ((i32, i32), i32) = tuple_literal (%.loc7_36, %.loc7_39)
-// CHECK:STDOUT:   %.loc7_41.1: ref (i32, i32) = tuple_access %x, member0
-// CHECK:STDOUT:   %.loc7_41.2: ref i32 = tuple_access %.loc7_41.1, member0
-// CHECK:STDOUT:   %.loc7_41.3: init i32 = initialize_from %.loc7_30 to %.loc7_41.2
-// CHECK:STDOUT:   %.loc7_41.4: ref i32 = tuple_access %.loc7_41.1, member1
-// CHECK:STDOUT:   %.loc7_41.5: init i32 = initialize_from %.loc7_34 to %.loc7_41.4
-// CHECK:STDOUT:   %.loc7_41.6: init (i32, i32) = tuple_init %.loc7_36, (%.loc7_41.3, %.loc7_41.5)
-// CHECK:STDOUT:   %.loc7_41.7: ref i32 = tuple_access %x, member1
-// CHECK:STDOUT:   %.loc7_41.8: init i32 = initialize_from %.loc7_39 to %.loc7_41.7
-// CHECK:STDOUT:   %.loc7_41.9: init ((i32, i32), i32) = tuple_init %.loc7_40, (%.loc7_41.6, %.loc7_41.8)
-// CHECK:STDOUT:   assign %x, %.loc7_41.9
+// CHECK:STDOUT:   %.loc7_40.1: ((i32, i32), i32) = tuple_literal (%.loc7_36.1, %.loc7_39)
+// CHECK:STDOUT:   %.loc7_40.2: ref (i32, i32) = tuple_access %x, member0
+// CHECK:STDOUT:   %.loc7_36.2: ref i32 = tuple_access %.loc7_40.2, member0
+// CHECK:STDOUT:   %.loc7_36.3: init i32 = initialize_from %.loc7_30 to %.loc7_36.2
+// CHECK:STDOUT:   %.loc7_36.4: ref i32 = tuple_access %.loc7_40.2, member1
+// CHECK:STDOUT:   %.loc7_36.5: init i32 = initialize_from %.loc7_34 to %.loc7_36.4
+// CHECK:STDOUT:   %.loc7_36.6: init (i32, i32) = tuple_init %.loc7_36.1, (%.loc7_36.3, %.loc7_36.5)
+// CHECK:STDOUT:   %.loc7_40.3: ref i32 = tuple_access %x, member1
+// CHECK:STDOUT:   %.loc7_40.4: init i32 = initialize_from %.loc7_39 to %.loc7_40.3
+// CHECK:STDOUT:   %.loc7_40.5: init ((i32, i32), i32) = tuple_init %.loc7_40.1, (%.loc7_36.6, %.loc7_40.4)
+// CHECK:STDOUT:   assign %x, %.loc7_40.5
 // CHECK:STDOUT: }

+ 16 - 16
toolchain/check/testdata/tuples/nested_tuple_in_place.carbon

@@ -30,13 +30,13 @@ fn H() {
 // CHECK:STDOUT:   %.loc10_43.2: ((type, type, type), (type, type, type)) = tuple_literal (%.loc10_25, %.loc10_42)
 // CHECK:STDOUT:   %.loc10_43.3: type = tuple_type ((i32, i32, i32), (i32, i32, i32))
 // CHECK:STDOUT:   %v: ref ((i32, i32, i32), (i32, i32, i32)) = var "v"
-// CHECK:STDOUT:   %.loc10_57.1: ref (i32, i32, i32) = tuple_access %v, member0
-// CHECK:STDOUT:   %.loc10_49: init (i32, i32, i32) = call @F() to %.loc10_57.1
-// CHECK:STDOUT:   %.loc10_57.2: ref (i32, i32, i32) = tuple_access %v, member1
-// CHECK:STDOUT:   %.loc10_54: init (i32, i32, i32) = call @F() to %.loc10_57.2
-// CHECK:STDOUT:   %.loc10_56: ((i32, i32, i32), (i32, i32, i32)) = tuple_literal (%.loc10_49, %.loc10_54)
-// CHECK:STDOUT:   %.loc10_57.3: init ((i32, i32, i32), (i32, i32, i32)) = tuple_init %.loc10_56, (%.loc10_49, %.loc10_54)
-// CHECK:STDOUT:   assign %v, %.loc10_57.3
+// CHECK:STDOUT:   %.loc10_56.1: ref (i32, i32, i32) = tuple_access %v, member0
+// CHECK:STDOUT:   %.loc10_49: init (i32, i32, i32) = call @F() to %.loc10_56.1
+// CHECK:STDOUT:   %.loc10_56.2: ref (i32, i32, i32) = tuple_access %v, member1
+// CHECK:STDOUT:   %.loc10_54: init (i32, i32, i32) = call @F() to %.loc10_56.2
+// CHECK:STDOUT:   %.loc10_56.3: ((i32, i32, i32), (i32, i32, i32)) = tuple_literal (%.loc10_49, %.loc10_54)
+// CHECK:STDOUT:   %.loc10_56.4: init ((i32, i32, i32), (i32, i32, i32)) = tuple_init %.loc10_56.3, (%.loc10_49, %.loc10_54)
+// CHECK:STDOUT:   assign %v, %.loc10_56.4
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -48,15 +48,15 @@ fn H() {
 // CHECK:STDOUT:   %.loc14_36.3: type = tuple_type (i32, (i32, i32, i32), i32)
 // CHECK:STDOUT:   %v: ref (i32, (i32, i32, i32), i32) = var "v"
 // CHECK:STDOUT:   %.loc14_41: i32 = int_literal 1
-// CHECK:STDOUT:   %.loc14_51.1: ref (i32, i32, i32) = tuple_access %v, member1
-// CHECK:STDOUT:   %.loc14_45: init (i32, i32, i32) = call @F() to %.loc14_51.1
+// CHECK:STDOUT:   %.loc14_50.1: ref (i32, i32, i32) = tuple_access %v, member1
+// CHECK:STDOUT:   %.loc14_45: init (i32, i32, i32) = call @F() to %.loc14_50.1
 // CHECK:STDOUT:   %.loc14_49: i32 = int_literal 2
-// CHECK:STDOUT:   %.loc14_50: (i32, (i32, i32, i32), i32) = tuple_literal (%.loc14_41, %.loc14_45, %.loc14_49)
-// CHECK:STDOUT:   %.loc14_51.2: ref i32 = tuple_access %v, member0
-// CHECK:STDOUT:   %.loc14_51.3: init i32 = initialize_from %.loc14_41 to %.loc14_51.2
-// CHECK:STDOUT:   %.loc14_51.4: ref i32 = tuple_access %v, member2
-// CHECK:STDOUT:   %.loc14_51.5: init i32 = initialize_from %.loc14_49 to %.loc14_51.4
-// CHECK:STDOUT:   %.loc14_51.6: init (i32, (i32, i32, i32), i32) = tuple_init %.loc14_50, (%.loc14_51.3, %.loc14_45, %.loc14_51.5)
-// CHECK:STDOUT:   assign %v, %.loc14_51.6
+// CHECK:STDOUT:   %.loc14_50.2: (i32, (i32, i32, i32), i32) = tuple_literal (%.loc14_41, %.loc14_45, %.loc14_49)
+// CHECK:STDOUT:   %.loc14_50.3: ref i32 = tuple_access %v, member0
+// CHECK:STDOUT:   %.loc14_50.4: init i32 = initialize_from %.loc14_41 to %.loc14_50.3
+// CHECK:STDOUT:   %.loc14_50.5: ref i32 = tuple_access %v, member2
+// CHECK:STDOUT:   %.loc14_50.6: init i32 = initialize_from %.loc14_49 to %.loc14_50.5
+// CHECK:STDOUT:   %.loc14_50.7: init (i32, (i32, i32, i32), i32) = tuple_init %.loc14_50.2, (%.loc14_50.4, %.loc14_45, %.loc14_50.6)
+// CHECK:STDOUT:   assign %v, %.loc14_50.7
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }

+ 7 - 5
toolchain/check/testdata/tuples/one_element.carbon

@@ -13,11 +13,13 @@ var y: (i32,) = x;
 // CHECK:STDOUT:   %.loc7_13.3: type = tuple_type (i32)
 // CHECK:STDOUT:   %x: ref (i32,) = var "x"
 // CHECK:STDOUT:   %.loc7_18: i32 = int_literal 4
-// CHECK:STDOUT:   %.loc7_20: (i32,) = tuple_literal (%.loc7_18)
-// CHECK:STDOUT:   %.loc7_21: init (i32,) = tuple_init %.loc7_20, (%.loc7_18)
-// CHECK:STDOUT:   assign %x, %.loc7_21
+// CHECK:STDOUT:   %.loc7_20.1: (i32,) = tuple_literal (%.loc7_18)
+// CHECK:STDOUT:   %.loc7_20.2: init (i32,) = tuple_init %.loc7_20.1, (%.loc7_18)
+// CHECK:STDOUT:   assign %x, %.loc7_20.2
 // CHECK:STDOUT:   %.loc8: (type,) = tuple_literal (i32)
 // CHECK:STDOUT:   %y: ref (i32,) = var "y"
-// CHECK:STDOUT:   %.loc7_5: (i32,) = bind_value %x
-// CHECK:STDOUT:   assign %y, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = tuple_access %x, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: init (i32,) = tuple_init %x, (%.loc7_5.2)
+// CHECK:STDOUT:   assign %y, %.loc7_5.3
 // CHECK:STDOUT: }

+ 17 - 9
toolchain/check/testdata/tuples/two_elements.carbon

@@ -14,15 +14,23 @@ var y: (i32, i32) = x;
 // CHECK:STDOUT:   %x: ref (i32, i32) = var "x"
 // CHECK:STDOUT:   %.loc7_22: i32 = int_literal 4
 // CHECK:STDOUT:   %.loc7_25: i32 = int_literal 102
-// CHECK:STDOUT:   %.loc7_28: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_25)
-// CHECK:STDOUT:   %.loc7_29.1: ref i32 = tuple_access %x, member0
-// CHECK:STDOUT:   %.loc7_29.2: init i32 = initialize_from %.loc7_22 to %.loc7_29.1
-// CHECK:STDOUT:   %.loc7_29.3: ref i32 = tuple_access %x, member1
-// CHECK:STDOUT:   %.loc7_29.4: init i32 = initialize_from %.loc7_25 to %.loc7_29.3
-// CHECK:STDOUT:   %.loc7_29.5: init (i32, i32) = tuple_init %.loc7_28, (%.loc7_29.2, %.loc7_29.4)
-// CHECK:STDOUT:   assign %x, %.loc7_29.5
+// CHECK:STDOUT:   %.loc7_28.1: (i32, i32) = tuple_literal (%.loc7_22, %.loc7_25)
+// CHECK:STDOUT:   %.loc7_28.2: ref i32 = tuple_access %x, member0
+// CHECK:STDOUT:   %.loc7_28.3: init i32 = initialize_from %.loc7_22 to %.loc7_28.2
+// CHECK:STDOUT:   %.loc7_28.4: ref i32 = tuple_access %x, member1
+// CHECK:STDOUT:   %.loc7_28.5: init i32 = initialize_from %.loc7_25 to %.loc7_28.4
+// CHECK:STDOUT:   %.loc7_28.6: init (i32, i32) = tuple_init %.loc7_28.1, (%.loc7_28.3, %.loc7_28.5)
+// CHECK:STDOUT:   assign %x, %.loc7_28.6
 // CHECK:STDOUT:   %.loc8: (type, type) = tuple_literal (i32, i32)
 // CHECK:STDOUT:   %y: ref (i32, i32) = var "y"
-// CHECK:STDOUT:   %.loc7_5: (i32, i32) = bind_value %x
-// CHECK:STDOUT:   assign %y, %.loc7_5
+// CHECK:STDOUT:   %.loc7_5.1: ref i32 = tuple_access %x, member0
+// CHECK:STDOUT:   %.loc7_5.2: i32 = bind_value %.loc7_5.1
+// CHECK:STDOUT:   %.loc7_5.3: ref i32 = tuple_access %y, member0
+// CHECK:STDOUT:   %.loc7_5.4: init i32 = initialize_from %.loc7_5.2 to %.loc7_5.3
+// CHECK:STDOUT:   %.loc7_5.5: ref i32 = tuple_access %x, member1
+// CHECK:STDOUT:   %.loc7_5.6: i32 = bind_value %.loc7_5.5
+// CHECK:STDOUT:   %.loc7_5.7: ref i32 = tuple_access %y, member1
+// CHECK:STDOUT:   %.loc7_5.8: init i32 = initialize_from %.loc7_5.6 to %.loc7_5.7
+// CHECK:STDOUT:   %.loc7_5.9: init (i32, i32) = tuple_init %x, (%.loc7_5.4, %.loc7_5.8)
+// CHECK:STDOUT:   assign %y, %.loc7_5.9
 // CHECK:STDOUT: }

+ 3 - 0
toolchain/diagnostics/diagnostic_kind.def

@@ -125,7 +125,10 @@ CARBON_DIAGNOSTIC_KIND(RepeatedConst)
 CARBON_DIAGNOSTIC_KIND(InvalidArrayExpression)
 CARBON_DIAGNOSTIC_KIND(TypeNotIndexable)
 CARBON_DIAGNOSTIC_KIND(IndexOutOfBounds)
+CARBON_DIAGNOSTIC_KIND(StructInitElementCountMismatch)
+CARBON_DIAGNOSTIC_KIND(StructInitFieldNameMismatch)
 CARBON_DIAGNOSTIC_KIND(TupleIndexIntegerLiteral)
+CARBON_DIAGNOSTIC_KIND(TupleInitElementCountMismatch)
 CARBON_DIAGNOSTIC_KIND(ReturnStatementDisallowExpression)
 CARBON_DIAGNOSTIC_KIND(ReturnStatementImplicitNote)
 CARBON_DIAGNOSTIC_KIND(ReturnStatementMissingExpression)

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

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

+ 8 - 0
toolchain/lower/testdata/struct/two_entries.carbon

@@ -20,5 +20,13 @@ fn Run() -> i32 {
 // CHECK:STDOUT:   %b = getelementptr inbounds { i32, i32 }, ptr %x, i32 0, i32 1
 // CHECK:STDOUT:   store i32 2, ptr %b, align 4
 // CHECK:STDOUT:   %y = alloca { i32, i32 }, align 8
+// CHECK:STDOUT:   %a1 = getelementptr inbounds { i32, i32 }, ptr %x, i32 0, i32 0
+// CHECK:STDOUT:   %1 = load i32, ptr %a1, align 4
+// CHECK:STDOUT:   %a2 = getelementptr inbounds { i32, i32 }, ptr %y, i32 0, i32 0
+// CHECK:STDOUT:   store i32 %1, ptr %a2, align 4
+// CHECK:STDOUT:   %b3 = getelementptr inbounds { i32, i32 }, ptr %x, i32 0, i32 1
+// CHECK:STDOUT:   %2 = load i32, ptr %b3, align 4
+// CHECK:STDOUT:   %b4 = getelementptr inbounds { i32, i32 }, ptr %y, i32 0, i32 1
+// CHECK:STDOUT:   store i32 %2, ptr %b4, align 4
 // CHECK:STDOUT:   ret i32 0
 // CHECK:STDOUT: }

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

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

+ 8 - 0
toolchain/lower/testdata/tuple/two_entries.carbon

@@ -20,5 +20,13 @@ fn Run() -> i32 {
 // CHECK:STDOUT:   %tuple.elem1 = getelementptr inbounds { i32, i32 }, ptr %x, i32 0, i32 1
 // CHECK:STDOUT:   store i32 7, ptr %tuple.elem1, align 4
 // CHECK:STDOUT:   %y = alloca { i32, i32 }, align 8
+// CHECK:STDOUT:   %tuple.elem2 = getelementptr inbounds { i32, i32 }, ptr %x, i32 0, i32 0
+// CHECK:STDOUT:   %1 = load i32, ptr %tuple.elem2, align 4
+// CHECK:STDOUT:   %tuple.elem3 = getelementptr inbounds { i32, i32 }, ptr %y, i32 0, i32 0
+// CHECK:STDOUT:   store i32 %1, ptr %tuple.elem3, align 4
+// CHECK:STDOUT:   %tuple.elem4 = getelementptr inbounds { i32, i32 }, ptr %x, i32 0, i32 1
+// CHECK:STDOUT:   %2 = load i32, ptr %tuple.elem4, align 4
+// CHECK:STDOUT:   %tuple.elem5 = getelementptr inbounds { i32, i32 }, ptr %y, i32 0, i32 1
+// CHECK:STDOUT:   store i32 %2, ptr %tuple.elem5, align 4
 // CHECK:STDOUT:   ret i32 0
 // CHECK:STDOUT: }

+ 56 - 0
toolchain/lower/testdata/tuple/value_formation.carbon

@@ -0,0 +1,56 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn G(ab: ((i32, i32, i32), (i32, i32, i32)));
+
+fn F() {
+  var a: (i32, i32, i32);
+  var b: (i32, i32, i32);
+  G((a, b));
+}
+
+// CHECK:STDOUT: ; ModuleID = 'value_formation.carbon'
+// CHECK:STDOUT: source_filename = "value_formation.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @G(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @F() {
+// CHECK:STDOUT:   %a = alloca { i32, i32, i32 }, align 8
+// CHECK:STDOUT:   %b = alloca { i32, i32, i32 }, align 8
+// CHECK:STDOUT:   %tuple.elem = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 0
+// CHECK:STDOUT:   %1 = load i32, ptr %tuple.elem, align 4
+// CHECK:STDOUT:   %tuple.elem1 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 1
+// CHECK:STDOUT:   %2 = load i32, ptr %tuple.elem1, align 4
+// CHECK:STDOUT:   %tuple.elem2 = getelementptr inbounds { i32, i32, i32 }, ptr %a, i32 0, i32 2
+// CHECK:STDOUT:   %3 = load i32, ptr %tuple.elem2, align 4
+// CHECK:STDOUT:   %tuple = alloca { i32, i32, i32 }, align 8
+// CHECK:STDOUT:   %4 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple, i32 0, i32 0
+// CHECK:STDOUT:   store i32 %1, ptr %4, align 4
+// CHECK:STDOUT:   %5 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple, i32 0, i32 1
+// CHECK:STDOUT:   store i32 %2, ptr %5, align 4
+// CHECK:STDOUT:   %6 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple, i32 0, i32 2
+// CHECK:STDOUT:   store i32 %3, ptr %6, align 4
+// CHECK:STDOUT:   %tuple.elem3 = getelementptr inbounds { i32, i32, i32 }, ptr %b, i32 0, i32 0
+// CHECK:STDOUT:   %7 = load i32, ptr %tuple.elem3, align 4
+// CHECK:STDOUT:   %tuple.elem4 = getelementptr inbounds { i32, i32, i32 }, ptr %b, i32 0, i32 1
+// CHECK:STDOUT:   %8 = load i32, ptr %tuple.elem4, align 4
+// CHECK:STDOUT:   %tuple.elem5 = getelementptr inbounds { i32, i32, i32 }, ptr %b, i32 0, i32 2
+// CHECK:STDOUT:   %9 = load i32, ptr %tuple.elem5, align 4
+// CHECK:STDOUT:   %tuple6 = alloca { i32, i32, i32 }, align 8
+// CHECK:STDOUT:   %10 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple6, i32 0, i32 0
+// CHECK:STDOUT:   store i32 %7, ptr %10, align 4
+// CHECK:STDOUT:   %11 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple6, i32 0, i32 1
+// CHECK:STDOUT:   store i32 %8, ptr %11, align 4
+// CHECK:STDOUT:   %12 = getelementptr inbounds { i32, i32, i32 }, ptr %tuple6, i32 0, i32 2
+// CHECK:STDOUT:   store i32 %9, ptr %12, align 4
+// CHECK:STDOUT:   %tuple7 = alloca { { i32, i32, i32 }, { i32, i32, i32 } }, align 8
+// CHECK:STDOUT:   %13 = getelementptr inbounds { { i32, i32, i32 }, { i32, i32, i32 } }, ptr %tuple7, i32 0, i32 0
+// CHECK:STDOUT:   store ptr %tuple, ptr %13, align 8
+// CHECK:STDOUT:   %14 = getelementptr inbounds { { i32, i32, i32 }, { i32, i32, i32 } }, ptr %tuple7, i32 0, i32 1
+// CHECK:STDOUT:   store ptr %tuple6, ptr %14, align 8
+// CHECK:STDOUT:   call void @G(ptr %tuple7)
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }

+ 26 - 0
toolchain/lower/testdata/tuple/value_forwarding.carbon

@@ -0,0 +1,26 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// AUTOUPDATE
+
+fn G(ab: ((i32, i32, i32), (i32, i32, i32)));
+
+fn F(a: (i32, i32, i32), b: (i32, i32, i32)) {
+  G((a, b));
+}
+
+// CHECK:STDOUT: ; ModuleID = 'value_forwarding.carbon'
+// CHECK:STDOUT: source_filename = "value_forwarding.carbon"
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @G(ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @F(ptr %a, ptr %b) {
+// CHECK:STDOUT:   %tuple = alloca { { i32, i32, i32 }, { i32, i32, i32 } }, align 8
+// CHECK:STDOUT:   %1 = getelementptr inbounds { { i32, i32, i32 }, { i32, i32, i32 } }, ptr %tuple, i32 0, i32 0
+// CHECK:STDOUT:   store ptr %a, ptr %1, align 8
+// CHECK:STDOUT:   %2 = getelementptr inbounds { { i32, i32, i32 }, { i32, i32, i32 } }, ptr %tuple, i32 0, i32 1
+// CHECK:STDOUT:   store ptr %b, ptr %2, align 8
+// CHECK:STDOUT:   call void @G(ptr %tuple)
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }

+ 21 - 6
toolchain/sem_ir/file.h

@@ -192,6 +192,13 @@ class File : public Printable<File> {
     return id;
   }
 
+  // Adds a node block of the given size.
+  auto AddUninitializedNodeBlock(size_t size) -> NodeBlockId {
+    NodeBlockId id(node_blocks_.size());
+    node_blocks_.push_back(AllocateUninitialized<NodeId>(size));
+    return id;
+  }
+
   // Returns the requested node block.
   auto GetNodeBlock(NodeBlockId block_id) const -> llvm::ArrayRef<NodeId> {
     CARBON_CHECK(block_id != NodeBlockId::Unreachable);
@@ -305,16 +312,23 @@ class File : public Printable<File> {
   auto filename() const -> llvm::StringRef { return filename_; }
 
  private:
-  // Allocates a copy of the given data using our slab allocator.
+  // Allocates an uninitialized array using our slab allocator.
   template <typename T>
-  auto AllocateCopy(llvm::ArrayRef<T> data) -> llvm::MutableArrayRef<T> {
+  auto AllocateUninitialized(std::size_t size) -> llvm::MutableArrayRef<T> {
     // We're not going to run a destructor, so ensure that's OK.
     static_assert(std::is_trivially_destructible_v<T>);
 
-    T* storage = static_cast<T*>(
-        allocator_.Allocate(data.size() * sizeof(T), alignof(T)));
-    std::uninitialized_copy(data.begin(), data.end(), storage);
-    return llvm::MutableArrayRef<T>(storage, data.size());
+    T* storage =
+        static_cast<T*>(allocator_.Allocate(size * sizeof(T), alignof(T)));
+    return llvm::MutableArrayRef<T>(storage, size);
+  }
+
+  // Allocates a copy of the given data using our slab allocator.
+  template <typename T>
+  auto AllocateCopy(llvm::ArrayRef<T> data) -> llvm::MutableArrayRef<T> {
+    auto result = AllocateUninitialized<T>(data.size());
+    std::uninitialized_copy(data.begin(), data.end(), result.begin());
+    return result;
   }
 
   bool has_errors_ = false;
@@ -390,6 +404,7 @@ enum class ExpressionCategory : int8_t {
   // and struct literals, where the subexpressions for different elements can
   // have different categories.
   Mixed,
+  Last = Mixed
 };
 
 // Returns the expression category for a node.