Parcourir la source

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 il y a 10 mois
Parent
commit
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;
       }