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

When making a direct call to a thunk, inline the call in SemIR. (#5642)

This preserves the constant values of the arguments to the thunk, which
is important if the thunk requires conversion of an `IntLiteral` to some
other type. This should become unnecessary once we have form support,
but avoiding the indirection through a thunk function seems valuable
even once that support is in place.

To support this, track whether a function is a thunk on the Function
object, and if so, what the callee of the thunk is. This information is
also included in formatted SemIR when dumping the thunk.
Richard Smith 10 месяцев назад
Родитель
Сommit
4e5dccdbf7

+ 29 - 9
toolchain/check/call.cpp

@@ -13,7 +13,10 @@
 #include "toolchain/check/deduce.h"
 #include "toolchain/check/facet_type.h"
 #include "toolchain/check/function.h"
+#include "toolchain/check/import_ref.h"
 #include "toolchain/check/inst.h"
+#include "toolchain/check/name_ref.h"
+#include "toolchain/check/thunk.h"
 #include "toolchain/check/type.h"
 #include "toolchain/diagnostics/format_providers.h"
 #include "toolchain/sem_ir/builtin_function_kind.h"
@@ -245,17 +248,34 @@ static auto PerformCallToFunction(Context& context, SemIR::LocId loc_id,
       break;
   }
 
+  auto& callee = context.functions().Get(callee_function.function_id);
+
   // Convert the arguments to match the parameters.
-  auto converted_args_id = ConvertCallArgs(
-      context, loc_id, callee_function.self_id, arg_ids, return_slot_arg_id,
-      context.functions().Get(callee_function.function_id),
-      *callee_specific_id);
-  auto call_inst_id = GetOrAddInst<SemIR::Call>(context, loc_id,
-                                                {.type_id = return_info.type_id,
-                                                 .callee_id = callee_id,
-                                                 .args_id = converted_args_id});
+  auto converted_args_id =
+      ConvertCallArgs(context, loc_id, callee_function.self_id, arg_ids,
+                      return_slot_arg_id, callee, *callee_specific_id);
+
+  // If we're about to form a direct call to a thunk, inline it.
+  if (callee.special_function_kind ==
+      SemIR::Function::SpecialFunctionKind::Thunk) {
+    LoadImportRef(context, callee.thunk_decl_id());
+
+    // Name the thunk target within the enclosing scope of the thunk.
+    auto thunk_ref_id =
+        BuildNameRef(context, loc_id, callee.name_id, callee.thunk_decl_id(),
+                     callee_function.enclosing_specific_id);
+
+    // This recurses back into `PerformCall`. However, we never form a thunk to
+    // a thunk, so we only recurse once.
+    return PerformThunkCall(context, loc_id, callee_function.function_id,
+                            context.inst_blocks().Get(converted_args_id),
+                            thunk_ref_id);
+  }
 
-  return call_inst_id;
+  return GetOrAddInst<SemIR::Call>(context, loc_id,
+                                   {.type_id = return_info.type_id,
+                                    .callee_id = callee_id,
+                                    .args_id = converted_args_id});
 }
 
 auto PerformCall(Context& context, SemIR::LocId loc_id, SemIR::InstId callee_id,

+ 1 - 1
toolchain/check/eval.cpp

@@ -1717,7 +1717,7 @@ static auto MakeConstantForCall(EvalContext& eval_context,
     // Calls to builtins might be constant.
     builtin_kind = eval_context.functions()
                        .Get(callee_function.function_id)
-                       .builtin_function_kind;
+                       .builtin_function_kind();
     if (builtin_kind == SemIR::BuiltinFunctionKind::None) {
       // TODO: Eventually we'll want to treat some kinds of non-builtin
       // functions as producing constants.

+ 1 - 1
toolchain/check/handle_function.cpp

@@ -727,7 +727,7 @@ auto HandleParseNode(Context& context,
 
     auto& function = context.functions().Get(function_id);
     if (IsValidBuiltinDeclaration(context, function, builtin_kind)) {
-      function.builtin_function_kind = builtin_kind;
+      function.SetBuiltinFunction(builtin_kind);
       // Build an empty generic definition if this is a generic builtin.
       StartGenericDefinition(context, function.generic_id);
       FinishGenericDefinition(context, function.generic_id);

+ 19 - 3
toolchain/check/import_ref.cpp

@@ -1841,9 +1841,7 @@ static auto MakeFunctionDecl(ImportContext& context,
   function_decl.function_id = context.local_functions().Add(
       {GetIncompleteLocalEntityBase(context, function_decl_id, import_function),
        {.call_params_id = SemIR::InstBlockId::None,
-        .return_slot_pattern_id = SemIR::InstId::None,
-        .special_function_kind = import_function.special_function_kind,
-        .builtin_function_kind = import_function.builtin_function_kind}});
+        .return_slot_pattern_id = SemIR::InstId::None}});
 
   function_decl.type_id = GetFunctionType(
       context.local_context(), function_decl.function_id, specific_id);
@@ -1928,6 +1926,24 @@ static auto TryResolveTypedInst(ImportRefResolver& resolver,
     new_function.definition_id = new_function.first_owning_decl_id;
   }
 
+  switch (import_function.special_function_kind) {
+    case SemIR::Function::SpecialFunctionKind::None: {
+      break;
+    }
+    case SemIR::Function::SpecialFunctionKind::Builtin: {
+      new_function.SetBuiltinFunction(import_function.builtin_function_kind());
+      break;
+    }
+    case SemIR::Function::SpecialFunctionKind::Thunk: {
+      auto entity_name_id = resolver.local_entity_names().AddCanonical(
+          {.name_id = new_function.name_id,
+           .parent_scope_id = new_function.parent_scope_id});
+      new_function.SetThunk(AddImportRef(
+          resolver, import_function.thunk_decl_id(), entity_name_id));
+      break;
+    }
+  }
+
   return ResolveResult::Done(function_const_id, new_function.first_decl_id());
 }
 

+ 5 - 3
toolchain/check/testdata/impl/fail_call_invalid.carbon

@@ -148,7 +148,7 @@ fn InstanceCall(n: i32) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @G.2(%self.param: <error>);
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @G.3(%self.param: %i32) {
+// CHECK:STDOUT: fn @G.3(%self.param: %i32) [thunk @impl.006.%G.decl.loc23_27.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %G.ref: %G.type.c9825d.1 = name_ref G, @impl.006.%G.decl.loc23_27.1 [concrete = constants.%G.e73e91.1]
 // CHECK:STDOUT:   %self.ref: %i32 = name_ref self, %self.param
@@ -161,10 +161,12 @@ fn InstanceCall(n: i32) {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %n.ref: %i32 = name_ref n, %n
 // CHECK:STDOUT:   %Simple.ref: type = name_ref Simple, file.%Simple.decl [concrete = constants.%Simple.type]
-// CHECK:STDOUT:   %G.ref: %Simple.assoc_type = name_ref G, @Simple.%assoc0 [concrete = constants.%assoc0.db2]
+// CHECK:STDOUT:   %G.ref.loc27_12: %Simple.assoc_type = name_ref G, @Simple.%assoc0 [concrete = constants.%assoc0.db2]
 // CHECK:STDOUT:   %impl.elem0: %.8e6 = impl_witness_access constants.%Simple.impl_witness, element0 [concrete = constants.%G.e73e91.2]
 // CHECK:STDOUT:   %bound_method: <bound method> = bound_method %n.ref, %impl.elem0
-// CHECK:STDOUT:   %G.call: init %empty_tuple.type = call %bound_method(%n.ref)
+// CHECK:STDOUT:   %G.ref.loc27_16: %G.type.c9825d.1 = name_ref G, @impl.006.%G.decl.loc23_27.1 [concrete = constants.%G.e73e91.1]
+// CHECK:STDOUT:   %G.bound: <bound method> = bound_method %n.ref, %G.ref.loc27_16
+// CHECK:STDOUT:   %G.call: init %empty_tuple.type = call %G.bound(<error>)
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 23 - 15
toolchain/check/testdata/impl/fail_impl_bad_assoc_fn.carbon

@@ -1199,7 +1199,7 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2(%b.param: bool);
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3() {
+// CHECK:STDOUT: fn @F.3() [thunk @impl.ddd.%F.decl.loc68_18.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.44ef8c.1 = name_ref F, @impl.ddd.%F.decl.loc68_18.1 [concrete = constants.%F.424e9e.1]
 // CHECK:STDOUT:   return
@@ -1207,7 +1207,7 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.4(%self.param: %FExtraImplicitParam);
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.5() {
+// CHECK:STDOUT: fn @F.5() [thunk @impl.698.%F.decl.loc84_23.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.e1abdd.1 = name_ref F, @impl.698.%F.decl.loc84_23.1 [concrete = constants.%F.6ff574.1]
 // CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.ref(<error>)
@@ -1222,22 +1222,24 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.8(%self.param: bool) -> bool;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.9(%self.param: bool, %b.param: bool) -> bool {
+// CHECK:STDOUT: fn @F.9(%self.param: bool, %b.param: bool) -> bool [thunk @impl.5cf.%F.decl.loc116_31.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.69596c.1 = name_ref F, @impl.5cf.%F.decl.loc116_31.1 [concrete = constants.%F.738f31.1]
 // CHECK:STDOUT:   %self.ref: bool = name_ref self, %self.param
-// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %b.ref: bool = name_ref b, %b.param
+// CHECK:STDOUT:   %return.ref: ref bool = name_ref <return slot>, %return.param
+// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.10(%b.param: bool) -> bool;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.11(%self.param: bool, %b.param: bool) -> bool {
+// CHECK:STDOUT: fn @F.11(%self.param: bool, %b.param: bool) -> bool [thunk @impl.bac.%F.decl.loc129_26.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.d97cef.1 = name_ref F, @impl.bac.%F.decl.loc129_26.1 [concrete = constants.%F.01de92.1]
 // CHECK:STDOUT:   %self.ref: bool = name_ref self, %self.param
 // CHECK:STDOUT:   %b.ref: bool = name_ref b, %b.param
+// CHECK:STDOUT:   %return.ref: ref bool = name_ref <return slot>, %return.param
 // CHECK:STDOUT:   %F.call: init bool = call %F.ref(%b.ref)
 // CHECK:STDOUT:   %.loc129_26.1: bool = value_of_initializer %F.call
 // CHECK:STDOUT:   %.loc129_26.2: bool = converted %F.call, %.loc129_26.1
@@ -1246,12 +1248,13 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.12(%self.param: bool, %b.param: bool);
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.13(%self.param: bool, %b.param: bool) -> bool {
+// CHECK:STDOUT: fn @F.13(%self.param: bool, %b.param: bool) -> bool [thunk @impl.1a7.%F.decl.loc151_30.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.123d7a.1 = name_ref F, @impl.1a7.%F.decl.loc151_30.1 [concrete = constants.%F.c7d02d.1]
 // CHECK:STDOUT:   %self.ref: bool = name_ref self, %self.param
-// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %b.ref: bool = name_ref b, %b.param
+// CHECK:STDOUT:   %return.ref: ref bool = name_ref <return slot>, %return.param
+// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.bound(%self.ref, %b.ref)
 // CHECK:STDOUT:   %.loc151: bool = converted %F.call, <error> [concrete = <error>]
 // CHECK:STDOUT:   return <error>
@@ -1259,12 +1262,13 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.14(%self.param: bool, %b.param: %FDifferentParamType) -> bool;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.15(%self.param: bool, %b.param: bool) -> bool {
+// CHECK:STDOUT: fn @F.15(%self.param: bool, %b.param: bool) -> bool [thunk @impl.f2b.%F.decl.loc170_38.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.6b537d.1 = name_ref F, @impl.f2b.%F.decl.loc170_38.1 [concrete = constants.%F.04313a.1]
 // CHECK:STDOUT:   %self.ref: bool = name_ref self, %self.param
-// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %b.ref: bool = name_ref b, %b.param
+// CHECK:STDOUT:   %return.ref: ref bool = name_ref <return slot>, %return.param
+// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %.loc102: %FDifferentParamType = converted %b.ref, <error> [concrete = <error>]
 // CHECK:STDOUT:   %F.call: init bool = call %F.bound(%self.ref, <error>)
 // CHECK:STDOUT:   %.loc170_38.1: bool = value_of_initializer %F.call
@@ -1274,12 +1278,13 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.16(%self.param: %FDifferentImplicitParamType, %b.param: bool) -> bool;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.17(%self.param: bool, %b.param: bool) -> bool {
+// CHECK:STDOUT: fn @F.17(%self.param: bool, %b.param: bool) -> bool [thunk @impl.db4.%F.decl.loc183_38.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.d6232a.1 = name_ref F, @impl.db4.%F.decl.loc183_38.1 [concrete = constants.%F.886f70.1]
 // CHECK:STDOUT:   %self.ref: bool = name_ref self, %self.param
-// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %b.ref: bool = name_ref b, %b.param
+// CHECK:STDOUT:   %return.ref: ref bool = name_ref <return slot>, %return.param
+// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %.loc102: %FDifferentImplicitParamType = converted %self.ref, <error> [concrete = <error>]
 // CHECK:STDOUT:   %F.call: init bool = call %F.bound(<error>, %b.ref)
 // CHECK:STDOUT:   %.loc183_38.1: bool = value_of_initializer %F.call
@@ -1289,12 +1294,13 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.18(%self.param: bool, %b.param: bool) -> %return.param: %FDifferentReturnType;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.19(%self.param: bool, %b.param: bool) -> bool {
+// CHECK:STDOUT: fn @F.19(%self.param: bool, %b.param: bool) -> bool [thunk @impl.fcc.%F.decl.loc199_38.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.d3b58f.1 = name_ref F, @impl.fcc.%F.decl.loc199_38.1 [concrete = constants.%F.be86c9.1]
 // CHECK:STDOUT:   %self.ref: bool = name_ref self, %self.param
-// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %b.ref: bool = name_ref b, %b.param
+// CHECK:STDOUT:   %return.ref: ref bool = name_ref <return slot>, %return.param
+// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %.loc199_38.1: ref %FDifferentReturnType = temporary_storage
 // CHECK:STDOUT:   %F.call: init %FDifferentReturnType = call %F.bound(%self.ref, %b.ref) to %.loc199_38.1
 // CHECK:STDOUT:   %.loc199_38.2: bool = converted %F.call, <error> [concrete = <error>]
@@ -1316,10 +1322,11 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.21(%x.param: %tuple.type.a7d) -> %return.param: %array_type.a41;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.22(%x.param: %tuple.type.9c9) -> %return.param: %array_type.a41 {
+// CHECK:STDOUT: fn @F.22(%x.param: %tuple.type.9c9) -> %return.param: %array_type.a41 [thunk @impl.6a5.%F.decl.loc222_87.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.f90165.1 = name_ref F, @impl.6a5.%F.decl.loc222_87.1 [concrete = constants.%F.fa8d04.1]
 // CHECK:STDOUT:   %x.ref: %tuple.type.9c9 = name_ref x, %x.param
+// CHECK:STDOUT:   %return.ref: ref %array_type.a41 = name_ref <return slot>, %return.param
 // CHECK:STDOUT:   %.loc210_41: ref %array_type.a41 = splice_block %return {}
 // CHECK:STDOUT:   %tuple.elem0: %ptr.4cd = tuple_access %x.ref, element0
 // CHECK:STDOUT:   %tuple.elem1: %struct_type.x.y.a89 = tuple_access %x.ref, element1
@@ -1331,10 +1338,11 @@ class SelfNestedBadReturnType {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.23(%x.param: %tuple.type.eb9) -> %return.param: %array_type.a41;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.24(%x.param: %tuple.type.eb9) -> %return.param: %array_type.126 {
+// CHECK:STDOUT: fn @F.24(%x.param: %tuple.type.eb9) -> %return.param: %array_type.126 [thunk @impl.bfc.%F.decl.loc238_112.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.0e7d1d.1 = name_ref F, @impl.bfc.%F.decl.loc238_112.1 [concrete = constants.%F.0bc78a.1]
 // CHECK:STDOUT:   %x.ref: %tuple.type.eb9 = name_ref x, %x.param
+// CHECK:STDOUT:   %return.ref: ref %array_type.126 = name_ref <return slot>, %return.param
 // CHECK:STDOUT:   %.loc238_112.1: ref %array_type.a41 = temporary_storage
 // CHECK:STDOUT:   %F.call: init %array_type.a41 = call %F.ref(%x.ref) to %.loc238_112.1
 // CHECK:STDOUT:   %.loc238_112.2: %array_type.126 = converted %F.call, <error> [concrete = <error>]

+ 1 - 1
toolchain/check/testdata/impl/fail_self_type_mismatch.carbon

@@ -240,7 +240,7 @@ impl i32 as I {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.3(%c.param: %C.6fb);
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.4(%c.param: %C.d88) {
+// CHECK:STDOUT: fn @F.4(%c.param: %C.d88) [thunk @impl.a9a.%F.decl.loc42_18.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.066a53.1 = name_ref F, @impl.a9a.%F.decl.loc42_18.1 [concrete = constants.%F.9ec58f.1]
 // CHECK:STDOUT:   %c.ref: %C.d88 = name_ref c, %c.param

+ 10 - 8
toolchain/check/testdata/impl/impl_thunk.carbon

@@ -359,7 +359,7 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2(%y.param: %struct_type.b.a.40c) -> %return.param: %struct_type.d.c.b36;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%x.param: %struct_type.a.b.391) -> %return.param: %struct_type.c.d.15a {
+// CHECK:STDOUT: fn @F.3(%x.param: %struct_type.a.b.391) -> %return.param: %struct_type.c.d.15a [thunk @impl.%F.decl.loc10_48.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.39e918.1 = name_ref F, @impl.%F.decl.loc10_48.1 [concrete = constants.%F.c04b92.1]
 // CHECK:STDOUT:   <elided>
@@ -440,7 +440,7 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2(%self.param: %ptr.6db, %other.param: %ptr.6db) -> %ptr.019;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%self.param: %ptr.e79, %other.param: %ptr.e79) -> %ptr.e79 {
+// CHECK:STDOUT: fn @F.3(%self.param: %ptr.e79, %other.param: %ptr.e79) -> %ptr.e79 [thunk @impl.%F.decl.loc14_39.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.f1b0b1.1 = name_ref F, @impl.%F.decl.loc14_39.1 [concrete = constants.%F.5161e9.1]
 // CHECK:STDOUT:   <elided>
@@ -497,7 +497,7 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2(%self.param: %A, %other.param: %A);
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%self.param: %B, %other.param: %B) {
+// CHECK:STDOUT: fn @F.3(%self.param: %B, %other.param: %B) [thunk @impl.%F.decl.loc13_26.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.f1b0b1.1 = name_ref F, @impl.%F.decl.loc13_26.1 [concrete = constants.%F.5161e9.1]
 // CHECK:STDOUT:   <elided>
@@ -567,7 +567,7 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2(%self.param: %ptr.6db, %other.param: %ptr.6db) -> %ptr.019;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%self.param: %ptr.e79, %other.param: %ptr.e79) -> %ptr.e79 {
+// CHECK:STDOUT: fn @F.3(%self.param: %ptr.e79, %other.param: %ptr.e79) -> %ptr.e79 [thunk @impl.%F.decl.loc14_39.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.f1b0b1.1 = name_ref F, @impl.%F.decl.loc14_39.1 [concrete = constants.%F.5161e9.1]
 // CHECK:STDOUT:   <elided>
@@ -618,9 +618,10 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2() -> %return.param: %B;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3() -> %return.param: %A {
+// CHECK:STDOUT: fn @F.3() -> %return.param: %A [thunk @impl.%F.decl.loc20_14.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.b24d6f.1 = name_ref F, @impl.%F.decl.loc20_14.1 [concrete = constants.%F.77e9d5.1]
+// CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:   %.loc20_14.1: ref %B = temporary_storage
 // CHECK:STDOUT:   %F.call: init %B = call %F.ref() to %.loc20_14.1
 // CHECK:STDOUT:   %.loc20_14.2: ref %B = temporary %.loc20_14.1, %F.call
@@ -696,7 +697,7 @@ impl () as I({}) {
 // CHECK:STDOUT:   %F.specific_fn.loc10_29.2: <specific function> = specific_function constants.%F.c04b92.1, @F.2(%ptr) [symbolic = %F.specific_fn.loc10_29.2 (constants.%F.specific_fn)]
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:
-// CHECK:STDOUT:   fn(%x.param: @F.3.%ptr (%ptr.79f)) -> @F.3.%ptr (%ptr.79f) {
+// CHECK:STDOUT:   fn(%x.param: @F.3.%ptr (%ptr.79f)) -> @F.3.%ptr (%ptr.79f) [thunk @impl.%F.decl.loc10_29.1] {
 // CHECK:STDOUT:   !entry:
 // CHECK:STDOUT:     %F.ref: %F.type.39e918.1 = name_ref F, @impl.%F.decl.loc10_29.1 [concrete = constants.%F.c04b92.1]
 // CHECK:STDOUT:     <elided>
@@ -799,7 +800,7 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: !definition:
 // CHECK:STDOUT:
-// CHECK:STDOUT:   fn(%self.param: %empty_tuple.type, %x.param: @F.3.%ptr (%ptr.79f)) -> @F.3.%ptr (%ptr.79f) {
+// CHECK:STDOUT:   fn(%self.param: %empty_tuple.type, %x.param: @F.3.%ptr (%ptr.79f)) -> @F.3.%ptr (%ptr.79f) [thunk @impl.%F.decl.loc19_39.1] {
 // CHECK:STDOUT:   !entry:
 // CHECK:STDOUT:     %F.ref: %F.type.39e918.1 = name_ref F, @impl.%F.decl.loc19_39.1 [concrete = constants.%F.c04b92.1]
 // CHECK:STDOUT:     <elided>
@@ -859,9 +860,10 @@ impl () as I({}) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F.2() -> %return.param: %struct_type.b.a.1b0;
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3() -> %return.param: %struct_type.a.b.f95 {
+// CHECK:STDOUT: fn @F.3() -> %return.param: %struct_type.a.b.f95 [thunk @impl.%F.decl.loc10_29.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.29ab63.1 = name_ref F, @impl.%F.decl.loc10_29.1 [concrete = constants.%F.975709.1]
+// CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:   %.loc10_29.1: ref %struct_type.b.a.1b0 = temporary_storage
 // CHECK:STDOUT:   %F.call: init %struct_type.b.a.1b0 = call %F.ref() to %.loc10_29.1
 // CHECK:STDOUT:   %.loc10_29.2: ref %struct_type.b.a.1b0 = temporary %.loc10_29.1, %F.call

+ 81 - 0
toolchain/check/testdata/impl/impl_thunk_min_prelude.carbon

@@ -78,3 +78,84 @@ interface OpWith(U:! type) {
 impl forall [T:! type, U:! Core.ImplicitAs(Wrap(T))] Wrap(T) as OpWith(U) {
   fn Op[self: Self](other: Self) = "no_op";
 }
+
+// --- thunk_literal_convert.carbon
+
+library "[[@TEST_NAME]]";
+
+fn IntLiteral() -> type = "int_literal.make_type";
+fn Int(size: IntLiteral()) -> type = "int.make_type_signed";
+
+impl IntLiteral() as Core.ImplicitAs(Int(32)) {
+  fn Convert[self: Self]() -> Int(32) = "int.convert_checked";
+}
+
+interface Add(T:! type) {
+  fn Op(a: Self, b: T) -> Self;
+}
+impl forall [T:! Core.ImplicitAs(Int(32))] Int(32) as Add(T) {
+  fn Op(a: Self, b: Self) -> Self = "int.sadd";
+}
+
+fn Call() -> Int(32) {
+  let a: Int(32) = 1;
+  // The conversion from 2 to IntLiteral here relies on having the
+  // constant value available, so is only possible if the thunk is
+  // inlined.
+  return Int(32).(Add(IntLiteral()).Op)
+  //@dump-sem-ir-begin
+        (a, 2)
+  //@dump-sem-ir-end
+  ;
+}
+
+// CHECK:STDOUT: --- thunk_literal_convert.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
+// CHECK:STDOUT:   %i32.builtin: type = int_type signed, %int_32 [concrete]
+// CHECK:STDOUT:   %ImplicitAs.type.61e: type = facet_type <@ImplicitAs, @ImplicitAs(%i32.builtin)> [concrete]
+// CHECK:STDOUT:   %Convert.type.059: type = fn_type @Convert.1, @ImplicitAs(%i32.builtin) [concrete]
+// CHECK:STDOUT:   %ImplicitAs.impl_witness: <witness> = impl_witness file.%ImplicitAs.impl_witness_table [concrete]
+// CHECK:STDOUT:   %Convert.type.1dd: type = fn_type @Convert.2 [concrete]
+// CHECK:STDOUT:   %Convert.52c: %Convert.type.1dd = struct_value () [concrete]
+// CHECK:STDOUT:   %ImplicitAs.facet.54f: %ImplicitAs.type.61e = facet_value Core.IntLiteral, (%ImplicitAs.impl_witness) [concrete]
+// CHECK:STDOUT:   %.f22: type = fn_type_with_self_type %Convert.type.059, %ImplicitAs.facet.54f [concrete]
+// CHECK:STDOUT:   %Op.type.049d3a.1: type = fn_type @Op.2, @impl.797(%ImplicitAs.facet.54f) [concrete]
+// CHECK:STDOUT:   %Op.1fb3b5.1: %Op.type.049d3a.1 = struct_value () [concrete]
+// CHECK:STDOUT:   %int_2.ecc: Core.IntLiteral = int_value 2 [concrete]
+// CHECK:STDOUT:   %Convert.bound.713: <bound method> = bound_method %int_2.ecc, %Convert.52c [concrete]
+// CHECK:STDOUT:   %int_2.5a1: %i32.builtin = int_value 2 [concrete]
+// CHECK:STDOUT:   %Op.specific_fn.01379b.2: <specific function> = specific_function %Op.1fb3b5.1, @Op.2(%ImplicitAs.facet.54f) [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Call() -> %i32.builtin {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %a.ref: %i32.builtin = name_ref a, %a
+// CHECK:STDOUT:   %int_2: Core.IntLiteral = int_value 2 [concrete = constants.%int_2.ecc]
+// CHECK:STDOUT:   %ImplicitAs.facet.loc25_14.1: %ImplicitAs.type.61e = facet_value Core.IntLiteral, (constants.%ImplicitAs.impl_witness) [concrete = constants.%ImplicitAs.facet.54f]
+// CHECK:STDOUT:   %.loc25_14.1: %ImplicitAs.type.61e = converted Core.IntLiteral, %ImplicitAs.facet.loc25_14.1 [concrete = constants.%ImplicitAs.facet.54f]
+// CHECK:STDOUT:   %ImplicitAs.facet.loc25_14.2: %ImplicitAs.type.61e = facet_value Core.IntLiteral, (constants.%ImplicitAs.impl_witness) [concrete = constants.%ImplicitAs.facet.54f]
+// CHECK:STDOUT:   %.loc25_14.2: %ImplicitAs.type.61e = converted Core.IntLiteral, %ImplicitAs.facet.loc25_14.2 [concrete = constants.%ImplicitAs.facet.54f]
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %.loc25_14.3: %Op.type.049d3a.1 = specific_constant @impl.797.%Op.decl.loc15_35.1, @impl.797(constants.%ImplicitAs.facet.54f) [concrete = constants.%Op.1fb3b5.1]
+// CHECK:STDOUT:   %Op.ref.loc25: %Op.type.049d3a.1 = name_ref Op, %.loc25_14.3 [concrete = constants.%Op.1fb3b5.1]
+// CHECK:STDOUT:   %impl.elem0.loc25_14: %.f22 = impl_witness_access constants.%ImplicitAs.impl_witness, element0 [concrete = constants.%Convert.52c]
+// CHECK:STDOUT:   %bound_method.loc25_14: <bound method> = bound_method %int_2, %impl.elem0.loc25_14 [concrete = constants.%Convert.bound.713]
+// CHECK:STDOUT:   %int.convert_checked.loc25_14: init %i32.builtin = call %bound_method.loc25_14(%int_2) [concrete = constants.%int_2.5a1]
+// CHECK:STDOUT:   %.loc25_14.4: %i32.builtin = value_of_initializer %int.convert_checked.loc25_14 [concrete = constants.%int_2.5a1]
+// CHECK:STDOUT:   %.loc25_14.5: %i32.builtin = converted %int_2, %.loc25_14.4 [concrete = constants.%int_2.5a1]
+// CHECK:STDOUT:   %Op.specific_fn: <specific function> = specific_function %Op.ref.loc25, @Op.2(constants.%ImplicitAs.facet.54f) [concrete = constants.%Op.specific_fn.01379b.2]
+// CHECK:STDOUT:   %impl.elem0.loc25_13: %.f22 = impl_witness_access constants.%ImplicitAs.impl_witness, element0 [concrete = constants.%Convert.52c]
+// CHECK:STDOUT:   %bound_method.loc25_13: <bound method> = bound_method %int_2, %impl.elem0.loc25_13 [concrete = constants.%Convert.bound.713]
+// CHECK:STDOUT:   %int.convert_checked.loc25_13: init %i32.builtin = call %bound_method.loc25_13(%int_2) [concrete = constants.%int_2.5a1]
+// CHECK:STDOUT:   %.loc25_13.1: %i32.builtin = value_of_initializer %int.convert_checked.loc25_13 [concrete = constants.%int_2.5a1]
+// CHECK:STDOUT:   %.loc25_13.2: %i32.builtin = converted %int_2, %.loc25_13.1 [concrete = constants.%int_2.5a1]
+// CHECK:STDOUT:   %int.sadd: init %i32.builtin = call %Op.specific_fn(%a.ref, %.loc25_13.2)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:

+ 16 - 7
toolchain/check/testdata/impl/import_thunk.carbon

@@ -247,7 +247,7 @@ fn G() {
 // CHECK:STDOUT:   %require_complete: <witness> = require_complete_type %C [symbolic = %require_complete (constants.%require_complete)]
 // CHECK:STDOUT:   %C.val: @F.3.%C (%C.13320f.2) = struct_value () [symbolic = %C.val (constants.%C.val)]
 // CHECK:STDOUT:
-// CHECK:STDOUT:   fn(%x.param: %empty_struct_type) {
+// CHECK:STDOUT:   fn(%x.param: %empty_struct_type) [thunk @impl.%F.decl.loc8_17.1] {
 // CHECK:STDOUT:   !entry:
 // CHECK:STDOUT:     %.loc8: @F.3.%F.type (%F.type.0daaa1.1) = specific_constant @impl.%F.decl.loc8_17.1, @impl(constants.%Y) [symbolic = %F (constants.%F.49c1ac.1)]
 // CHECK:STDOUT:     %F.ref: @F.3.%F.type (%F.type.0daaa1.1) = name_ref F, %.loc8 [symbolic = %F (constants.%F.49c1ac.1)]
@@ -352,7 +352,7 @@ fn G() {
 // CHECK:STDOUT:   %Main.import_ref.572 = import_ref Main//b, inst29 [no loc], unloaded
 // CHECK:STDOUT:   %Main.import_ref.e5d = import_ref Main//a, inst17 [no loc], unloaded
 // CHECK:STDOUT:   %Main.import_ref.c44: %I.assoc_type = import_ref Main//a, loc5_14, loaded [concrete = constants.%assoc0]
-// CHECK:STDOUT:   %Main.F = import_ref Main//a, F, unloaded
+// CHECK:STDOUT:   %Main.F.8b9 = import_ref Main//a, F, unloaded
 // CHECK:STDOUT:   %Main.import_ref.e03: %F.type.cf0 = import_ref Main//a, loc5_14, loaded [concrete = constants.%F.bc6]
 // CHECK:STDOUT:   %Main.import_ref.5dd: %I.type = import_ref Main//a, inst17 [no loc], loaded [symbolic = constants.%Self]
 // CHECK:STDOUT:   %Main.import_ref.f89: <witness> = import_ref Main//b, loc7_32, loaded [symbolic = @impl.%I.impl_witness (constants.%I.impl_witness.7d9)]
@@ -363,6 +363,7 @@ fn G() {
 // CHECK:STDOUT:   %I.impl_witness_table = impl_witness_table (%Main.import_ref.047), @impl [concrete]
 // CHECK:STDOUT:   %Main.import_ref.eb1c17.3: %empty_tuple.type = import_ref Main//b, loc7_14, loaded [symbolic = @impl.%Y (constants.%Y)]
 // CHECK:STDOUT:   %Main.import_ref.eb1c17.4: %empty_tuple.type = import_ref Main//b, loc7_14, loaded [symbolic = @impl.%Y (constants.%Y)]
+// CHECK:STDOUT:   %Main.F.5a8: @impl.%F.type.1 (%F.type.0daaa1.1) = import_ref Main//b, F, loaded [symbolic = @impl.%F.1 (constants.%F.49c1ac.1)]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -379,7 +380,7 @@ fn G() {
 // CHECK:STDOUT: !members:
 // CHECK:STDOUT:   .Self = imports.%Main.import_ref.e5d
 // CHECK:STDOUT:   .F = imports.%Main.import_ref.c44
-// CHECK:STDOUT:   witness = (imports.%Main.F)
+// CHECK:STDOUT:   witness = (imports.%Main.F.8b9)
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: generic impl @impl(imports.%Main.import_ref.eb1c17.2: %empty_tuple.type) [from "b.carbon"] {
@@ -420,17 +421,25 @@ fn G() {
 // CHECK:STDOUT:   %.loc7_7: %empty_tuple.type = converted %.loc7_6, %empty_tuple [concrete = constants.%empty_tuple]
 // CHECK:STDOUT:   %C: type = class_type @C, @C(constants.%empty_tuple) [concrete = constants.%C.607]
 // CHECK:STDOUT:   %I.ref: type = name_ref I, imports.%Main.I [concrete = constants.%I.type]
-// CHECK:STDOUT:   %F.ref: %I.assoc_type = name_ref F, imports.%Main.import_ref.c44 [concrete = constants.%assoc0]
+// CHECK:STDOUT:   %F.ref.loc7_11: %I.assoc_type = name_ref F, imports.%Main.import_ref.c44 [concrete = constants.%assoc0]
 // CHECK:STDOUT:   %I.facet: %I.type = facet_value constants.%C.607, (constants.%I.impl_witness.02b) [concrete = constants.%I.facet]
 // CHECK:STDOUT:   %.loc7_8: %I.type = converted %C, %I.facet [concrete = constants.%I.facet]
 // CHECK:STDOUT:   %impl.elem0: %.885 = impl_witness_access constants.%I.impl_witness.02b, element0 [concrete = constants.%F.5fa954.2]
 // CHECK:STDOUT:   %.loc7_16.1: %empty_struct_type = struct_literal ()
 // CHECK:STDOUT:   %empty_struct.loc7_16.1: %empty_struct_type = struct_value () [concrete = constants.%empty_struct]
-// CHECK:STDOUT:   %.loc7_17: %empty_struct_type = converted %.loc7_16.1, %empty_struct.loc7_16.1 [concrete = constants.%empty_struct]
+// CHECK:STDOUT:   %.loc7_17.1: %empty_struct_type = converted %.loc7_16.1, %empty_struct.loc7_16.1 [concrete = constants.%empty_struct]
 // CHECK:STDOUT:   %specific_fn: <specific function> = specific_function %impl.elem0, @F.3(constants.%empty_tuple) [concrete = constants.%F.specific_fn.4832e8.1]
 // CHECK:STDOUT:   %empty_struct.loc7_16.2: %empty_struct_type = struct_value () [concrete = constants.%empty_struct]
 // CHECK:STDOUT:   %.loc7_16.2: %empty_struct_type = converted %.loc7_16.1, %empty_struct.loc7_16.2 [concrete = constants.%empty_struct]
-// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %specific_fn(%.loc7_16.2)
+// CHECK:STDOUT:   %.loc7_17.2: %F.type.af4856.1 = specific_constant imports.%Main.F.5a8, @impl(constants.%empty_tuple) [concrete = constants.%F.5fa954.1]
+// CHECK:STDOUT:   %F.ref.loc7_17: %F.type.af4856.1 = name_ref F, %.loc7_17.2 [concrete = constants.%F.5fa954.1]
+// CHECK:STDOUT:   %F.specific_fn: <specific function> = specific_function %F.ref.loc7_17, @F.2(constants.%empty_tuple) [concrete = constants.%F.specific_fn.4832e8.2]
+// CHECK:STDOUT:   %.loc7_16.3: ref %C.607 = temporary_storage
+// CHECK:STDOUT:   %.loc7_16.4: init %C.607 = class_init (), %.loc7_16.3 [concrete = constants.%C.val.12f]
+// CHECK:STDOUT:   %.loc7_16.5: ref %C.607 = temporary %.loc7_16.3, %.loc7_16.4
+// CHECK:STDOUT:   %.loc7_16.6: ref %C.607 = converted %.loc7_16.2, %.loc7_16.5
+// CHECK:STDOUT:   %.loc7_16.7: %C.607 = bind_value %.loc7_16.6
+// CHECK:STDOUT:   %F.call: init %empty_tuple.type = call %F.specific_fn(%.loc7_16.7)
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -459,7 +468,7 @@ fn G() {
 // CHECK:STDOUT:   %require_complete: <witness> = require_complete_type %C [symbolic = %require_complete (constants.%require_complete)]
 // CHECK:STDOUT:   %C.val: @F.3.%C (%C.13320f.2) = struct_value () [symbolic = %C.val (constants.%C.val.56a)]
 // CHECK:STDOUT:
-// CHECK:STDOUT:   fn;
+// CHECK:STDOUT:   fn [thunk imports.%Main.F.5a8];
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: specific @C(constants.%X) {

+ 9 - 5
toolchain/check/testdata/impl/use_assoc_const.carbon

@@ -2420,10 +2420,11 @@ fn F() {
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%u.param: %i32) -> %i32 {
+// CHECK:STDOUT: fn @F.3(%u.param: %i32) -> %i32 [thunk @impl.%F.decl.loc24_21.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.b842fd.1 = name_ref F, @impl.%F.decl.loc24_21.1 [concrete = constants.%F.b07d12.1]
 // CHECK:STDOUT:   %u.ref: %i32 = name_ref u, %u.param
+// CHECK:STDOUT:   %return.ref: ref %i32 = name_ref <return slot>, %return.param
 // CHECK:STDOUT:   %F.call: init <error> = call %F.ref(<error>)
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }
@@ -2688,11 +2689,12 @@ fn F() {
 // CHECK:STDOUT:   return %z.ref
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%self.param: %empty_tuple.type, %.param: <error>) -> <error> {
+// CHECK:STDOUT: fn @F.3(%self.param: %empty_tuple.type, %.param: <error>) -> <error> [thunk @impl.4e9.%F.decl.loc30_33.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.f405c5.1 = name_ref F, @impl.4e9.%F.decl.loc30_33.1 [concrete = constants.%F.214a71.1]
 // CHECK:STDOUT:   %self.ref: %empty_tuple.type = name_ref self, %self.param
 // CHECK:STDOUT:   %.ref: <error> = name_ref <none>, %.param [concrete = <error>]
+// CHECK:STDOUT:   %return.ref: ref <error> = name_ref <return slot>, %return.param [concrete = <error>]
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -2702,12 +2704,13 @@ fn F() {
 // CHECK:STDOUT:   return %self.ref
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.5(%self.param: %C2, %.param: <error>) -> <error> {
+// CHECK:STDOUT: fn @F.5(%self.param: %C2, %.param: <error>) -> <error> [thunk @impl.8b3.%F.decl.loc39_33.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.05d6a0.1 = name_ref F, @impl.8b3.%F.decl.loc39_33.1 [concrete = constants.%F.bfa759.1]
 // CHECK:STDOUT:   %self.ref: %C2 = name_ref self, %self.param
-// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %.ref: <error> = name_ref <none>, %.param [concrete = <error>]
+// CHECK:STDOUT:   %return.ref: ref <error> = name_ref <return slot>, %return.param [concrete = <error>]
+// CHECK:STDOUT:   %F.bound: <bound method> = bound_method %self.ref, %F.ref
 // CHECK:STDOUT:   %F.call: init %C2 = call %F.bound(%self.ref, <error>)
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }
@@ -2913,11 +2916,12 @@ fn F() {
 // CHECK:STDOUT:   return %v.ref
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @F.3(%self.param: %empty_tuple.type, %v.param: %struct_type.a) -> %struct_type.a {
+// CHECK:STDOUT: fn @F.3(%self.param: %empty_tuple.type, %v.param: %struct_type.a) -> %struct_type.a [thunk @impl.%F.decl.loc16_45.1] {
 // CHECK:STDOUT: !entry:
 // CHECK:STDOUT:   %F.ref: %F.type.c5249c.1 = name_ref F, @impl.%F.decl.loc16_45.1 [concrete = constants.%F.5eb4dd.1]
 // CHECK:STDOUT:   %self.ref: %empty_tuple.type = name_ref self, %self.param
 // CHECK:STDOUT:   %v.ref: %struct_type.a = name_ref v, %v.param
+// CHECK:STDOUT:   %return.ref: ref %struct_type.a = name_ref <return slot>, %return.param
 // CHECK:STDOUT:   return <error>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 61 - 48
toolchain/check/thunk.cpp

@@ -191,26 +191,25 @@ static auto CloneFunctionDecl(Context& context, SemIR::LocId loc_id,
 
   // Create the `Function` object.
   auto& callee = context.functions().Get(callee_id);
-  function_decl.function_id = context.functions().Add(SemIR::Function{
-      {.name_id = signature.name_id,
-       .parent_scope_id = callee.parent_scope_id,
-       .generic_id = generic_id,
-       .first_param_node_id = signature.first_param_node_id,
-       .last_param_node_id = signature.last_param_node_id,
-       .pattern_block_id = pattern_block_id,
-       .implicit_param_patterns_id = implicit_param_patterns_id,
-       .param_patterns_id = param_patterns_id,
-       .is_extern = false,
-       .extern_library_id = SemIR::LibraryNameId::None,
-       .non_owning_decl_id = SemIR::InstId::None,
-       .first_owning_decl_id = decl_id,
-       .definition_id = decl_id},
-      {.call_params_id = call_params_id,
-       .return_slot_pattern_id = return_slot_pattern_id,
-       .special_function_kind = SemIR::Function::SpecialFunctionKind::Thunk,
-       .virtual_modifier = callee.virtual_modifier,
-       .virtual_index = callee.virtual_index,
-       .self_param_id = self_param_id}});
+  function_decl.function_id = context.functions().Add(
+      SemIR::Function{{.name_id = signature.name_id,
+                       .parent_scope_id = callee.parent_scope_id,
+                       .generic_id = generic_id,
+                       .first_param_node_id = signature.first_param_node_id,
+                       .last_param_node_id = signature.last_param_node_id,
+                       .pattern_block_id = pattern_block_id,
+                       .implicit_param_patterns_id = implicit_param_patterns_id,
+                       .param_patterns_id = param_patterns_id,
+                       .is_extern = false,
+                       .extern_library_id = SemIR::LibraryNameId::None,
+                       .non_owning_decl_id = SemIR::InstId::None,
+                       .first_owning_decl_id = decl_id,
+                       .definition_id = decl_id},
+                      {.call_params_id = call_params_id,
+                       .return_slot_pattern_id = return_slot_pattern_id,
+                       .virtual_modifier = callee.virtual_modifier,
+                       .virtual_index = callee.virtual_index,
+                       .self_param_id = self_param_id}});
   function_decl.type_id =
       GetFunctionType(context, function_decl.function_id,
                       context.scope_stack().PeekSpecificId());
@@ -269,6 +268,9 @@ auto BuildThunk(Context& context, SemIR::FunctionId signature_id,
       CloneFunctionDecl(context, SemIR::LocId(callee_id), signature_id,
                         signature_specific_id, callee.function_id);
 
+  // Track that this function is a thunk.
+  context.functions().Get(function_id).SetThunk(callee_id);
+
   // Register the thunk to be defined when we reach the end of the enclosing
   // deferred definition scope, for example an `impl` or `class` definition, as
   // if the thunk's body were written inline in this location.
@@ -284,7 +286,8 @@ auto BuildThunk(Context& context, SemIR::FunctionId signature_id,
 }
 
 // Build an expression that names the value matched by a pattern.
-static auto BuildPatternRef(Context& context, SemIR::FunctionId function_id,
+static auto BuildPatternRef(Context& context,
+                            llvm::ArrayRef<SemIR::InstId> arg_ids,
                             SemIR::InstId pattern_id) -> SemIR::InstId {
   auto pattern = context.insts().Get(pattern_id);
 
@@ -294,20 +297,7 @@ static auto BuildPatternRef(Context& context, SemIR::FunctionId function_id,
 
   auto pattern_ref_id = SemIR::InstId::None;
   if (auto value_param = pattern.TryAs<SemIR::ValueParamPattern>()) {
-    // Build a reference to this parameter.
-    auto call_param_id = context.inst_blocks().Get(
-        context.functions()
-            .Get(function_id)
-            .call_params_id)[value_param->index.index];
-    // Use a pretty name for the `name_ref`. While it's suspicious to use a
-    // pretty name in the IR like this, the only reason we include a name at
-    // all here is to make the formatted SemIR more readable.
-    pattern_ref_id = AddInst<SemIR::NameRef>(
-        context, SemIR::LocId(pattern_id),
-        {.type_id = context.insts().Get(call_param_id).type_id(),
-         .name_id = SemIR::GetPrettyNameFromPatternId(
-             context.sem_ir(), value_param->subpattern_id),
-         .value_id = call_param_id});
+    pattern_ref_id = arg_ids[value_param->index.index];
   } else {
     if (pattern_id != SemIR::ErrorInst::InstId) {
       context.TODO(
@@ -327,24 +317,17 @@ static auto BuildPatternRef(Context& context, SemIR::FunctionId function_id,
   return pattern_ref_id;
 }
 
-// Build a call to a function that forwards the arguments of the enclosing
-// function, for use when constructing a thunk.
-static auto BuildThunkCall(Context& context, SemIR::FunctionId function_id,
-                           SemIR::InstId callee_id) -> SemIR::InstId {
-  auto loc_id = SemIR::LocId(callee_id);
+auto PerformThunkCall(Context& context, SemIR::LocId loc_id,
+                      SemIR::FunctionId function_id,
+                      llvm::ArrayRef<SemIR::InstId> call_arg_ids,
+                      SemIR::InstId callee_id) -> SemIR::InstId {
   auto& function = context.functions().Get(function_id);
 
-  // Build a `NameRef` naming the callee, and a `SpecificConstant` if needed.
-  auto callee_type = context.types().GetAs<SemIR::FunctionType>(
-      context.insts().Get(callee_id).type_id());
-  callee_id = BuildNameRef(context, loc_id, function.name_id, callee_id,
-                           callee_type.specific_id);
-
   // If we have a self parameter, form `self.<callee_id>`.
   if (function.self_param_id.has_value()) {
     callee_id = PerformCompoundMemberAccess(
         context, loc_id,
-        BuildPatternRef(context, function_id, function.self_param_id),
+        BuildPatternRef(context, call_arg_ids, function.self_param_id),
         callee_id);
   }
 
@@ -352,12 +335,42 @@ static auto BuildThunkCall(Context& context, SemIR::FunctionId function_id,
   llvm::SmallVector<SemIR::InstId> args;
   for (auto pattern_id :
        context.inst_blocks().Get(function.param_patterns_id)) {
-    args.push_back(BuildPatternRef(context, function_id, pattern_id));
+    args.push_back(BuildPatternRef(context, call_arg_ids, pattern_id));
   }
 
   return PerformCall(context, loc_id, callee_id, args);
 }
 
+// Build a call to a function that forwards the arguments of the enclosing
+// function, for use when constructing a thunk.
+static auto BuildThunkCall(Context& context, SemIR::FunctionId function_id,
+                           SemIR::InstId callee_id) -> SemIR::InstId {
+  auto& function = context.functions().Get(function_id);
+
+  // Build a `NameRef` naming the callee, and a `SpecificConstant` if needed.
+  auto loc_id = SemIR::LocId(callee_id);
+  auto callee_type = context.types().GetAs<SemIR::FunctionType>(
+      context.insts().Get(callee_id).type_id());
+  callee_id = BuildNameRef(context, loc_id, function.name_id, callee_id,
+                           callee_type.specific_id);
+
+  // Build a reference to each parameter for use as call arguments.
+  llvm::SmallVector<SemIR::InstId> call_args;
+  auto call_params = context.inst_blocks().Get(function.call_params_id);
+  call_args.reserve(call_params.size());
+  for (auto call_param_id : call_params) {
+    // Use a pretty name for the `name_ref`. While it's suspicious to use a
+    // pretty name in the IR like this, the only reason we include a name at all
+    // here is to make the formatted SemIR more readable.
+    auto call_param = context.insts().GetAs<SemIR::AnyParam>(call_param_id);
+    call_args.push_back(BuildNameRef(context, SemIR::LocId(call_param_id),
+                                     call_param.pretty_name_id, call_param_id,
+                                     SemIR::SpecificId::None));
+  }
+
+  return PerformThunkCall(context, loc_id, function_id, call_args, callee_id);
+}
+
 // Given a declaration of a thunk and the function that it should call, build
 // the thunk body.
 static auto BuildThunkDefinition(Context& context,

+ 9 - 0
toolchain/check/thunk.h

@@ -18,6 +18,15 @@ auto BuildThunk(Context& context, SemIR::FunctionId signature_id,
                 SemIR::SpecificId signature_specific_id,
                 SemIR::InstId callee_id) -> SemIR::InstId;
 
+// Builds a call to a function that forwards a call argument list built for
+// `function_id` to a call to `callee_id`, for use when building a call from a
+// thunk to its target. This is like `PerformCall`, except that it takes a list
+// of call arguments for `function_id`, not a syntactic argument list.
+auto PerformThunkCall(Context& context, SemIR::LocId loc_id,
+                      SemIR::FunctionId function_id,
+                      llvm::ArrayRef<SemIR::InstId> call_arg_ids,
+                      SemIR::InstId callee_id) -> SemIR::InstId;
+
 // Builds the definition for a thunk whose definition was deferred until the end
 // of the enclosing scope.
 auto BuildThunkDefinition(Context& context,

+ 1 - 1
toolchain/lower/file_context.cpp

@@ -637,7 +637,7 @@ auto FileContext::BuildFunctionDecl(SemIR::FunctionId function_id,
   }
 
   // Don't lower builtins.
-  if (function.builtin_function_kind != SemIR::BuiltinFunctionKind::None) {
+  if (function.builtin_function_kind() != SemIR::BuiltinFunctionKind::None) {
     return nullptr;
   }
 

+ 1 - 1
toolchain/lower/handle_call.cpp

@@ -512,7 +512,7 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
                                       callee_function.function_id,
                                       callee_function.resolved_specific_id);
 
-  if (auto builtin_kind = function.builtin_function_kind;
+  if (auto builtin_kind = function.builtin_function_kind();
       builtin_kind != SemIR::BuiltinFunctionKind::None) {
     HandleBuiltinCall(context, inst_id, builtin_kind, arg_ids);
     return;

+ 3 - 0
toolchain/lower/mangler.cpp

@@ -163,6 +163,9 @@ auto Mangler::Mangle(SemIR::FunctionId function_id,
   switch (function.special_function_kind) {
     case SemIR::Function::SpecialFunctionKind::None:
       break;
+    case SemIR::Function::SpecialFunctionKind::Builtin:
+      CARBON_FATAL("Attempting to mangle declaration of builtin function {0}",
+                   function.builtin_function_kind());
     case SemIR::Function::SpecialFunctionKind::Thunk:
       os << ":thunk";
       break;

+ 32 - 6
toolchain/lower/testdata/impl/import_thunk.carbon

@@ -134,12 +134,24 @@ fn Test(a: A) -> C {
 // CHECK:STDOUT:
 // CHECK:STDOUT: define void @_CTest.Main(ptr sret({ i32 }) %return, ptr %a) !dbg !4 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   call void @"_CF:thunk.61ea2aba74ab3bf1:I.Main"(ptr %return, ptr %a), !dbg !7
-// CHECK:STDOUT:   ret void, !dbg !8
+// CHECK:STDOUT:   %.loc7_20.1.temp = alloca { i32 }, align 8, !dbg !7
+// CHECK:STDOUT:   %.loc7_20.2.temp = alloca { i32 }, align 8, !dbg !7
+// CHECK:STDOUT:   %.loc7_19.1.temp = alloca { i32 }, align 8, !dbg !8
+// CHECK:STDOUT:   call void @"_CConvert.A.Main:ImplicitAs.Core"(ptr %.loc7_19.1.temp, ptr %a), !dbg !8
+// CHECK:STDOUT:   call void @"_CF.61ea2aba74ab3bf1:I.Main"(ptr %.loc7_20.2.temp, ptr %.loc7_19.1.temp), !dbg !7
+// CHECK:STDOUT:   %.loc7_21.1.temp = alloca { i32 }, align 8, !dbg !9
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc7_21.1.temp, ptr %.loc7_20.2.temp), !dbg !9
+// CHECK:STDOUT:   ret void, !dbg !9
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: declare void @"_CF:thunk.61ea2aba74ab3bf1:I.Main"(ptr sret({ i32 }), ptr)
 // CHECK:STDOUT:
+// CHECK:STDOUT: declare void @"_CF.61ea2aba74ab3bf1:I.Main"(ptr sret({ i32 }), ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @"_CConvert.A.Main:ImplicitAs.Core"(ptr sret({ i32 }), ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @"_CConvert.B.Main:ImplicitAs.Core"(ptr sret({ i32 }), ptr)
+// CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
 // CHECK:STDOUT: !llvm.dbg.cu = !{!2}
 // CHECK:STDOUT:
@@ -151,7 +163,8 @@ fn Test(a: A) -> C {
 // CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
 // CHECK:STDOUT: !6 = !{}
 // CHECK:STDOUT: !7 = !DILocation(line: 7, column: 10, scope: !4)
-// CHECK:STDOUT: !8 = !DILocation(line: 7, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 7, column: 19, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 7, column: 3, scope: !4)
 // CHECK:STDOUT: ; ModuleID = 'thunk_for_imported_interface.carbon'
 // CHECK:STDOUT: source_filename = "thunk_for_imported_interface.carbon"
 // CHECK:STDOUT:
@@ -199,12 +212,24 @@ fn Test(a: A) -> C {
 // CHECK:STDOUT:
 // CHECK:STDOUT: define void @_CTest.Main(ptr sret({ i32 }) %return, ptr %a) !dbg !4 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   call void @"_CF:thunk.X.Main:I.Main"(ptr %return, ptr %a), !dbg !7
-// CHECK:STDOUT:   ret void, !dbg !8
+// CHECK:STDOUT:   %.loc8_19.1.temp = alloca { i32 }, align 8, !dbg !7
+// CHECK:STDOUT:   %.loc8_19.2.temp = alloca { i32 }, align 8, !dbg !7
+// CHECK:STDOUT:   %.loc8_18.1.temp = alloca { i32 }, align 8, !dbg !8
+// CHECK:STDOUT:   call void @"_CConvert.A.Main:ImplicitAs.Core"(ptr %.loc8_18.1.temp, ptr %a), !dbg !8
+// CHECK:STDOUT:   call void @"_CF.X.Main:I.Main"(ptr %.loc8_19.2.temp, ptr %.loc8_18.1.temp), !dbg !7
+// CHECK:STDOUT:   %.loc8_20.1.temp = alloca { i32 }, align 8, !dbg !9
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc8_20.1.temp, ptr %.loc8_19.2.temp), !dbg !9
+// CHECK:STDOUT:   ret void, !dbg !9
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: declare void @"_CF:thunk.X.Main:I.Main"(ptr sret({ i32 }), ptr)
 // CHECK:STDOUT:
+// CHECK:STDOUT: declare void @"_CF.X.Main:I.Main"(ptr sret({ i32 }), ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @"_CConvert.A.Main:ImplicitAs.Core"(ptr sret({ i32 }), ptr)
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @"_CConvert.B.Main:ImplicitAs.Core"(ptr sret({ i32 }), ptr)
+// CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
 // CHECK:STDOUT: !llvm.dbg.cu = !{!2}
 // CHECK:STDOUT:
@@ -216,4 +241,5 @@ fn Test(a: A) -> C {
 // CHECK:STDOUT: !5 = !DISubroutineType(types: !6)
 // CHECK:STDOUT: !6 = !{}
 // CHECK:STDOUT: !7 = !DILocation(line: 8, column: 10, scope: !4)
-// CHECK:STDOUT: !8 = !DILocation(line: 8, column: 3, scope: !4)
+// CHECK:STDOUT: !8 = !DILocation(line: 8, column: 18, scope: !4)
+// CHECK:STDOUT: !9 = !DILocation(line: 8, column: 3, scope: !4)

+ 45 - 29
toolchain/lower/testdata/impl/thunk.carbon

@@ -111,8 +111,14 @@ fn CallCallGeneric(c: C(()), b: B) -> A {
 // CHECK:STDOUT:
 // CHECK:STDOUT: define void @_CTest.Main(ptr sret({ i32 }) %return, ptr %a) !dbg !21 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   call void @"_CF:thunk.61ea2aba74ab3bf1:I.Main"(ptr %return, ptr %a), !dbg !22
-// CHECK:STDOUT:   ret void, !dbg !23
+// CHECK:STDOUT:   %.loc24_20.1.temp = alloca { i32 }, align 8, !dbg !22
+// CHECK:STDOUT:   %.loc24_20.2.temp = alloca { i32 }, align 8, !dbg !22
+// CHECK:STDOUT:   %.loc24_19.1.temp = alloca { i32 }, align 8, !dbg !23
+// CHECK:STDOUT:   call void @"_CConvert.A.Main:ImplicitAs.Core"(ptr %.loc24_19.1.temp, ptr %a), !dbg !23
+// CHECK:STDOUT:   call void @"_CF.61ea2aba74ab3bf1:I.Main"(ptr %.loc24_20.2.temp, ptr %.loc24_19.1.temp), !dbg !22
+// CHECK:STDOUT:   %.loc24_21.1.temp = alloca { i32 }, align 8, !dbg !24
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc24_21.1.temp, ptr %.loc24_20.2.temp), !dbg !24
+// CHECK:STDOUT:   ret void, !dbg !24
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1}
@@ -141,7 +147,8 @@ fn CallCallGeneric(c: C(()), b: B) -> A {
 // CHECK:STDOUT: !20 = !DILocation(line: 16, column: 8, scope: !18)
 // CHECK:STDOUT: !21 = distinct !DISubprogram(name: "Test", linkageName: "_CTest.Main", scope: null, file: !3, line: 23, type: !5, spFlags: DISPFlagDefinition, unit: !2)
 // CHECK:STDOUT: !22 = !DILocation(line: 24, column: 10, scope: !21)
-// CHECK:STDOUT: !23 = !DILocation(line: 24, column: 3, scope: !21)
+// CHECK:STDOUT: !23 = !DILocation(line: 24, column: 19, scope: !21)
+// CHECK:STDOUT: !24 = !DILocation(line: 24, column: 3, scope: !21)
 // CHECK:STDOUT: ; ModuleID = 'generic_thunk.carbon'
 // CHECK:STDOUT: source_filename = "generic_thunk.carbon"
 // CHECK:STDOUT:
@@ -156,30 +163,31 @@ fn CallCallGeneric(c: C(()), b: B) -> A {
 // CHECK:STDOUT:
 // CHECK:STDOUT: define void @_CCall.Main(ptr sret({}) %return, ptr %c, ptr %b) !dbg !8 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   call void @"_CF:thunk.C.Main:I.Main.e43630e9a6c38c3f"(ptr %return, ptr %c, ptr %b), !dbg !9
-// CHECK:STDOUT:   ret void, !dbg !10
+// CHECK:STDOUT:   %.loc22_23.1.temp = alloca {}, align 8, !dbg !9
+// CHECK:STDOUT:   %.loc22_23.3.temp = alloca {}, align 8, !dbg !9
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc22_23.3.temp, ptr %b), !dbg !9
+// CHECK:STDOUT:   %.loc22_23.7.temp = alloca {}, align 8, !dbg !9
+// CHECK:STDOUT:   %.loc22_22.1.temp = alloca {}, align 8, !dbg !10
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc22_22.1.temp, ptr %b), !dbg !10
+// CHECK:STDOUT:   call void @"_CF.C.Main:I.Main.e43630e9a6c38c3f"(ptr %.loc22_23.7.temp, ptr %c, ptr %.loc22_22.1.temp), !dbg !9
+// CHECK:STDOUT:   %.loc22_24.1.temp = alloca {}, align 8, !dbg !11
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc22_24.1.temp, ptr %.loc22_23.7.temp), !dbg !11
+// CHECK:STDOUT:   ret void, !dbg !11
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define void @_CCallCallGeneric.Main(ptr sret({}) %return, ptr %c, ptr %b) !dbg !11 {
+// CHECK:STDOUT: define void @_CCallCallGeneric.Main(ptr sret({}) %return, ptr %c, ptr %b) !dbg !12 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   call void @_CCallGeneric.Main.9ef6968dffa77413(ptr %return, ptr %c, ptr %b), !dbg !12
-// CHECK:STDOUT:   ret void, !dbg !13
+// CHECK:STDOUT:   call void @_CCallGeneric.Main.9ef6968dffa77413(ptr %return, ptr %c, ptr %b), !dbg !13
+// CHECK:STDOUT:   ret void, !dbg !14
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: nocallback nofree nounwind willreturn memory(argmem: readwrite)
 // CHECK:STDOUT: declare void @llvm.memcpy.p0.p0.i64(ptr noalias writeonly captures(none), ptr noalias readonly captures(none), i64, i1 immarg) #0
 // CHECK:STDOUT:
-// CHECK:STDOUT: define linkonce_odr void @"_CF:thunk.C.Main:I.Main.e43630e9a6c38c3f"(ptr sret({}) %return, ptr %self, ptr %x) !dbg !14 {
+// CHECK:STDOUT: define linkonce_odr void @"_CF.C.Main:I.Main.e43630e9a6c38c3f"(ptr sret({}) %return, ptr %self, ptr %x) !dbg !15 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %.loc18_31.2.temp = alloca {}, align 8, !dbg !15
-// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc18_31.2.temp, ptr %x), !dbg !15
-// CHECK:STDOUT:   %.loc18_31.6.temp = alloca {}, align 8, !dbg !15
-// CHECK:STDOUT:   %.loc12_21.1.temp = alloca {}, align 8, !dbg !16
-// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc12_21.1.temp, ptr %x), !dbg !16
-// CHECK:STDOUT:   call void @"_CF.C.Main:I.Main.e43630e9a6c38c3f"(ptr %.loc18_31.6.temp, ptr %self, ptr %.loc12_21.1.temp), !dbg !15
-// CHECK:STDOUT:   %.loc18_31.7.temp = alloca {}, align 8, !dbg !15
-// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc18_31.7.temp, ptr %.loc18_31.6.temp), !dbg !15
-// CHECK:STDOUT:   ret void, !dbg !15
+// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %return, ptr align 1 @B.val.loc18_42, i64 0, i1 false), !dbg !16
+// CHECK:STDOUT:   ret void, !dbg !16
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: define linkonce_odr void @_CCallGeneric.Main.9ef6968dffa77413(ptr sret({}) %return, ptr %c, ptr %b) !dbg !17 {
@@ -188,9 +196,16 @@ fn CallCallGeneric(c: C(()), b: B) -> A {
 // CHECK:STDOUT:   ret void, !dbg !19
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: define linkonce_odr void @"_CF.C.Main:I.Main.e43630e9a6c38c3f"(ptr sret({}) %return, ptr %self, ptr %x) !dbg !20 {
+// CHECK:STDOUT: define linkonce_odr void @"_CF:thunk.C.Main:I.Main.e43630e9a6c38c3f"(ptr sret({}) %return, ptr %self, ptr %x) !dbg !20 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   call void @llvm.memcpy.p0.p0.i64(ptr align 1 %return, ptr align 1 @B.val.loc18_42, i64 0, i1 false), !dbg !21
+// CHECK:STDOUT:   %.loc18_31.2.temp = alloca {}, align 8, !dbg !21
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc18_31.2.temp, ptr %x), !dbg !21
+// CHECK:STDOUT:   %.loc18_31.6.temp = alloca {}, align 8, !dbg !21
+// CHECK:STDOUT:   %.loc12_21.1.temp = alloca {}, align 8, !dbg !22
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc12_21.1.temp, ptr %x), !dbg !22
+// CHECK:STDOUT:   call void @"_CF.C.Main:I.Main.e43630e9a6c38c3f"(ptr %.loc18_31.6.temp, ptr %self, ptr %.loc12_21.1.temp), !dbg !21
+// CHECK:STDOUT:   %.loc18_31.7.temp = alloca {}, align 8, !dbg !21
+// CHECK:STDOUT:   call void @"_CConvert.B.Main:ImplicitAs.Core"(ptr %.loc18_31.7.temp, ptr %.loc18_31.6.temp), !dbg !21
 // CHECK:STDOUT:   ret void, !dbg !21
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -209,15 +224,16 @@ fn CallCallGeneric(c: C(()), b: B) -> A {
 // CHECK:STDOUT: !7 = !DILocation(line: 7, column: 37, scope: !4)
 // CHECK:STDOUT: !8 = distinct !DISubprogram(name: "Call", linkageName: "_CCall.Main", scope: null, file: !3, line: 21, type: !5, spFlags: DISPFlagDefinition, unit: !2)
 // CHECK:STDOUT: !9 = !DILocation(line: 22, column: 10, scope: !8)
-// CHECK:STDOUT: !10 = !DILocation(line: 22, column: 3, scope: !8)
-// CHECK:STDOUT: !11 = distinct !DISubprogram(name: "CallCallGeneric", linkageName: "_CCallCallGeneric.Main", scope: null, file: !3, line: 29, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !12 = !DILocation(line: 30, column: 10, scope: !11)
-// CHECK:STDOUT: !13 = !DILocation(line: 30, column: 3, scope: !11)
-// CHECK:STDOUT: !14 = distinct !DISubprogram(name: "F", linkageName: "_CF:thunk.C.Main:I.Main.e43630e9a6c38c3f", scope: null, file: !3, line: 18, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !15 = !DILocation(line: 18, column: 3, scope: !14)
-// CHECK:STDOUT: !16 = !DILocation(line: 12, column: 20, scope: !14)
+// CHECK:STDOUT: !10 = !DILocation(line: 22, column: 22, scope: !8)
+// CHECK:STDOUT: !11 = !DILocation(line: 22, column: 3, scope: !8)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "CallCallGeneric", linkageName: "_CCallCallGeneric.Main", scope: null, file: !3, line: 29, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !13 = !DILocation(line: 30, column: 10, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 30, column: 3, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "F", linkageName: "_CF.C.Main:I.Main.e43630e9a6c38c3f", scope: null, file: !3, line: 18, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !16 = !DILocation(line: 18, column: 33, scope: !15)
 // CHECK:STDOUT: !17 = distinct !DISubprogram(name: "CallGeneric", linkageName: "_CCallGeneric.Main.9ef6968dffa77413", scope: null, file: !3, line: 25, type: !5, spFlags: DISPFlagDefinition, unit: !2)
 // CHECK:STDOUT: !18 = !DILocation(line: 26, column: 10, scope: !17)
 // CHECK:STDOUT: !19 = !DILocation(line: 26, column: 3, scope: !17)
-// CHECK:STDOUT: !20 = distinct !DISubprogram(name: "F", linkageName: "_CF.C.Main:I.Main.e43630e9a6c38c3f", scope: null, file: !3, line: 18, type: !5, spFlags: DISPFlagDefinition, unit: !2)
-// CHECK:STDOUT: !21 = !DILocation(line: 18, column: 33, scope: !20)
+// CHECK:STDOUT: !20 = distinct !DISubprogram(name: "F", linkageName: "_CF:thunk.C.Main:I.Main.e43630e9a6c38c3f", scope: null, file: !3, line: 18, type: !5, spFlags: DISPFlagDefinition, unit: !2)
+// CHECK:STDOUT: !21 = !DILocation(line: 18, column: 3, scope: !20)
+// CHECK:STDOUT: !22 = !DILocation(line: 12, column: 20, scope: !20)

+ 3 - 0
toolchain/sem_ir/builtin_function_kind.h

@@ -27,6 +27,9 @@ class BuiltinFunctionKind : public CARBON_ENUM_BASE(BuiltinFunctionKind) {
   CARBON_ENUM_CONSTANT_DECL(Name)
 #include "toolchain/sem_ir/builtin_function_kind.def"
 
+  using EnumBase::AsInt;
+  using EnumBase::FromInt;
+
   // Returns the builtin function kind with the given name, or None if the name
   // is unknown.
   static auto ForBuiltinName(llvm::StringRef name) -> BuiltinFunctionKind;

+ 7 - 2
toolchain/sem_ir/formatter.cpp

@@ -487,12 +487,17 @@ auto Formatter::FormatFunction(FunctionId id) -> void {
   FormatParamList(fn.call_params_id, return_type_info.is_valid() &&
                                          return_type_info.has_return_slot());
 
-  if (fn.builtin_function_kind != BuiltinFunctionKind::None) {
+  if (fn.builtin_function_kind() != BuiltinFunctionKind::None) {
     out_ << " = \""
-         << FormatEscaped(fn.builtin_function_kind.name(),
+         << FormatEscaped(fn.builtin_function_kind().name(),
                           /*use_hex_escapes=*/true)
          << "\"";
   }
+  if (fn.thunk_decl_id().has_value()) {
+    out_ << " [thunk ";
+    FormatArg(fn.thunk_decl_id());
+    out_ << "]";
+  }
 
   if (!fn.body_block_ids.empty()) {
     out_ << ' ';

+ 34 - 8
toolchain/sem_ir/function.h

@@ -16,7 +16,7 @@ namespace Carbon::SemIR {
 // Function-specific fields.
 struct FunctionFields {
   // Kinds of special functions.
-  enum class SpecialFunctionKind : uint8_t { None, Thunk };
+  enum class SpecialFunctionKind : uint8_t { None, Builtin, Thunk };
 
   // Kinds of virtual modifiers that can apply to functions.
   enum class VirtualModifier : uint8_t { None, Virtual, Abstract, Impl };
@@ -60,13 +60,9 @@ struct FunctionFields {
   // implicit_param_patterns_id from EntityWithParamsBase.
   InstId self_param_id = InstId::None;
 
-  // The following member is set on the first call to the function, or at the
-  // point where the function is defined.
-
-  // The following members are set at the end of a builtin function definition.
-
-  // If this is a builtin function, the corresponding builtin kind.
-  BuiltinFunctionKind builtin_function_kind = BuiltinFunctionKind::None;
+  // Data that is specific to the special function kind. Use
+  // `builtin_function_kind()` or `thunk_decl_id()` to access this.
+  AnyRawId special_function_kind_data = AnyRawId(AnyRawId::NoneIndex);
 
   // The following members are accumulated throughout the function definition.
 
@@ -109,6 +105,22 @@ struct Function : public EntityWithParamsBase,
     out << "}";
   }
 
+  // Returns the builtin function kind for this function, or None if this is not
+  // a builtin function.
+  auto builtin_function_kind() const -> BuiltinFunctionKind {
+    return special_function_kind == SpecialFunctionKind::Builtin
+               ? BuiltinFunctionKind::FromInt(special_function_kind_data.index)
+               : BuiltinFunctionKind::None;
+  }
+
+  // Returns the declaration that this is a thunk for, or None if this function
+  // is not a thunk.
+  auto thunk_decl_id() const -> InstId {
+    return special_function_kind == SpecialFunctionKind::Thunk
+               ? InstId(special_function_kind_data.index)
+               : InstId::None;
+  }
+
   // Given the ID of an instruction from `param_patterns_id` or
   // `implicit_param_patterns_id`, returns a `ParamPatternInfo` value with the
   // corresponding `Call` parameter pattern, its ID, and the entity_name_id of
@@ -127,6 +139,20 @@ struct Function : public EntityWithParamsBase,
   auto GetDeclaredReturnType(const File& file,
                              SpecificId specific_id = SpecificId::None) const
       -> TypeId;
+
+  // Sets that this function is a builtin function.
+  auto SetBuiltinFunction(BuiltinFunctionKind kind) -> void {
+    CARBON_CHECK(special_function_kind == SpecialFunctionKind::None);
+    special_function_kind = SpecialFunctionKind::Builtin;
+    special_function_kind_data = AnyRawId(kind.AsInt());
+  }
+
+  // Sets that this function is a thunk.
+  auto SetThunk(InstId decl_id) -> void {
+    CARBON_CHECK(special_function_kind == SpecialFunctionKind::None);
+    special_function_kind = SpecialFunctionKind::Thunk;
+    special_function_kind_data = AnyRawId(decl_id.index);
+  }
 };
 
 class File;

+ 2 - 2
toolchain/sem_ir/inst_namer.cpp

@@ -662,10 +662,10 @@ auto InstNamer::NamingContext::NameInst() -> void {
       const auto& function =
           sem_ir().functions().Get(callee_function.function_id);
       // Name the call's result based on the callee.
-      if (function.builtin_function_kind != BuiltinFunctionKind::None) {
+      if (function.builtin_function_kind() != BuiltinFunctionKind::None) {
         // For a builtin, use the builtin name. Otherwise, we'd typically pick
         // the name `Op` below, which is probably not very useful.
-        AddInstName(function.builtin_function_kind.name().str());
+        AddInstName(function.builtin_function_kind().name().str());
         return;
       }