Prechádzať zdrojové kódy

Fix initialization of var parameters. (#7023)

When an initializing expression is used to initialize a var parameter,
we need to create the storage earlier in SemIR than the initializing
expression. To do so, pass a pending block to initialization containing
the var storage.

Also stop using `temporary` for this purpose, since we treat temporaries
as potentially-constant and immutable, but `var` parameters can be
mutated by the callee. We should ideally introduce a new kind of
instruction for this purpose but for now we just use `var_storage`.
Richard Smith 4 dní pred
rodič
commit
23339bc810

+ 44 - 5
toolchain/check/convert.cpp

@@ -24,6 +24,7 @@
 #include "toolchain/check/member_access.h"
 #include "toolchain/check/operator.h"
 #include "toolchain/check/pattern_match.h"
+#include "toolchain/check/pending_block.h"
 #include "toolchain/check/type.h"
 #include "toolchain/check/type_completion.h"
 #include "toolchain/diagnostics/emitter.h"
@@ -2048,16 +2049,24 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
   return expr_id;
 }
 
-auto Initialize(Context& context, SemIR::LocId loc_id, SemIR::InstId storage_id,
-                SemIR::InstId value_id, bool for_return) -> SemIR::InstId {
+auto InitializeExisting(Context& context, SemIR::LocId loc_id,
+                        SemIR::InstId storage_id, SemIR::InstId value_id,
+                        bool for_return) -> SemIR::InstId {
   auto type_id = context.insts().Get(storage_id).type_id();
   if (for_return &&
       !SemIR::InitRepr::ForType(context.sem_ir(), type_id).MightBeInPlace()) {
-    // TODO: is it safe to use storage_id when the init repr is dependent?
+    // TODO: Is it safe to use storage_id when the init repr is dependent?
     storage_id = SemIR::InstId::None;
   }
-  // TODO: add CHECK that storage_id.index < value_id.index to enforce the
-  // precondition, once existing violations have been cleaned up.
+
+  // TODO: This is only an approximation of a dominance check. Add a general
+  // end-of-phase dominance check and remove the check here and the one in
+  // `MergeReplacing`.
+  CARBON_CHECK(!storage_id.has_value() ||
+                   value_id == SemIR::ErrorInst::InstId ||
+                   context.insts().GetRawIndex(storage_id) <=
+                       context.insts().GetRawIndex(value_id),
+               "Storage might not dominate initializer");
   PendingBlock target_block(&context);
   return Convert(context, loc_id, value_id,
                  {.kind = ConversionTarget::Initializing,
@@ -2066,6 +2075,36 @@ auto Initialize(Context& context, SemIR::LocId loc_id, SemIR::InstId storage_id,
                   .storage_access_block = &target_block});
 }
 
+auto Initialize(Context& context, SemIR::LocId loc_id,
+                SemIR::InstId&& storage_id, PendingBlock&& storage_access_block,
+                SemIR::InstId value_id) -> InitializeResult {
+  CARBON_CHECK(storage_id.has_value());
+  auto type_id = context.insts().Get(storage_id).type_id();
+  auto result_id = Convert(context, loc_id, value_id,
+                           {.kind = ConversionTarget::Initializing,
+                            .type_id = type_id,
+                            .storage_id = storage_id,
+                            .storage_access_block = &storage_access_block});
+
+  // Insert the storage block now, in case it wasn't used by the initializer.
+  storage_access_block.InsertHere();
+  if (result_id == SemIR::ErrorInst::InstId) {
+    return {.storage_id = SemIR::ErrorInst::InstId,
+            .init_id = SemIR::ErrorInst::InstId};
+  }
+
+  // Find the storage argument. If the storage block was spliced or written over
+  // an existing storage argument by `Convert`, the resulting expression will
+  // have a storage argument that points to the possibly-rewritten storage
+  // instruction, and we can use that. Otherwise, the storage access block will
+  // have been inserted above, and we can use `storage_id` unchanged.
+  auto storage_arg_id =
+      SemIR::FindStorageArgForInitializer(context.sem_ir(), result_id);
+  return {
+      .storage_id = storage_arg_id.has_value() ? storage_arg_id : storage_id,
+      .init_id = result_id};
+}
+
 auto ConvertToValueExpr(Context& context, SemIR::InstId expr_id)
     -> SemIR::InstId {
   return Convert(context, SemIR::LocId(expr_id), expr_id,

+ 69 - 14
toolchain/check/convert.h

@@ -55,7 +55,7 @@ struct ConversionTarget {
     // be present if the initializing representation might be in-place.
     Initializing,
     // Convert to an in-place initializing expression whose storage is
-    // designated by `storage_id` (which must not be `None`)
+    // designated by `storage_id` (which must not be `None`).
     InPlaceInitializing,
     Last = InPlaceInitializing
   };
@@ -63,11 +63,14 @@ struct ConversionTarget {
   Kind kind;
   // The target type for the conversion.
   SemIR::TypeId type_id;
-  // The storage being initialized, if any.
+  // The storage being initialized, if any. It must be valid to reference this
+  // instruction after splicing in `storage_access_block` (if specified), so it
+  // must either dominate the initializer or be one of the instructions in
+  // `storage_access_block`.
   SemIR::InstId storage_id = SemIR::InstId::None;
   // For an initializer, a block of pending instructions that `storage_id`
-  // depends on, and that can be discarded if `storage_id` is not accessed.
-  // If this is not null or empty, its last element must be storage_id.
+  // depends on. This block will be spliced or merged before any reference to
+  // `storage_id`, and may be discarded if `storage_id` is not accessed.
   PendingBlock* storage_access_block = nullptr;
   // Whether failure of conversion is an error and is diagnosed to the user.
   // When looking for a possible conversion but with graceful fallback,
@@ -89,12 +92,17 @@ struct ConversionTarget {
 auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
              ConversionTarget target) -> SemIR::InstId;
 
-// Converts `value_id` to an initializing expression of the type of
-// `storage_id`, and returns the possibly-converted initializing expression.
+// Performs initialization of `storage_id` from the expression `value_id`, which
+// is converted to an initializing expression of the type of `storage_id` if
+// necessary, and returns the possibly-converted initializing expression.
+//
 // `storage_id` is used as the storage argument of the resulting expression
-// except as noted below, and when it is used as the storage argument it must
-// precede `value_id`. The caller is responsible for passing the result to an
-// inst that is documented as consuming it, such as `Assign`.
+// except as noted below. As a consequence, `storage_id` must dominate
+// `value_id` and its subexpressions.  This will typically only be the case if
+// `storage_id` syntactically precedes `value_id`. Otherwise, some action will
+// need to be taken to reorder the code, such as instead calling `Initialize`
+// with a pending block containing `storage_id`, or creating a separate
+// `InstBlock` to hold either the storage or the initializer.
 //
 // `for_return` indicates that this conversion is initializing the operand of a
 // `return` statement. This means that `storage_id` will be the return slot
@@ -102,11 +110,58 @@ auto Convert(Context& context, SemIR::LocId loc_id, SemIR::InstId expr_id,
 // representation is not in-place, so in that case `storage_id` will be used
 // solely for its type.
 //
-// TODO: Consider making the target type a separate parameter, and making
-// storage_id optional.
-auto Initialize(Context& context, SemIR::LocId loc_id, SemIR::InstId storage_id,
-                SemIR::InstId value_id, bool for_return = false)
-    -> SemIR::InstId;
+// This function does not guarantee to perform an in-place initialization, so
+// the caller is responsible for passing the returned `InstId` to an inst that
+// is documented as consuming it, such as `Assign`.
+auto InitializeExisting(Context& context, SemIR::LocId loc_id,
+                        SemIR::InstId storage_id, SemIR::InstId value_id,
+                        bool for_return = false) -> SemIR::InstId;
+
+// Result of Initialize.
+struct InitializeResult {
+  // The storage location that contains the initialized value. This may be
+  // different from the `storage_id` that was passed to `Initialize` if the
+  // storage block was written over existing instructions rather than being
+  // spliced in.
+  SemIR::InstId storage_id;
+  // The converted initializing expression used to initialize the storage.
+  SemIR::InstId init_id;
+};
+
+// Performs initialization of `storage_id` from the expression `value_id`, which
+// is converted to an initializing expression of the type of `storage_id` if
+// necessary. `storage_access_block` should be used to supply a pending block
+// that allocates the storage, and typically contains `storage_id`. The target
+// of the initialization will be either `storage_id` itself, or an existing
+// storage argument instruction that is overwritten to hold a copy of
+// `storage_id` as an optimization for SemIR compactness.
+//
+// The storage instruction will only be written over an existing instruction if
+// it is the sole instruction in the pending block. This is expected to be a
+// common case. After this happens, the copy of the instruction in the pending
+// block is expected to be unreachable from the SemIR::File. As a result, the
+// `storage_id` instruction should not be referenced again after calling this
+// function, and this function takes it by rvalue reference to remind the caller
+// of this.
+//
+// If the overwrite optimization is not performed, `storage_access_block` will
+// be inserted before any use of the storage by the initializer, and will be
+// inserted even if the initializer does not actually use the storage. It must
+// be valid to reference `storage_id` after splicing in `storage_access_block`,
+// so `storage_id` must either dominate the initializer (but see the TODO below)
+// or be one of the instructions in `storage_access_block`. If `storage_id` is
+// known to always dominate the initializer, `InitializeExisting` should be used
+// instead.
+//
+// TODO: We don't have an implementation of a proper dominance check, so we
+// fake one up by comparing the order in which the insts were created.
+//
+// This function does not guarantee to perform an in-place initialization, so
+// the caller is responsible for passing the returned `inst_id` to an inst that
+// is documented as consuming it, such as `Assign`.
+auto Initialize(Context& context, SemIR::LocId loc_id,
+                SemIR::InstId&& storage_id, PendingBlock&& storage_access_block,
+                SemIR::InstId value_id) -> InitializeResult;
 
 // Convert the given expression to a value expression of the same type.
 auto ConvertToValueExpr(Context& context, SemIR::InstId expr_id)

+ 13 - 5
toolchain/check/handle_loop_statement.cpp

@@ -16,6 +16,7 @@
 #include "toolchain/check/pattern_match.h"
 #include "toolchain/check/type.h"
 #include "toolchain/sem_ir/absolute_node_ref.h"
+#include "toolchain/sem_ir/expr_info.h"
 #include "toolchain/sem_ir/ids.h"
 
 namespace Carbon::Check {
@@ -169,12 +170,19 @@ auto HandleParseNode(Context& context, Parse::ForHeaderId node_id) -> bool {
                           .op_name = CoreIdentifier::NewCursor},
                          range_id);
   auto cursor_type_id = context.insts().Get(cursor_id).type_id();
-  auto cursor_var_id = AddInstWithCleanup<SemIR::VarStorage>(
-      context, node_id,
+  PendingBlock cursor_var_block(&context);
+  auto cursor_var_id = cursor_var_block.AddInstWithCleanup<SemIR::VarStorage>(
+      node_id,
       {.type_id = cursor_type_id, .pattern_id = SemIR::AbsoluteInstId::None});
-  auto init_id = Initialize(context, node_id, cursor_var_id, cursor_id);
-  AddInst<SemIR::Assign>(context, node_id,
-                         {.lhs_id = cursor_var_id, .rhs_id = init_id});
+  // Disable broken lint that suggests a "fix" that doesn't compile.
+  auto init_result = Initialize(context, node_id,
+                                // NOLINTNEXTLINE(performance-move-const-arg)
+                                std::move(cursor_var_id),
+                                std::move(cursor_var_block), cursor_id);
+  AddInst<SemIR::Assign>(
+      context, node_id,
+      {.lhs_id = init_result.storage_id, .rhs_id = init_result.init_id});
+  cursor_var_id = init_result.storage_id;
 
   // Start emitting the loop header block.
   auto loop_header_id = StartLoopHeader(context, start_node_id);

+ 1 - 1
toolchain/check/handle_operator.cpp

@@ -113,7 +113,7 @@ auto HandleParseNode(Context& context, Parse::InfixOperatorEqualId node_id)
   }
   // TODO: Destroy the old value before reinitializing. This will require
   // building the destruction code before we build the RHS subexpression.
-  rhs_id = Initialize(context, node_id, lhs_id, rhs_id);
+  rhs_id = InitializeExisting(context, node_id, lhs_id, rhs_id);
   AddInst<SemIR::Assign>(context, node_id,
                          {.lhs_id = lhs_id, .rhs_id = rhs_id});
   // We model assignment as an expression, so we need to push a value for

+ 40 - 37
toolchain/check/pattern_match.cpp

@@ -634,7 +634,6 @@ auto MatchContext::DoPreWork(State state, SemIR::VarPattern var_pattern,
 auto MatchContext::DoVarPreWorkImpl(State state, SemIR::TypeId pattern_type_id,
                                     SemIR::InstId scrutinee_id,
                                     WorkItem entry) const -> SemIR::InstId {
-  auto storage_id = SemIR::InstId::None;
   CARBON_KIND_SWITCH(state) {
     case CARBON_KIND(CalleeState* _): {
       // We're emitting pattern-match IR for the callee, but we're still on
@@ -646,52 +645,56 @@ auto MatchContext::DoVarPreWorkImpl(State state, SemIR::TypeId pattern_type_id,
       return scrutinee_id;
     }
     case CARBON_KIND(LocalState* _): {
+      // TODO: Find a more efficient way to put these insts in the global_init
+      // block (or drop the distinction between the global_init block and the
+      // file scope?)
+      if (context_.scope_stack().PeekIndex() == ScopeIndex::Package) {
+        context_.global_init().Resume();
+      }
+
       // In a `var`/`let` declaration, the `VarStorage` inst is created before
       // we start pattern matching.
       auto lookup_result = context_.var_storage_map().Lookup(entry.pattern_id);
       CARBON_CHECK(lookup_result);
-      storage_id = lookup_result.value();
-      break;
+      auto storage_id = lookup_result.value();
+      if (scrutinee_id.has_value()) {
+        auto init_id =
+            InitializeExisting(context_, SemIR::LocId(entry.pattern_id),
+                               storage_id, scrutinee_id, /*for_return=*/false);
+        // TODO: It's a bit weird to use an `Assign` instruction to model
+        // initialization. Consider adding a different instruction for this
+        // purpose.
+        AddInst<SemIR::Assign>(context_, SemIR::LocId(entry.pattern_id),
+                               {.lhs_id = storage_id, .rhs_id = init_id});
+      }
+
+      if (context_.scope_stack().PeekIndex() == ScopeIndex::Package) {
+        context_.global_init().Suspend();
+      }
+      return storage_id;
     }
     case CARBON_KIND(CallerState* _): {
-      storage_id = AddInst<SemIR::TemporaryStorage>(
+      // TODO: This variable's lifetime should end at the end of the call or the
+      // full-expression. We don't use a temporary here, because temporaries are
+      // treated as being immutable.
+      PendingBlock storage_block(&context_);
+      auto storage_id = storage_block.AddInstWithCleanup<SemIR::VarStorage>(
+          SemIR::LocId(entry.pattern_id),
+          {.type_id = pattern_type_id, .pattern_id = entry.pattern_id});
+      // Disable broken lint that suggests a "fix" that doesn't compile.
+      auto init_result = Initialize(
           context_, SemIR::LocId(entry.pattern_id),
-          {.type_id = pattern_type_id});
-      CARBON_CHECK(scrutinee_id.has_value());
-      break;
-    }
-  }
-  // TODO: Find a more efficient way to put these insts in the global_init
-  // block (or drop the distinction between the global_init block and the
-  // file scope?)
-  if (context_.scope_stack().PeekIndex() == ScopeIndex::Package) {
-    context_.global_init().Resume();
-  }
-  if (scrutinee_id.has_value()) {
-    auto init_id = Initialize(context_, SemIR::LocId(entry.pattern_id),
-                              storage_id, scrutinee_id);
-    // If we created a `TemporaryStorage` to hold the var, create a
-    // corresponding `Temporary` to model that its initialization is complete.
-    // TODO: If the subpattern is a binding, we may want to destroy the
-    // parameter variable in the callee instead of the caller so that we can
-    // support destructive move from it.
-    if (std::holds_alternative<CallerState*>(state)) {
-      storage_id = AddInstWithCleanup<SemIR::Temporary>(
+          // NOLINTNEXTLINE(performance-move-const-arg)
+          std::move(storage_id), std::move(storage_block), scrutinee_id);
+      // TODO: Consider instead creating something like a `Temporary`
+      // instruction that returns a reference so that constant evaluation can
+      // obtain the value of the var parameter.
+      AddInst<SemIR::Assign>(
           context_, SemIR::LocId(entry.pattern_id),
-          {.type_id = context_.insts().Get(storage_id).type_id(),
-           .storage_id = storage_id,
-           .init_id = init_id});
-    } else {
-      // TODO: Consider using different instruction kinds for assignment
-      // versus initialization.
-      AddInst<SemIR::Assign>(context_, SemIR::LocId(entry.pattern_id),
-                             {.lhs_id = storage_id, .rhs_id = init_id});
+          {.lhs_id = init_result.storage_id, .rhs_id = init_result.init_id});
+      return init_result.storage_id;
     }
   }
-  if (context_.scope_stack().PeekIndex() == ScopeIndex::Package) {
-    context_.global_init().Suspend();
-  }
-  return storage_id;
 }
 
 auto MatchContext::DoPostWork(State /*state*/,

+ 11 - 12
toolchain/check/pending_block.h

@@ -69,22 +69,21 @@ class PendingBlock {
 
   // Replace the instruction at target_id with the instructions in this block.
   // The new value for target_id should be value_id. Returns the InstId that
-  // should be used to refer to the result from now on. value_id must precede
-  // target_id, or be the last ID in this block, in order to preserve the
-  // property that SemIR is topologically sorted.
+  // should be used to refer to the result from now on. value_id must dominate
+  // target_id (but see below), or refer to an instruction within this block, in
+  // order to preserve the property that SemIR is topologically sorted.
   //
-  // TODO: we could also allow value_id to be one of the other insts in this
-  // block, but that would be costlier to enforce.
+  // TODO: We don't have an implementation of a proper dominance check, so we
+  // fake one up by comparing the order in which the insts were created.
+  // Add a general end-of-phase dominance check and remove the one here and in
+  // `InitializeExisting`.
   auto MergeReplacing(SemIR::InstId target_id, SemIR::InstId value_id)
       -> SemIR::InstId {
     CARBON_CHECK(target_id != value_id);
-
-    // TODO: consider adding an end-of-phase check that the SemIR::File is in
-    // SSA form, and dropping this check and the ordering preconditions here and
-    // on Initialize.
-    CARBON_CHECK(value_id.index <= target_id.index ||
-                     (!insts_.empty() && insts_.back() == value_id),
-                 "Splice would break topological sorting of insts");
+    CARBON_CHECK(context_->insts().GetRawIndex(value_id) <=
+                         context_->insts().GetRawIndex(target_id) ||
+                     llvm::is_contained(insts_, value_id),
+                 "Splice might break dominance condition");
     SemIR::LocIdAndInst value = context_->insts().GetWithLocId(value_id);
 
     auto result_id = value_id;

+ 2 - 2
toolchain/check/return.cpp

@@ -191,8 +191,8 @@ auto BuildReturnWithExpr(Context& context, SemIR::LocId loc_id,
         out_param_id =
             call_params[function.call_param_ranges.return_begin().index];
         CARBON_CHECK(out_param_id.has_value());
-        expr_id = Initialize(context, loc_id, out_param_id, expr_id,
-                             /*for_return=*/true);
+        expr_id = InitializeExisting(context, loc_id, out_param_id, expr_id,
+                                     /*for_return=*/true);
         if (!SemIR::InitRepr::ForType(context.sem_ir(), return_type_id)
                  .MightBeInPlace()) {
           out_param_id = SemIR::InstId::None;

+ 6 - 7
toolchain/check/testdata/function/call/form.carbon

@@ -286,10 +286,8 @@ fn F(Form:! Core.Form()) ->? Form;
 // CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete]
 // CHECK:STDOUT:   %bound_method: <bound method> = bound_method %int_0.5c6, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete]
 // CHECK:STDOUT:   %int_0.6a9: %i32 = int_value 0 [concrete]
-// CHECK:STDOUT:   %.eea: ref %i32 = temporary invalid, %int_0.6a9 [concrete]
 // CHECK:STDOUT:   %Destroy.Op.type.bae255.2: type = fn_type @Destroy.Op.loc4_7.2 [concrete]
 // CHECK:STDOUT:   %Destroy.Op.651ba6.2: %Destroy.Op.type.bae255.2 = struct_value () [concrete]
-// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %.eea, %Destroy.Op.651ba6.2 [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -317,16 +315,17 @@ fn F(Form:! Core.Form()) ->? Form;
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type = name_ref F, file.%F.decl [concrete = constants.%F]
 // CHECK:STDOUT:   %int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0.5c6]
-// CHECK:STDOUT:   %.loc4_7.1: ref %i32 = temporary_storage
 // CHECK:STDOUT:   %impl.elem0: %.545 = impl_witness_access constants.%ImplicitAs.impl_witness.6bc, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5]
 // CHECK:STDOUT:   %bound_method.loc4_7.1: <bound method> = bound_method %int_0, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound]
 // CHECK:STDOUT:   %specific_fn: <specific function> = specific_function %impl.elem0, @Core.IntLiteral.as.ImplicitAs.impl.Convert(constants.%int_32) [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn]
 // CHECK:STDOUT:   %bound_method.loc4_7.2: <bound method> = bound_method %int_0, %specific_fn [concrete = constants.%bound_method]
 // CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc4_7.2(%int_0) [concrete = constants.%int_0.6a9]
-// CHECK:STDOUT:   %.loc4_7.2: init %i32 = converted %int_0, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_0.6a9]
-// CHECK:STDOUT:   %.loc4_7.3: ref %i32 = temporary %.loc4_7.1, %.loc4_7.2 [concrete = constants.%.eea]
-// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc4_7.3)
-// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call constants.%Destroy.Op.bound(constants.%.eea)
+// CHECK:STDOUT:   %.loc4: init %i32 = converted %int_0, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_0.6a9]
+// CHECK:STDOUT:   %x.var: ref %i32 = var @F.%.loc4_7
+// CHECK:STDOUT:   assign %x.var, %.loc4
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%x.var)
+// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %x.var, constants.%Destroy.Op.651ba6.2
+// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(%x.var)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 5 - 5
toolchain/check/testdata/generic/var_param.carbon

@@ -110,16 +110,16 @@ fn Test(x: i32) {
 // CHECK:STDOUT:   %I.facet: %I.type = facet_value constants.%i32, (constants.%I.impl_witness) [concrete = constants.%I.facet]
 // CHECK:STDOUT:   %.loc27: %I.type = converted constants.%i32, %I.facet [concrete = constants.%I.facet]
 // CHECK:STDOUT:   %TestOp.specific_fn: <specific function> = specific_function %TestOp.ref, @TestOp(constants.%I.facet) [concrete = constants.%TestOp.specific_fn]
-// CHECK:STDOUT:   %.loc22_25.1: ref %i32 = temporary_storage
 // CHECK:STDOUT:   %impl.elem0: %.8e2 = impl_witness_access constants.%Copy.impl_witness.f17, element0 [concrete = constants.%Int.as.Copy.impl.Op.664]
 // CHECK:STDOUT:   %bound_method.loc27_10.1: <bound method> = bound_method %x.ref, %impl.elem0
 // CHECK:STDOUT:   %specific_fn: <specific function> = specific_function %impl.elem0, @Int.as.Copy.impl.Op(constants.%int_32) [concrete = constants.%Int.as.Copy.impl.Op.specific_fn]
 // CHECK:STDOUT:   %bound_method.loc27_10.2: <bound method> = bound_method %x.ref, %specific_fn
 // CHECK:STDOUT:   %Int.as.Copy.impl.Op.call: init %i32 = call %bound_method.loc27_10.2(%x.ref)
-// CHECK:STDOUT:   %.loc22_25.2: ref %i32 = temporary %.loc22_25.1, %Int.as.Copy.impl.Op.call
-// CHECK:STDOUT:   %TestOp.call: init %empty_tuple.type = call %TestOp.specific_fn(%.loc22_25.2)
-// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %.loc22_25.2, constants.%Destroy.Op.651ba6.2
-// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(%.loc22_25.2)
+// CHECK:STDOUT:   %x.var: ref %i32 = var @TestOp.%x.param_patt
+// CHECK:STDOUT:   assign %x.var, %Int.as.Copy.impl.Op.call
+// CHECK:STDOUT:   %TestOp.call: init %empty_tuple.type = call %TestOp.specific_fn(%x.var)
+// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %x.var, constants.%Destroy.Op.651ba6.2
+// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(%x.var)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 170 - 16
toolchain/check/testdata/var/var_pattern.carbon

@@ -2,7 +2,7 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/convert.carbon
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/int.carbon
 // TODO: Add ranges and switch to "--dump-sem-ir-ranges=only".
 // EXTRA-ARGS: --dump-sem-ir-ranges=if-present
 //
@@ -145,6 +145,38 @@ fn G() {
   let (unused x: (), var unused y: (), unused z: ()) = (F(), F(), F());
 }
 
+// --- var_param_from_init.carbon
+
+library "[[@TEST_NAME]]";
+
+class X {}
+
+//@dump-sem-ir-begin
+fn TakeVar(var a: X);
+//@dump-sem-ir-end
+
+fn MakeInit() -> X;
+
+fn PassInitToVar() {
+  //@dump-sem-ir-begin
+  TakeVar(MakeInit());
+  //@dump-sem-ir-end
+}
+
+// --- var_param_from_int_literal.carbon
+
+library "[[@TEST_NAME]]";
+
+//@dump-sem-ir-begin
+fn TakeVar(var n: i32);
+//@dump-sem-ir-end
+
+fn Call() {
+  //@dump-sem-ir-begin
+  TakeVar(123);
+  //@dump-sem-ir-end
+}
+
 // CHECK:STDOUT: --- basic.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
@@ -439,7 +471,6 @@ fn G() {
 // CHECK:STDOUT:   %F: %F.type = struct_value () [concrete]
 // CHECK:STDOUT:   %G.type: type = fn_type @G [concrete]
 // CHECK:STDOUT:   %G: %G.type = struct_value () [concrete]
-// CHECK:STDOUT:   %.a4d: ref %empty_tuple.type = temporary invalid, %empty_tuple [concrete]
 // CHECK:STDOUT:   %Destroy.type: type = facet_type <@Destroy> [concrete]
 // CHECK:STDOUT:   %Destroy.Op.type: type = fn_type @Destroy.Op [concrete]
 // CHECK:STDOUT:   %Destroy.Op: %Destroy.Op.type = struct_value () [concrete]
@@ -506,13 +537,15 @@ fn G() {
 // CHECK:STDOUT:   %.loc8_9.1: %empty_tuple.type = tuple_literal () [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %tuple: %empty_tuple.type = tuple_value () [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %.loc8_5: %empty_tuple.type = converted %v.ref, %tuple [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.loc4_13.1: ref %empty_tuple.type = temporary_storage
+// CHECK:STDOUT:   %y.var: ref %empty_tuple.type = var @F.%y.param_patt
 // CHECK:STDOUT:   %.loc8_9.2: init %empty_tuple.type = tuple_init () [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.loc4_13.2: init %empty_tuple.type = converted %.loc8_9.1, %.loc8_9.2 [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.loc4_13.3: ref %empty_tuple.type = temporary %.loc4_13.1, %.loc4_13.2 [concrete = constants.%.a4d]
-// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc8_5, %.loc4_13.3)
-// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %v.var, constants.%Destroy.Op
-// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(%v.var)
+// CHECK:STDOUT:   %.loc4: init %empty_tuple.type = converted %.loc8_9.1, %.loc8_9.2 [concrete = constants.%empty_tuple]
+// CHECK:STDOUT:   assign %y.var, %.loc4
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc8_5, %y.var)
+// CHECK:STDOUT:   %Destroy.Op.bound.loc4: <bound method> = bound_method %y.var, constants.%Destroy.Op
+// CHECK:STDOUT:   %Destroy.Op.call.loc4: init %empty_tuple.type = call %Destroy.Op.bound.loc4(%y.var)
+// CHECK:STDOUT:   %Destroy.Op.bound.loc7: <bound method> = bound_method %v.var, constants.%Destroy.Op
+// CHECK:STDOUT:   %Destroy.Op.call.loc7: init %empty_tuple.type = call %Destroy.Op.bound.loc7(%v.var)
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -623,8 +656,8 @@ fn G() {
 // CHECK:STDOUT:   %G: %G.type = struct_value () [concrete]
 // CHECK:STDOUT:   %F.type: type = fn_type @F [concrete]
 // CHECK:STDOUT:   %F: %F.type = struct_value () [concrete]
+// CHECK:STDOUT:   %pattern_type.cb1: type = pattern_type %empty_tuple.type [concrete]
 // CHECK:STDOUT:   %empty_tuple: %empty_tuple.type = tuple_value () [concrete]
-// CHECK:STDOUT:   %.a4d: ref %empty_tuple.type = temporary invalid, %empty_tuple [concrete]
 // CHECK:STDOUT:   %Destroy.type: type = facet_type <@Destroy> [concrete]
 // CHECK:STDOUT:   %Self: %Destroy.type = symbolic_binding Self, 0 [symbolic]
 // CHECK:STDOUT:   %Destroy.WithSelf.Op.type.cb2: type = fn_type @Destroy.WithSelf.Op, @Destroy.WithSelf(%Self) [symbolic]
@@ -637,7 +670,6 @@ fn G() {
 // CHECK:STDOUT:   %Destroy.facet.5ae: %Destroy.type = facet_value %empty_tuple.type, (%custom_witness.8d7) [concrete]
 // CHECK:STDOUT:   %Destroy.WithSelf.Op.type.89a: type = fn_type @Destroy.WithSelf.Op, @Destroy.WithSelf(%Destroy.facet.5ae) [concrete]
 // CHECK:STDOUT:   %.518: type = fn_type_with_self_type %Destroy.WithSelf.Op.type.89a, %Destroy.facet.5ae [concrete]
-// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %.a4d, %Destroy.Op [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -651,6 +683,8 @@ fn G() {
 // CHECK:STDOUT:     import P//default
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %P.F: %F.type = import_ref P//default, F, loaded [concrete = constants.%F]
+// CHECK:STDOUT:   %y.patt: %pattern_type.cb1 = ref_binding_pattern y [concrete]
+// CHECK:STDOUT:   %y.param_patt: %pattern_type.cb1 = var_param_pattern %y.patt [concrete]
 // CHECK:STDOUT:   %Core.Destroy: type = import_ref Core//prelude/parts/destroy, Destroy, loaded [concrete = constants.%Destroy.type]
 // CHECK:STDOUT:   %Core.import_ref.06b: %Destroy.assoc_type = import_ref Core//prelude/parts/destroy, loc{{\d+_\d+}}, loaded [concrete = constants.%assoc0]
 // CHECK:STDOUT:   %Core.import_ref.8bb: @Destroy.WithSelf.%Destroy.WithSelf.Op.type (%Destroy.WithSelf.Op.type.cb2) = import_ref Core//prelude/parts/destroy, loc{{\d+_\d+}}, loaded [symbolic = @Destroy.WithSelf.%Destroy.WithSelf.Op (constants.%Destroy.WithSelf.Op.9f1)]
@@ -675,15 +709,15 @@ fn G() {
 // CHECK:STDOUT:   %.loc7_12.1: %empty_tuple.type = tuple_literal () [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %empty_tuple: %empty_tuple.type = tuple_value () [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %.loc7_8.2: %empty_tuple.type = converted %.loc7_8.1, %empty_tuple [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.1: ref %empty_tuple.type = temporary_storage
+// CHECK:STDOUT:   %y.var: ref %empty_tuple.type = var imports.%y.param_patt
 // CHECK:STDOUT:   %.loc7_12.2: init %empty_tuple.type = tuple_init () [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.2: init %empty_tuple.type = converted %.loc7_12.1, %.loc7_12.2 [concrete = constants.%empty_tuple]
-// CHECK:STDOUT:   %.3: ref %empty_tuple.type = temporary %.1, %.2 [concrete = constants.%.a4d]
-// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc7_8.2, %.3)
+// CHECK:STDOUT:   %.1: init %empty_tuple.type = converted %.loc7_12.1, %.loc7_12.2 [concrete = constants.%empty_tuple]
+// CHECK:STDOUT:   assign %y.var, %.1
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(%.loc7_8.2, %y.var)
 // CHECK:STDOUT:   %Op.ref: %Destroy.assoc_type = name_ref Op, imports.%Core.import_ref.06b [concrete = constants.%assoc0]
 // CHECK:STDOUT:   %impl.elem0: %.518 = impl_witness_access constants.%custom_witness.8d7, element0 [concrete = constants.%Destroy.Op]
-// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.3, %impl.elem0 [concrete = constants.%Destroy.Op.bound]
-// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %bound_method(%.3) [concrete = constants.%empty_tuple]
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %y.var, %impl.elem0
+// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %bound_method(%y.var)
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -950,3 +984,123 @@ fn G() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @Destroy.Op(%self.param: ref %empty_tuple.type) = "no_op";
 // CHECK:STDOUT:
+// CHECK:STDOUT: --- var_param_from_init.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %X: type = class_type @X [concrete]
+// CHECK:STDOUT:   %empty_struct_type: type = struct_type {} [concrete]
+// CHECK:STDOUT:   %pattern_type.05f: type = pattern_type %X [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %TakeVar.type: type = fn_type @TakeVar [concrete]
+// CHECK:STDOUT:   %TakeVar: %TakeVar.type = struct_value () [concrete]
+// CHECK:STDOUT:   %MakeInit.type: type = fn_type @MakeInit [concrete]
+// CHECK:STDOUT:   %MakeInit: %MakeInit.type = struct_value () [concrete]
+// CHECK:STDOUT:   %Destroy.Op.type.bae255.2: type = fn_type @Destroy.Op.loc7_12.2 [concrete]
+// CHECK:STDOUT:   %Destroy.Op.651ba6.2: %Destroy.Op.type.bae255.2 = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %TakeVar.decl: %TakeVar.type = fn_decl @TakeVar [concrete = constants.%TakeVar] {
+// CHECK:STDOUT:     %a.patt: %pattern_type.05f = ref_binding_pattern a [concrete]
+// CHECK:STDOUT:     %a.param_patt: %pattern_type.05f = var_param_pattern %a.patt [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %a.param: ref %X = ref_param call_param0
+// CHECK:STDOUT:     %X.ref: type = name_ref X, file.%X.decl [concrete = constants.%X]
+// CHECK:STDOUT:     %a: ref %X = ref_binding a, %a.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @TakeVar(%a.param: ref %X);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @PassInitToVar() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %TakeVar.ref: %TakeVar.type = name_ref TakeVar, file.%TakeVar.decl [concrete = constants.%TakeVar]
+// CHECK:STDOUT:   %MakeInit.ref: %MakeInit.type = name_ref MakeInit, file.%MakeInit.decl [concrete = constants.%MakeInit]
+// CHECK:STDOUT:   %a.var: ref %X = var @TakeVar.%a.param_patt
+// CHECK:STDOUT:   %MakeInit.call: init %X to %a.var = call %MakeInit.ref()
+// CHECK:STDOUT:   assign %a.var, %MakeInit.call
+// CHECK:STDOUT:   %TakeVar.call: init %empty_tuple.type = call %TakeVar.ref(%a.var)
+// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method <unexpected>.inst{{[0-9A-F]+}}.loc7_12, constants.%Destroy.Op.651ba6.2
+// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(<unexpected>.inst{{[0-9A-F]+}}.loc7_12)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Destroy.Op.loc7_12.1(%self.param: ref %empty_struct_type) = "no_op";
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Destroy.Op.loc7_12.2(%self.param: ref %X) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- var_param_from_int_literal.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
+// CHECK:STDOUT:   %pattern_type.7ce: type = pattern_type %i32 [concrete]
+// CHECK:STDOUT:   %i32.builtin: type = int_type signed, %int_32 [concrete]
+// CHECK:STDOUT:   %TakeVar.type: type = fn_type @TakeVar [concrete]
+// CHECK:STDOUT:   %TakeVar: %TakeVar.type = struct_value () [concrete]
+// CHECK:STDOUT:   %int_123.fff: Core.IntLiteral = int_value 123 [concrete]
+// CHECK:STDOUT:   %ImplicitAs.type.e8c: type = facet_type <@ImplicitAs, @ImplicitAs(%i32)> [concrete]
+// CHECK:STDOUT:   %To: Core.IntLiteral = symbolic_binding To, 0 [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%To) [symbolic]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6 = struct_value () [symbolic]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness.6bc: <witness> = impl_witness imports.%ImplicitAs.impl_witness_table.74f, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d: type = fn_type @Core.IntLiteral.as.ImplicitAs.impl.Convert, @Core.IntLiteral.as.ImplicitAs.impl(%int_32) [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5: %Core.IntLiteral.as.ImplicitAs.impl.Convert.type.e0d = struct_value () [concrete]
+// CHECK:STDOUT:   %ImplicitAs.facet: %ImplicitAs.type.e8c = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness.6bc) [concrete]
+// CHECK:STDOUT:   %ImplicitAs.WithSelf.Convert.type.b37: type = fn_type @ImplicitAs.WithSelf.Convert, @ImplicitAs.WithSelf(%i32, %ImplicitAs.facet) [concrete]
+// CHECK:STDOUT:   %.545: type = fn_type_with_self_type %ImplicitAs.WithSelf.Convert.type.b37, %ImplicitAs.facet [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.bound: <bound method> = bound_method %int_123.fff, %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5 [concrete]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn: <specific function> = specific_function %Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5, @Core.IntLiteral.as.ImplicitAs.impl.Convert(%int_32) [concrete]
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %int_123.fff, %Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn [concrete]
+// CHECK:STDOUT:   %int_123.f7f: %i32 = int_value 123 [concrete]
+// CHECK:STDOUT:   %Destroy.Op.type.bae255.2: type = fn_type @Destroy.Op.loc5_12.2 [concrete]
+// CHECK:STDOUT:   %Destroy.Op.651ba6.2: %Destroy.Op.type.bae255.2 = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT:   %Core.import_ref.42d: @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert.type (%Core.IntLiteral.as.ImplicitAs.impl.Convert.type.4e6) = import_ref Core//prelude/parts/int, loc{{\d+_\d+}}, loaded [symbolic = @Core.IntLiteral.as.ImplicitAs.impl.%Core.IntLiteral.as.ImplicitAs.impl.Convert (constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.3c2)]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness_table.74f = impl_witness_table (%Core.import_ref.42d), @Core.IntLiteral.as.ImplicitAs.impl [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: file {
+// CHECK:STDOUT:   %TakeVar.decl: %TakeVar.type = fn_decl @TakeVar [concrete = constants.%TakeVar] {
+// CHECK:STDOUT:     %n.patt: %pattern_type.7ce = ref_binding_pattern n [concrete]
+// CHECK:STDOUT:     %n.param_patt: %pattern_type.7ce = var_param_pattern %n.patt [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %n.param: ref %i32 = ref_param call_param0
+// CHECK:STDOUT:     %i32: type = type_literal constants.%i32 [concrete = constants.%i32]
+// CHECK:STDOUT:     %n: ref %i32 = ref_binding n, %n.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @TakeVar(%n.param: ref %i32);
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Call() {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %TakeVar.ref: %TakeVar.type = name_ref TakeVar, file.%TakeVar.decl [concrete = constants.%TakeVar]
+// CHECK:STDOUT:   %int_123: Core.IntLiteral = int_value 123 [concrete = constants.%int_123.fff]
+// CHECK:STDOUT:   %impl.elem0: %.545 = impl_witness_access constants.%ImplicitAs.impl_witness.6bc, element0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.0b5]
+// CHECK:STDOUT:   %bound_method.loc5_12.1: <bound method> = bound_method %int_123, %impl.elem0 [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.bound]
+// CHECK:STDOUT:   %specific_fn: <specific function> = specific_function %impl.elem0, @Core.IntLiteral.as.ImplicitAs.impl.Convert(constants.%int_32) [concrete = constants.%Core.IntLiteral.as.ImplicitAs.impl.Convert.specific_fn]
+// CHECK:STDOUT:   %bound_method.loc5_12.2: <bound method> = bound_method %int_123, %specific_fn [concrete = constants.%bound_method]
+// CHECK:STDOUT:   %Core.IntLiteral.as.ImplicitAs.impl.Convert.call: init %i32 = call %bound_method.loc5_12.2(%int_123) [concrete = constants.%int_123.f7f]
+// CHECK:STDOUT:   %.loc5: init %i32 = converted %int_123, %Core.IntLiteral.as.ImplicitAs.impl.Convert.call [concrete = constants.%int_123.f7f]
+// CHECK:STDOUT:   %n.var: ref %i32 = var @TakeVar.%n.param_patt
+// CHECK:STDOUT:   assign %n.var, %.loc5
+// CHECK:STDOUT:   %TakeVar.call: init %empty_tuple.type = call %TakeVar.ref(%n.var)
+// CHECK:STDOUT:   %Destroy.Op.bound: <bound method> = bound_method %n.var, constants.%Destroy.Op.651ba6.2
+// CHECK:STDOUT:   %Destroy.Op.call: init %empty_tuple.type = call %Destroy.Op.bound(%n.var)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Destroy.Op.loc5_12.1(%self.param: ref %i32.builtin) = "no_op";
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Destroy.Op.loc5_12.2(%self.param: ref %i32) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   return
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 2 - 2
toolchain/check/thunk.cpp

@@ -435,8 +435,8 @@ auto BuildThunkDefinitionForExport(Context& context,
         context.inst_blocks().Get(thunk_function.call_params_id).back();
 
     SemIR::LocId loc_id(out_param_id);
-    auto init_id =
-        Initialize(context, loc_id, out_param_id, call_id, /*for_return=*/true);
+    auto init_id = InitializeExisting(context, loc_id, out_param_id, call_id,
+                                      /*for_return=*/true);
     AddInst(context, loc_id,
             SemIR::Assign{
                 .lhs_id = out_param_id,

+ 1 - 0
toolchain/lower/aggregate.cpp

@@ -7,6 +7,7 @@
 #include "llvm/ADT/STLExtras.h"
 #include "toolchain/sem_ir/expr_info.h"
 #include "toolchain/sem_ir/inst.h"
+#include "toolchain/sem_ir/inst_kind.h"
 #include "toolchain/sem_ir/type_info.h"
 #include "toolchain/sem_ir/typed_insts.h"
 

+ 5 - 6
toolchain/lower/testdata/function/call/form.carbon

@@ -116,17 +116,16 @@ fn G() {
 // CHECK:STDOUT: ; ModuleID = 'var_form_param.carbon'
 // CHECK:STDOUT: source_filename = "var_form_param.carbon"
 // CHECK:STDOUT:
-// CHECK:STDOUT: @int_0.6a9 = internal constant i32 0
-// CHECK:STDOUT:
 // CHECK:STDOUT: declare void @_CF.Main(ptr)
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_CG.Main() #0 !dbg !4 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %.loc3_7.1.temp = alloca i32, align 4, !dbg !7
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc3_7.1.temp), !dbg !7
-// CHECK:STDOUT:   call void @_CF.Main(ptr @int_0.6a9), !dbg !8
-// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr @int_0.6a9), !dbg !7
+// CHECK:STDOUT:   %x.var = alloca i32, align 4, !dbg !7
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %x.var), !dbg !7
+// CHECK:STDOUT:   store i32 0, ptr %x.var, align 4, !dbg !7
+// CHECK:STDOUT:   call void @_CF.Main(ptr %x.var), !dbg !8
+// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %x.var), !dbg !7
 // CHECK:STDOUT:   ret void, !dbg !9
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 10 - 5
toolchain/lower/testdata/function/definition/destroy.carbon

@@ -137,7 +137,7 @@ fn InitLet() {
 // CHECK:STDOUT: ; ModuleID = 'var_param.carbon'
 // CHECK:STDOUT: source_filename = "var_param.carbon"
 // CHECK:STDOUT:
-// CHECK:STDOUT: @C.val = internal constant {} zeroinitializer
+// CHECK:STDOUT: @C.val.loc6 = internal constant {} zeroinitializer
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_CF.Main(ptr %_) #0 !dbg !4 {
@@ -150,11 +150,12 @@ fn InitLet() {
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_CCallF.Main() #0 !dbg !11 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %.loc6_6.1.temp = alloca {}, align 8, !dbg !14
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc6_6.1.temp), !dbg !14
-// CHECK:STDOUT:   call void @_CF.Main(ptr @C.val), !dbg !15
+// CHECK:STDOUT:   %_.var = alloca {}, align 8, !dbg !14
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var), !dbg !14
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %_.var, ptr align 1 @C.val.loc6, i64 0, i1 false), !dbg !14
+// CHECK:STDOUT:   call void @_CF.Main(ptr %_.var), !dbg !15
 // CHECK:STDOUT:   call void @_CG.Main(), !dbg !16
-// CHECK:STDOUT:   call void @"_COp.71e25083d63c4d6c:core.Destroy.Core"(ptr @C.val), !dbg !14
+// CHECK:STDOUT:   call void @"_COp.71e25083d63c4d6c:core.Destroy.Core"(ptr %_.var), !dbg !14
 // CHECK:STDOUT:   ret void, !dbg !17
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -167,8 +168,12 @@ fn InitLet() {
 // CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
 // CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
 // CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.memcpy.p0.p0.i64(ptr noalias writeonly captures(none), ptr noalias readonly captures(none), i64, i1 immarg) #2
+// CHECK:STDOUT:
 // CHECK:STDOUT: attributes #0 = { nounwind }
 // CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT: attributes #2 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) }
 // CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
 // CHECK:STDOUT: !llvm.dbg.cu = !{!2}

+ 35 - 25
toolchain/lower/testdata/function/definition/var_param.carbon

@@ -31,9 +31,8 @@ fn Call() {
 // CHECK:STDOUT: ; ModuleID = 'var_param.carbon'
 // CHECK:STDOUT: source_filename = "var_param.carbon"
 // CHECK:STDOUT:
-// CHECK:STDOUT: @int_1.5d2 = internal constant i32 1
 // CHECK:STDOUT: @X.val = internal constant {} zeroinitializer
-// CHECK:STDOUT: @X.val.loc27_18.6 = internal constant {} zeroinitializer
+// CHECK:STDOUT: @X.val.loc16 = internal constant {} zeroinitializer
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_COneVar_i32.Main(ptr %_) #0 !dbg !4 {
@@ -68,32 +67,38 @@ fn Call() {
 // CHECK:STDOUT: ; Function Attrs: nounwind
 // CHECK:STDOUT: define void @_CCall.Main() #0 !dbg !35 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %.loc15_15.1.temp = alloca i32, align 4, !dbg !38
-// CHECK:STDOUT:   %.loc16_13.1.temp = alloca {}, align 8, !dbg !39
-// CHECK:STDOUT:   %.loc18_12.1.temp = alloca i32, align 4, !dbg !40
-// CHECK:STDOUT:   %.loc18_24.1.temp = alloca {}, align 8, !dbg !41
-// CHECK:STDOUT:   %.loc20_15.1.temp = alloca i32, align 4, !dbg !42
+// CHECK:STDOUT:   %_.var.loc15 = alloca i32, align 4, !dbg !38
+// CHECK:STDOUT:   %_.var.loc16 = alloca {}, align 8, !dbg !39
+// CHECK:STDOUT:   %_.var.loc18_12 = alloca i32, align 4, !dbg !40
+// CHECK:STDOUT:   %_.var.loc18_24 = alloca {}, align 8, !dbg !41
+// CHECK:STDOUT:   %_.var.loc20 = alloca i32, align 4, !dbg !42
 // CHECK:STDOUT:   %.loc27_18.2.temp = alloca {}, align 8, !dbg !43
-// CHECK:STDOUT:   %.loc21_23.1.temp = alloca {}, align 8, !dbg !44
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc15_15.1.temp), !dbg !38
-// CHECK:STDOUT:   call void @_COneVar_i32.Main(ptr @int_1.5d2), !dbg !45
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc16_13.1.temp), !dbg !39
-// CHECK:STDOUT:   call void @_COneVar_X.Main(ptr @X.val), !dbg !46
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc18_12.1.temp), !dbg !40
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc18_24.1.temp), !dbg !41
-// CHECK:STDOUT:   call void @_CTwoVars.Main(ptr @int_1.5d2, ptr @X.val), !dbg !47
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc20_15.1.temp), !dbg !42
+// CHECK:STDOUT:   %_.var.loc21 = alloca {}, align 8, !dbg !44
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc15), !dbg !38
+// CHECK:STDOUT:   store i32 1, ptr %_.var.loc15, align 4, !dbg !38
+// CHECK:STDOUT:   call void @_COneVar_i32.Main(ptr %_.var.loc15), !dbg !45
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc16), !dbg !39
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %_.var.loc16, ptr align 1 @X.val.loc16, i64 0, i1 false), !dbg !39
+// CHECK:STDOUT:   call void @_COneVar_X.Main(ptr %_.var.loc16), !dbg !46
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc18_12), !dbg !40
+// CHECK:STDOUT:   store i32 1, ptr %_.var.loc18_12, align 4, !dbg !40
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc18_24), !dbg !41
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %_.var.loc18_24, ptr align 1 @X.val.loc16, i64 0, i1 false), !dbg !41
+// CHECK:STDOUT:   call void @_CTwoVars.Main(ptr %_.var.loc18_12, ptr %_.var.loc18_24), !dbg !47
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc20), !dbg !42
+// CHECK:STDOUT:   store i32 1, ptr %_.var.loc20, align 4, !dbg !42
 // CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc27_18.2.temp), !dbg !43
-// CHECK:STDOUT:   call void @_CVarThenLet.Main(ptr @int_1.5d2, ptr @X.val.loc27_18.6), !dbg !48
-// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc21_23.1.temp), !dbg !44
-// CHECK:STDOUT:   call void @_CLetThenVar.Main(i32 1, ptr @X.val), !dbg !49
-// CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr @X.val), !dbg !44
+// CHECK:STDOUT:   call void @_CVarThenLet.Main(ptr %_.var.loc20, ptr @X.val.loc16), !dbg !48
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %_.var.loc21), !dbg !44
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %_.var.loc21, ptr align 1 @X.val.loc16, i64 0, i1 false), !dbg !44
+// CHECK:STDOUT:   call void @_CLetThenVar.Main(i32 1, ptr %_.var.loc21), !dbg !49
+// CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr %_.var.loc21), !dbg !44
 // CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr @X.val), !dbg !43
-// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr @int_1.5d2), !dbg !42
-// CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr @X.val), !dbg !41
-// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr @int_1.5d2), !dbg !40
-// CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr @X.val), !dbg !39
-// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr @int_1.5d2), !dbg !38
+// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %_.var.loc20), !dbg !42
+// CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr %_.var.loc18_24), !dbg !41
+// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %_.var.loc18_12), !dbg !40
+// CHECK:STDOUT:   call void @"_COp.607f187990247af9:core.Destroy.Core"(ptr %_.var.loc16), !dbg !39
+// CHECK:STDOUT:   call void @"_COp.7e389eab4a7e5487:core.Destroy.Core"(ptr %_.var.loc15), !dbg !38
 // CHECK:STDOUT:   ret void, !dbg !50
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -112,13 +117,18 @@ fn Call() {
 // CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
 // CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
 // CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.memcpy.p0.p0.i64(ptr noalias writeonly captures(none), ptr noalias readonly captures(none), i64, i1 immarg) #2
+// CHECK:STDOUT:
 // CHECK:STDOUT: ; uselistorder directives
 // CHECK:STDOUT: uselistorder ptr @"_COp.607f187990247af9:core.Destroy.Core", { 3, 2, 1, 0 }
 // CHECK:STDOUT: uselistorder ptr @"_COp.7e389eab4a7e5487:core.Destroy.Core", { 2, 1, 0 }
 // CHECK:STDOUT: uselistorder ptr @llvm.lifetime.start.p0, { 6, 5, 4, 3, 2, 1, 0 }
+// CHECK:STDOUT: uselistorder ptr @llvm.memcpy.p0.p0.i64, { 2, 1, 0 }
 // CHECK:STDOUT:
 // CHECK:STDOUT: attributes #0 = { nounwind }
 // CHECK:STDOUT: attributes #1 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT: attributes #2 = { nocallback nofree nounwind willreturn memory(argmem: readwrite) }
 // CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
 // CHECK:STDOUT: !llvm.dbg.cu = !{!2}

+ 5 - 0
toolchain/sem_ir/pattern.cpp

@@ -55,6 +55,11 @@ auto GetFirstBindingNameFromPatternId(const File& sem_ir, InstId pattern_id)
 
     // TODO: Look through struct patterns.
 
+    if (auto form_pattern = inst.TryAs<FormParamPattern>()) {
+      // TODO: This introduces a name, but we don't model it as a binding.
+      return EntityNameId::None;
+    }
+
     auto [name_id, entity_name_id] = GetBoundEntityName(sem_ir, inst);
     CARBON_CHECK(entity_name_id.has_value(), "Unhandled pattern inst kind {0}",
                  inst);