Browse Source

Add return value support to C++ thunks. (#5976)

Based on #5948. A couple of tricky parts:

* When generating the C++ side of the thunk, we are given a pointer to
the location to emplace the return value. The only mechanism C++
provides to perform this emplacement is using placement `operator new`,
which requires a library function in the `<new>` header. We handle this
by declaring that library function ourselves, and rely on Clang not
actually needing a definition for it (which the standard library owns).

* On the Carbon side of the thunk, we want to form an initializing
expression as the result of the call. We don't have a way of expressing
in SemIR that an initializing expression performs its initialization by
storing through a pointer, so this PR adds a new initializing
instruction, `InPlaceInit`, to model an initialization that's performed
opaquely in-place.
Richard Smith 8 months ago
parent
commit
d37f1ae6b5

+ 184 - 47
toolchain/check/cpp_thunk.cpp

@@ -74,34 +74,46 @@ auto IsCppThunkRequired(Context& context, const SemIR::Function& function)
     return false;
   }
 
+  if (isa<clang::CXXConstructorDecl>(
+          context.sem_ir().clang_decls().Get(function.clang_decl_id).decl)) {
+    // TODO: Support generating thunks for constructors.
+    return false;
+  }
+
+  // A thunk is required if any parameter or return type requires it. However,
+  // we don't generate a thunk if any relevant type is erroneous.
+  bool thunk_required = false;
+
   SemIR::TypeId return_type_id =
       function.GetDeclaredReturnType(context.sem_ir());
   if (return_type_id.has_value()) {
-    // TODO: Support non-void return values.
-    return false;
+    if (return_type_id == SemIR::ErrorInst::TypeId) {
+      return false;
+    }
+    thunk_required = IsThunkRequiredForType(context, return_type_id);
   }
 
-  bool thunk_required_for_param = false;
   for (auto param_id :
        context.inst_blocks().GetOrEmpty(function.call_params_id)) {
     if (param_id == SemIR::ErrorInst::InstId) {
       return false;
     }
-    if (!thunk_required_for_param &&
+    if (!thunk_required &&
         IsThunkRequiredForType(
             context,
             context.insts().GetAs<SemIR::AnyParam>(param_id).type_id)) {
-      thunk_required_for_param = true;
+      thunk_required = true;
     }
   }
 
-  return thunk_required_for_param;
+  return thunk_required;
 }
 
-// Returns whether the type is a pointer or a signed int of 32 or 64 bits.
+// Returns whether the type is void, a pointer, or a signed int of 32 or 64
+// bits.
 static auto IsSimpleAbiType(clang::ASTContext& ast_context,
                             clang::QualType type) -> bool {
-  if (type->isPointerType()) {
+  if (type->isVoidType() || type->isPointerType()) {
     return true;
   }
 
@@ -125,6 +137,8 @@ struct CalleeFunctionInfo {
     if (has_object_parameter && method_decl->isImplicitObjectMemberFunction()) {
       implicit_this_type = method_decl->getThisType();
     }
+    has_simple_return_type =
+        IsSimpleAbiType(decl->getASTContext(), decl->getReturnType());
   }
 
   // Returns whether this callee has an implicit `this` parameter.
@@ -139,13 +153,23 @@ struct CalleeFunctionInfo {
 
   // Returns the number of parameters the thunk should have.
   auto num_thunk_params() const -> unsigned {
-    return has_implicit_object_parameter() + decl->getNumParams();
+    return has_implicit_object_parameter() + decl->getNumParams() +
+           !has_simple_return_type;
   }
 
+  // Returns the thunk parameter index corresponding to a given callee parameter
+  // index.
   auto GetThunkParamIndex(unsigned callee_param_index) const -> unsigned {
     return has_implicit_object_parameter() + callee_param_index;
   }
 
+  // Returns the thunk parameter index corresponding to the parameter that holds
+  // the address of the return value.
+  auto GetThunkReturnParamIndex() const -> unsigned {
+    CARBON_CHECK(!has_simple_return_type);
+    return has_implicit_object_parameter() + decl->getNumParams();
+  }
+
   // The callee function.
   clang::FunctionDecl* decl;
 
@@ -156,9 +180,32 @@ struct CalleeFunctionInfo {
   // If the callee has an implicit object parameter, the corresponding `this`
   // type. Otherwise a null type.
   clang::QualType implicit_this_type;
+
+  // Whether the callee has a simple return type, that we can return directly.
+  // If not, we'll return through an out parameter instead.
+  bool has_simple_return_type;
 };
 }  // namespace
 
+// Given a pointer type, returns the corresponding _Nonnull-qualified pointer
+// type.
+static auto GetNonnullType(clang::ASTContext& ast_context,
+                           clang::QualType pointer_type) -> clang::QualType {
+  return ast_context.getAttributedType(clang::NullabilityKind::NonNull,
+                                       pointer_type, pointer_type);
+}
+
+// Given the type of a callee parameter, returns the type to use for the
+// corresponding thunk parameter.
+static auto GetThunkParameterType(clang::ASTContext& ast_context,
+                                  clang::QualType callee_type)
+    -> clang::QualType {
+  if (IsSimpleAbiType(ast_context, callee_type)) {
+    return callee_type;
+  }
+  return GetNonnullType(ast_context, ast_context.getPointerType(callee_type));
+}
+
 // Creates the thunk parameter types given the callee function.
 static auto BuildThunkParameterTypes(clang::ASTContext& ast_context,
                                      CalleeFunctionInfo callee_info)
@@ -166,21 +213,22 @@ static auto BuildThunkParameterTypes(clang::ASTContext& ast_context,
   llvm::SmallVector<clang::QualType> thunk_param_types;
   thunk_param_types.reserve(callee_info.num_thunk_params());
   if (callee_info.has_implicit_object_parameter()) {
-    thunk_param_types.push_back(ast_context.getAttributedType(
-        clang::NullabilityKind::NonNull, callee_info.implicit_this_type,
-        callee_info.implicit_this_type));
+    thunk_param_types.push_back(
+        GetNonnullType(ast_context, callee_info.implicit_this_type));
   }
 
   for (const clang::ParmVarDecl* callee_param :
        callee_info.decl->parameters()) {
-    clang::QualType param_type = callee_param->getType();
-    bool is_simple_abi_type = IsSimpleAbiType(ast_context, param_type);
-    if (!is_simple_abi_type) {
-      clang::QualType pointer_type = ast_context.getPointerType(param_type);
-      param_type = ast_context.getAttributedType(
-          clang::NullabilityKind::NonNull, pointer_type, pointer_type);
-    }
-    thunk_param_types.push_back(param_type);
+    // TODO: We should use the type from the function signature, not the type of
+    // the parameter here.
+    thunk_param_types.push_back(
+        GetThunkParameterType(ast_context, callee_param->getType()));
+  }
+
+  if (!callee_info.has_simple_return_type) {
+    thunk_param_types.push_back(GetNonnullType(
+        ast_context,
+        ast_context.getPointerType(callee_info.decl->getReturnType())));
   }
 
   CARBON_CHECK(thunk_param_types.size() == callee_info.num_thunk_params());
@@ -220,6 +268,16 @@ static auto BuildThunkParameters(clang::ASTContext& ast_context,
     thunk_params.push_back(thunk_param);
   }
 
+  if (!callee_info.has_simple_return_type) {
+    clang::ParmVarDecl* thunk_param =
+        clang::ParmVarDecl::Create(ast_context, thunk_function_decl, clang_loc,
+                                   clang_loc, &ast_context.Idents.get("return"),
+                                   thunk_function_proto_type->getParamType(
+                                       callee_info.GetThunkReturnParamIndex()),
+                                   nullptr, clang::SC_None, nullptr);
+    thunk_params.push_back(thunk_param);
+  }
+
   CARBON_CHECK(thunk_params.size() == num_thunk_params);
   return thunk_params;
 }
@@ -237,7 +295,9 @@ static auto CreateThunkFunctionDecl(
 
   auto ext_proto_info = clang::FunctionProtoType::ExtProtoInfo();
   clang::QualType thunk_function_type = ast_context.getFunctionType(
-      callee_info.decl->getReturnType(), thunk_param_types, ext_proto_info);
+      callee_info.has_simple_return_type ? callee_info.decl->getReturnType()
+                                         : ast_context.VoidTy,
+      thunk_param_types, ext_proto_info);
 
   clang::DeclContext* decl_context = ast_context.getTranslationUnitDecl();
   // TODO: Thunks should not have external linkage, consider using `SC_Static`.
@@ -264,11 +324,13 @@ static auto CreateThunkFunctionDecl(
   return thunk_function_decl;
 }
 
-// Builds a reference to the given parameter thunk, which is carrying a value of
-// the given type.
+// Builds a reference to the given parameter thunk. If `type` is specified, that
+// is the callee parameter type that's being held by the parameter, and
+// conversions will be performed as necessary to recover a value of that type.
 static auto BuildThunkParamRef(clang::Sema& sema,
                                clang::FunctionDecl* thunk_function_decl,
-                               unsigned thunk_index, clang::QualType type)
+                               unsigned thunk_index,
+                               clang::QualType type = clang::QualType())
     -> clang::Expr* {
   clang::ParmVarDecl* thunk_param =
       thunk_function_decl->getParamDecl(thunk_index);
@@ -277,7 +339,7 @@ static auto BuildThunkParamRef(clang::Sema& sema,
   clang::Expr* call_arg = sema.BuildDeclRefExpr(
       thunk_param, thunk_param->getType().getNonReferenceType(),
       clang::VK_LValue, clang_loc);
-  if (thunk_param->getType() != type) {
+  if (!type.isNull() && thunk_param->getType() != type) {
     // TODO: Consider inserting a cast to an rvalue. Note that we currently
     // pass pointers to non-temporary objects as the argument when calling a
     // thunk, so we'll need to either change that or generate different thunks
@@ -329,6 +391,9 @@ static auto BuildThunkBody(clang::Sema& sema,
                            clang::FunctionDecl* thunk_function_decl,
                            CalleeFunctionInfo callee_info)
     -> clang::StmtResult {
+  // TODO: Consider building a CompoundStmt holding our created statement to
+  // make our result more closely resemble a real C++ function.
+
   clang::SourceLocation clang_loc = callee_info.decl->getLocation();
 
   // If the callee has an object parameter, build a member access expression as
@@ -337,9 +402,9 @@ static auto BuildThunkBody(clang::Sema& sema,
   if (callee_info.has_object_parameter) {
     auto* object_param_ref =
         BuildThunkParamRef(sema, thunk_function_decl, 0,
-                           callee_info.has_implicit_object_parameter()
-                               ? thunk_function_decl->getParamDecl(0)->getType()
-                               : callee_info.decl->getParamDecl(0)->getType());
+                           callee_info.has_explicit_object_parameter()
+                               ? callee_info.decl->getParamDecl(0)->getType()
+                               : clang::QualType());
     bool is_arrow = callee_info.has_implicit_object_parameter();
     auto object =
         sema.PerformMemberExprBaseConversion(object_param_ref, is_arrow);
@@ -373,9 +438,20 @@ static auto BuildThunkBody(clang::Sema& sema,
     return clang::StmtError();
   }
 
-  // TODO: Consider building a CompoundStmt holding this to make our result more
-  // closely resemble a real C++ function.
-  return sema.BuildReturnStmt(clang_loc, call.get());
+  if (callee_info.has_simple_return_type) {
+    return sema.BuildReturnStmt(clang_loc, call.get());
+  }
+
+  auto* return_object_addr = BuildThunkParamRef(
+      sema, thunk_function_decl, callee_info.GetThunkReturnParamIndex());
+  auto return_type = callee_info.decl->getReturnType();
+  auto* return_type_info =
+      sema.Context.getTrivialTypeSourceInfo(return_type, clang_loc);
+  auto placement_new = sema.BuildCXXNew(
+      clang_loc, /*UseGlobal=*/true, clang_loc, {return_object_addr}, clang_loc,
+      /*TypeIdParens=*/clang::SourceRange(), return_type, return_type_info,
+      /*ArraySize=*/std::nullopt, clang_loc, call.get());
+  return sema.ActOnExprStmt(placement_new, /*DiscardedValue=*/true);
 }
 
 auto BuildCppThunk(Context& context, const SemIR::Function& callee_function)
@@ -413,22 +489,55 @@ auto PerformCppThunkCall(Context& context, SemIR::LocId loc_id,
                          SemIR::FunctionId callee_function_id,
                          llvm::ArrayRef<SemIR::InstId> callee_arg_ids,
                          SemIR::InstId thunk_callee_id) -> SemIR::InstId {
-  llvm::ArrayRef<SemIR::InstId> callee_function_params =
-      context.inst_blocks().GetOrEmpty(
-          context.functions().Get(callee_function_id).call_params_id);
-
-  llvm::ArrayRef<SemIR::InstId> thunk_function_params =
-      context.inst_blocks().GetOrEmpty(
-          context.functions()
-              .Get(GetCalleeFunction(context.sem_ir(), thunk_callee_id)
-                       .function_id)
-              .call_params_id);
-
-  size_t num_params = callee_function_params.size();
-  CARBON_CHECK(thunk_function_params.size() == num_params);
-  CARBON_CHECK(callee_arg_ids.size() == num_params);
+  auto& callee_function = context.functions().Get(callee_function_id);
+  auto callee_function_params =
+      context.inst_blocks().Get(callee_function.call_params_id);
+
+  auto thunk_callee = GetCalleeFunction(context.sem_ir(), thunk_callee_id);
+  auto& thunk_function = context.functions().Get(thunk_callee.function_id);
+  auto thunk_function_params =
+      context.inst_blocks().Get(thunk_function.call_params_id);
+
+  // Whether we need to pass a return address to the thunk as a final argument.
+  bool thunk_takes_return_address =
+      callee_function.return_slot_pattern_id.has_value() &&
+      !thunk_function.return_slot_pattern_id.has_value();
+
+  // The number of arguments we should be acquiring in order to call the thunk.
+  // This includes the return address parameter, if any.
+  unsigned num_thunk_args =
+      context.inst_blocks().Get(thunk_function.param_patterns_id).size();
+
+  // The corresponding number of arguments that would be provided in a syntactic
+  // call to the callee. This excludes the return slot.
+  unsigned num_callee_args = num_thunk_args - thunk_takes_return_address;
+
+  // Grab the return slot argument, if we were given one.
+  auto return_slot_id = SemIR::InstId::None;
+  if (callee_arg_ids.size() == num_callee_args + 1) {
+    return_slot_id = callee_arg_ids.consume_back();
+  }
+
+  // If there's a return slot pattern, drop the corresponding parameter.
+  // TODO: The parameter should probably only be created if the return pattern
+  // actually needs a return address to be passed in.
+  if (thunk_function.return_slot_pattern_id.has_value()) {
+    thunk_function_params.consume_back();
+  }
+  if (callee_function.return_slot_pattern_id.has_value()) {
+    callee_function_params.consume_back();
+  }
+
+  // We assume that the call parameters exactly match the parameter patterns for
+  // both the thunk and the callee. This is currently guaranteed because we only
+  // create trivial *ParamPatterns when importing a C++ function.
+  CARBON_CHECK(num_callee_args == callee_function_params.size());
+  CARBON_CHECK(num_callee_args == callee_arg_ids.size());
+  CARBON_CHECK(num_thunk_args == thunk_function_params.size());
+
+  // Build the thunk arguments by converting the callee arguments as needed.
   llvm::SmallVector<SemIR::InstId> thunk_arg_ids;
-  thunk_arg_ids.reserve(num_params);
+  thunk_arg_ids.reserve(num_thunk_args);
   for (auto [callee_param_inst_id, thunk_param_inst_id, callee_arg_id] :
        llvm::zip(callee_function_params, thunk_function_params,
                  callee_arg_ids)) {
@@ -453,7 +562,35 @@ auto PerformCppThunkCall(Context& context, SemIR::LocId loc_id,
     thunk_arg_ids.push_back(arg_id);
   }
 
-  return PerformCall(context, loc_id, thunk_callee_id, thunk_arg_ids);
+  // Add an argument to hold the result of the call, if necessary.
+  auto return_type_id = callee_function.GetDeclaredReturnType(context.sem_ir());
+  if (thunk_takes_return_address) {
+    // Create a temporary if the caller didn't provide a return slot.
+    if (!return_slot_id.has_value()) {
+      return_slot_id = AddInstWithCleanup<SemIR::TemporaryStorage>(
+          context, loc_id, {.type_id = return_type_id});
+    }
+
+    auto arg_id = AddInst<SemIR::AddrOf>(
+        context, loc_id,
+        {.type_id = GetPointerType(
+             context, context.types().GetInstId(
+                          context.insts().Get(return_slot_id).type_id())),
+         .lvalue_id = return_slot_id});
+    thunk_arg_ids.push_back(arg_id);
+  }
+
+  auto result_id = PerformCall(context, loc_id, thunk_callee_id, thunk_arg_ids);
+
+  // Produce the result of the call, taking the value from the return storage.
+  if (thunk_takes_return_address) {
+    result_id = AddInst<SemIR::InPlaceInit>(context, loc_id,
+                                            {.type_id = return_type_id,
+                                             .src_id = result_id,
+                                             .dest_id = return_slot_id});
+  }
+
+  return result_id;
 }
 
 }  // namespace Carbon::Check

+ 28 - 0
toolchain/check/import_cpp.cpp

@@ -102,6 +102,34 @@ static auto GenerateCppIncludesHeaderCode(
                   << "\"\n";
     }
   }
+
+  // Inject a declaration of placement operator new, because the code we
+  // generate in thunks depends on it for placement new expressions. Clang has
+  // special-case logic for lowering a new-expression using this, so a
+  // definition is not required.
+  // TODO: This is a hack. We should be able to directly generate Clang AST to
+  // construct objects in-place without this.
+  // TODO: Once we can rely on libc++ being available, consider including
+  // `<__new/placement_new_delete.h>` instead.
+  code_stream << R"(# 1 "<carbon-internal>"
+#undef constexpr
+#if __cplusplus > 202302L
+constexpr
+#endif
+#undef void
+#undef operator
+#undef new
+void* operator new(__SIZE_TYPE__, void*)
+#if __cplusplus < 201103L
+#undef throw
+throw()
+#else
+#undef noexcept
+noexcept
+#endif
+;
+)";
+
   return code;
 }
 

+ 69 - 12
toolchain/check/testdata/interop/cpp/function/arithmetic_types_bridged.carbon

@@ -1642,11 +1642,17 @@ fn F() {
 // CHECK:STDOUT: --- import_bool_return.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
 // CHECK:STDOUT:   %Bool.type: type = fn_type @Bool [concrete]
 // CHECK:STDOUT:   %Bool: %Bool.type = struct_value () [concrete]
 // CHECK:STDOUT:   %pattern_type.831: type = pattern_type bool [concrete]
 // CHECK:STDOUT:   %foo_bool.type: type = fn_type @foo_bool [concrete]
 // CHECK:STDOUT:   %foo_bool: %foo_bool.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.bb2: type = ptr_type bool [concrete]
+// CHECK:STDOUT:   %foo_bool__carbon_thunk.type: type = fn_type @foo_bool__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo_bool__carbon_thunk: %foo_bool__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.type.655: type = fn_type @T.as.Destroy.impl.Op, @T.as.Destroy.impl(bool) [concrete]
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.8b7: %T.as.Destroy.impl.Op.type.655 = struct_value () [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -1659,6 +1665,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo_bool__carbon_thunk.decl: %foo_bool__carbon_thunk.type = fn_decl @foo_bool__carbon_thunk [concrete = constants.%foo_bool__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F() {
@@ -1668,26 +1679,40 @@ fn F() {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo_bool.ref: %foo_bool.type = name_ref foo_bool, imports.%foo_bool.decl [concrete = constants.%foo_bool]
-// CHECK:STDOUT:   %foo_bool.call: init bool = call %foo_bool.ref()
+// CHECK:STDOUT:   %.loc8_30.1: ref bool = temporary_storage
+// CHECK:STDOUT:   %addr.loc8_30.1: %ptr.bb2 = addr_of %.loc8_30.1
+// CHECK:STDOUT:   %foo_bool__carbon_thunk.call: init %empty_tuple.type = call imports.%foo_bool__carbon_thunk.decl(%addr.loc8_30.1)
+// CHECK:STDOUT:   %.loc8_30.2: init bool = in_place_init %foo_bool__carbon_thunk.call, %.loc8_30.1
 // CHECK:STDOUT:   %.loc8_10.1: type = splice_block %.loc8_10.3 [concrete = bool] {
 // CHECK:STDOUT:     %Bool.call: init type = call constants.%Bool() [concrete = bool]
 // CHECK:STDOUT:     %.loc8_10.2: type = value_of_initializer %Bool.call [concrete = bool]
 // CHECK:STDOUT:     %.loc8_10.3: type = converted %Bool.call, %.loc8_10.2 [concrete = bool]
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %.loc8_30.1: bool = value_of_initializer %foo_bool.call
-// CHECK:STDOUT:   %.loc8_30.2: bool = converted %foo_bool.call, %.loc8_30.1
-// CHECK:STDOUT:   %x: bool = bind_name x, %.loc8_30.2
+// CHECK:STDOUT:   %.loc8_30.3: bool = value_of_initializer %.loc8_30.2
+// CHECK:STDOUT:   %.loc8_30.4: bool = converted %.loc8_30.2, %.loc8_30.3
+// CHECK:STDOUT:   %x: bool = bind_name x, %.loc8_30.4
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_30.1, constants.%T.as.Destroy.impl.Op.8b7
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_30.1, %T.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr.loc8_30.2: %ptr.bb2 = addr_of %.loc8_30.1
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_30.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: --- import_short_return.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
 // CHECK:STDOUT:   %int_16: Core.IntLiteral = int_value 16 [concrete]
 // CHECK:STDOUT:   %i16: type = class_type @Int, @Int(%int_16) [concrete]
 // CHECK:STDOUT:   %pattern_type.2f8: type = pattern_type %i16 [concrete]
 // CHECK:STDOUT:   %foo_short.type: type = fn_type @foo_short [concrete]
 // CHECK:STDOUT:   %foo_short: %foo_short.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.251: type = ptr_type %i16 [concrete]
+// CHECK:STDOUT:   %foo_short__carbon_thunk.type: type = fn_type @foo_short__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo_short__carbon_thunk: %foo_short__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.type.613: type = fn_type @Int.as.Destroy.impl.Op, @Int.as.Destroy.impl(%int_16) [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.536: %Int.as.Destroy.impl.Op.type.613 = struct_value () [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -1700,6 +1725,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo_short__carbon_thunk.decl: %foo_short__carbon_thunk.type = fn_decl @foo_short__carbon_thunk [concrete = constants.%foo_short__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F() {
@@ -1709,14 +1739,22 @@ fn F() {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo_short.ref: %foo_short.type = name_ref foo_short, imports.%foo_short.decl [concrete = constants.%foo_short]
-// CHECK:STDOUT:   %foo_short.call: init %i16 = call %foo_short.ref()
+// CHECK:STDOUT:   %.loc8_30.1: ref %i16 = temporary_storage
+// CHECK:STDOUT:   %addr.loc8_30.1: %ptr.251 = addr_of %.loc8_30.1
+// CHECK:STDOUT:   %foo_short__carbon_thunk.call: init %empty_tuple.type = call imports.%foo_short__carbon_thunk.decl(%addr.loc8_30.1)
+// CHECK:STDOUT:   %.loc8_30.2: init %i16 = in_place_init %foo_short__carbon_thunk.call, %.loc8_30.1
 // CHECK:STDOUT:   %.loc8_10: type = splice_block %i16 [concrete = constants.%i16] {
 // CHECK:STDOUT:     %int_16: Core.IntLiteral = int_value 16 [concrete = constants.%int_16]
 // CHECK:STDOUT:     %i16: type = class_type @Int, @Int(constants.%int_16) [concrete = constants.%i16]
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %.loc8_30.1: %i16 = value_of_initializer %foo_short.call
-// CHECK:STDOUT:   %.loc8_30.2: %i16 = converted %foo_short.call, %.loc8_30.1
-// CHECK:STDOUT:   %x: %i16 = bind_name x, %.loc8_30.2
+// CHECK:STDOUT:   %.loc8_30.3: %i16 = value_of_initializer %.loc8_30.2
+// CHECK:STDOUT:   %.loc8_30.4: %i16 = converted %.loc8_30.2, %.loc8_30.3
+// CHECK:STDOUT:   %x: %i16 = bind_name x, %.loc8_30.4
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_30.1, constants.%Int.as.Destroy.impl.Op.536
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_30.1, %Int.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr.loc8_30.2: %ptr.251 = addr_of %.loc8_30.1
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_30.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -1744,11 +1782,17 @@ fn F() {
 // CHECK:STDOUT: --- import_double_return.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
 // CHECK:STDOUT:   %int_64: Core.IntLiteral = int_value 64 [concrete]
 // CHECK:STDOUT:   %f64.d77: type = class_type @Float, @Float(%int_64) [concrete]
 // CHECK:STDOUT:   %pattern_type.0ae: type = pattern_type %f64.d77 [concrete]
 // CHECK:STDOUT:   %foo_double.type: type = fn_type @foo_double [concrete]
 // CHECK:STDOUT:   %foo_double: %foo_double.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.bcc: type = ptr_type %f64.d77 [concrete]
+// CHECK:STDOUT:   %foo_double__carbon_thunk.type: type = fn_type @foo_double__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo_double__carbon_thunk: %foo_double__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %Float.as.Destroy.impl.Op.type.cd5: type = fn_type @Float.as.Destroy.impl.Op, @Float.as.Destroy.impl(%int_64) [concrete]
+// CHECK:STDOUT:   %Float.as.Destroy.impl.Op.b8c: %Float.as.Destroy.impl.Op.type.cd5 = struct_value () [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -1761,6 +1805,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo_double__carbon_thunk.decl: %foo_double__carbon_thunk.type = fn_decl @foo_double__carbon_thunk [concrete = constants.%foo_double__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F() {
@@ -1770,14 +1819,22 @@ fn F() {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo_double.ref: %foo_double.type = name_ref foo_double, imports.%foo_double.decl [concrete = constants.%foo_double]
-// CHECK:STDOUT:   %foo_double.call: init %f64.d77 = call %foo_double.ref()
+// CHECK:STDOUT:   %.loc8_31.1: ref %f64.d77 = temporary_storage
+// CHECK:STDOUT:   %addr.loc8_31.1: %ptr.bcc = addr_of %.loc8_31.1
+// CHECK:STDOUT:   %foo_double__carbon_thunk.call: init %empty_tuple.type = call imports.%foo_double__carbon_thunk.decl(%addr.loc8_31.1)
+// CHECK:STDOUT:   %.loc8_31.2: init %f64.d77 = in_place_init %foo_double__carbon_thunk.call, %.loc8_31.1
 // CHECK:STDOUT:   %.loc8_10: type = splice_block %f64 [concrete = constants.%f64.d77] {
 // CHECK:STDOUT:     %int_64: Core.IntLiteral = int_value 64 [concrete = constants.%int_64]
 // CHECK:STDOUT:     %f64: type = class_type @Float, @Float(constants.%int_64) [concrete = constants.%f64.d77]
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %.loc8_31.1: %f64.d77 = value_of_initializer %foo_double.call
-// CHECK:STDOUT:   %.loc8_31.2: %f64.d77 = converted %foo_double.call, %.loc8_31.1
-// CHECK:STDOUT:   %x: %f64.d77 = bind_name x, %.loc8_31.2
+// CHECK:STDOUT:   %.loc8_31.3: %f64.d77 = value_of_initializer %.loc8_31.2
+// CHECK:STDOUT:   %.loc8_31.4: %f64.d77 = converted %.loc8_31.2, %.loc8_31.3
+// CHECK:STDOUT:   %x: %f64.d77 = bind_name x, %.loc8_31.4
+// CHECK:STDOUT:   %Float.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_31.1, constants.%Float.as.Destroy.impl.Op.b8c
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_31.1, %Float.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr.loc8_31.2: %ptr.bcc = addr_of %.loc8_31.1
+// CHECK:STDOUT:   %Float.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_31.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 31 - 6
toolchain/check/testdata/interop/cpp/function/class.carbon

@@ -447,13 +447,29 @@ auto foo() -> C;
 
 library "[[@TEST_NAME]]";
 
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+12]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:4:6: error: calling 'foo' with incomplete return type 'C' [CppInteropParseError]
+// CHECK:STDERR:     4 | auto foo() -> C;
+// CHECK:STDERR:       |      ^~~
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+8]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:4:6: note: 'foo' declared here [CppInteropParseNote]
+// CHECK:STDERR:     4 | auto foo() -> C;
+// CHECK:STDERR:       |      ^
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+4]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:2:7: note: forward declaration of 'C' [CppInteropParseNote]
+// CHECK:STDERR:     2 | class C;
+// CHECK:STDERR:       |       ^
 import Cpp library "decl_value_return_type.h";
 
 fn F() {
+  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+13]]:3: note: in `Cpp` name lookup for `foo` [InCppNameLookup]
+  // CHECK:STDERR:   Cpp.foo();
+  // CHECK:STDERR:   ^~~~~~~
+  // CHECK:STDERR:
   // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+9]]:3: error: function returns incomplete type `Cpp.C` [IncompleteTypeInFunctionReturnType]
   // CHECK:STDERR:   Cpp.foo();
   // CHECK:STDERR:   ^~~~~~~~~
-  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
+  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE-10]]:10: in file included here [InCppInclude]
   // CHECK:STDERR: ./decl_value_return_type.h:2:7: note: class was forward declared here [ClassForwardDeclaredHere]
   // CHECK:STDERR: class C;
   // CHECK:STDERR:       ^
@@ -1157,9 +1173,11 @@ fn F() {
 // CHECK:STDOUT:   %C: type = class_type @C [concrete]
 // CHECK:STDOUT:   %foo.type: type = fn_type @foo [concrete]
 // CHECK:STDOUT:   %foo: %foo.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.d9e: type = ptr_type %C [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk.type: type = fn_type @foo__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk: %foo__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.type.1b3: type = fn_type @T.as.Destroy.impl.Op, @T.as.Destroy.impl(%C) [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.21b: %T.as.Destroy.impl.Op.type.1b3 = struct_value () [concrete]
-// CHECK:STDOUT:   %ptr.d9e: type = ptr_type %C [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -1172,6 +1190,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo__carbon_thunk.decl: %foo__carbon_thunk.type = fn_decl @foo__carbon_thunk [concrete = constants.%foo__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F() {
@@ -1179,13 +1202,15 @@ fn F() {
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo.ref: %foo.type = name_ref foo, imports.%foo.decl [concrete = constants.%foo]
 // CHECK:STDOUT:   %.loc8_11.1: ref %C = temporary_storage
-// CHECK:STDOUT:   %foo.call: init %C = call %foo.ref() to %.loc8_11.1
-// CHECK:STDOUT:   %.loc8_11.2: ref %C = temporary %.loc8_11.1, %foo.call
+// CHECK:STDOUT:   %addr.loc8_11.1: %ptr.d9e = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %foo__carbon_thunk.call: init %empty_tuple.type = call imports.%foo__carbon_thunk.decl(%addr.loc8_11.1)
+// CHECK:STDOUT:   %.loc8_11.2: init %C = in_place_init %foo__carbon_thunk.call, %.loc8_11.1
+// CHECK:STDOUT:   %.loc8_11.3: ref %C = temporary %.loc8_11.1, %.loc8_11.2
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_11.1, constants.%T.as.Destroy.impl.Op.21b
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_11.1, %T.as.Destroy.impl.Op.specific_fn
-// CHECK:STDOUT:   %addr: %ptr.d9e = addr_of %.loc8_11.1
-// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr)
+// CHECK:STDOUT:   %addr.loc8_11.2: %ptr.d9e = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_11.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 36 - 4
toolchain/check/testdata/interop/cpp/function/full_semir.carbon

@@ -284,6 +284,7 @@ fn F() {
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
 // CHECK:STDOUT:   %F.type: type = fn_type @F [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
 // CHECK:STDOUT:   %F: %F.type = struct_value () [concrete]
 // CHECK:STDOUT:   %int_16: Core.IntLiteral = int_value 16 [concrete]
 // CHECK:STDOUT:   %Int.type: type = generic_class_type @Int [concrete]
@@ -292,11 +293,20 @@ fn F() {
 // CHECK:STDOUT:   %pattern_type.2f8: type = pattern_type %i16 [concrete]
 // CHECK:STDOUT:   %foo_short.type: type = fn_type @foo_short [concrete]
 // CHECK:STDOUT:   %foo_short: %foo_short.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.251: type = ptr_type %i16 [concrete]
+// CHECK:STDOUT:   %pattern_type.54c: type = pattern_type %ptr.251 [concrete]
+// CHECK:STDOUT:   %foo_short__carbon_thunk.type: type = fn_type @foo_short__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo_short__carbon_thunk: %foo_short__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %Destroy.type: type = facet_type <@Destroy> [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.type.613: type = fn_type @Int.as.Destroy.impl.Op, @Int.as.Destroy.impl(%int_16) [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.536: %Int.as.Destroy.impl.Op.type.613 = struct_value () [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.specific_fn: <specific function> = specific_function %Int.as.Destroy.impl.Op.536, @Int.as.Destroy.impl.Op(%int_16) [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
 // CHECK:STDOUT:   %Core: <namespace> = namespace file.%Core.import, [concrete] {
 // CHECK:STDOUT:     .Int = %Core.Int
+// CHECK:STDOUT:     .Destroy = %Core.Destroy
 // CHECK:STDOUT:     import Core//prelude
 // CHECK:STDOUT:     import Core//prelude/...
 // CHECK:STDOUT:   }
@@ -314,6 +324,18 @@ fn F() {
 // CHECK:STDOUT:     %return.param: ref %i16 = out_param call_param0
 // CHECK:STDOUT:     %return: ref %i16 = return_slot %return.param
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo_short__carbon_thunk.decl: %foo_short__carbon_thunk.type = fn_decl @foo_short__carbon_thunk [concrete = constants.%foo_short__carbon_thunk] {
+// CHECK:STDOUT:     %return.patt: %pattern_type.54c = binding_pattern r#return [concrete]
+// CHECK:STDOUT:     %return.param_patt: %pattern_type.54c = value_param_pattern %return.patt, call_param0 [concrete]
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     %return.param: %ptr.251 = value_param call_param0
+// CHECK:STDOUT:     %.1: type = splice_block constants.%ptr.251 [concrete = constants.%ptr.251] {
+// CHECK:STDOUT:       %int_16: Core.IntLiteral = int_value 16 [concrete = constants.%int_16]
+// CHECK:STDOUT:       %i16: type = class_type @Int, @Int(constants.%int_16) [concrete = constants.%i16]
+// CHECK:STDOUT:     }
+// CHECK:STDOUT:     %return: %ptr.251 = bind_name r#return, %return.param
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %Core.Destroy: type = import_ref Core//prelude/parts/destroy, Destroy, loaded [concrete = constants.%Destroy.type]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: file {
@@ -336,16 +358,26 @@ fn F() {
 // CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo_short.ref: %foo_short.type = name_ref foo_short, imports.%foo_short.decl [concrete = constants.%foo_short]
-// CHECK:STDOUT:   %foo_short.call: init %i16 = call %foo_short.ref()
+// CHECK:STDOUT:   %.loc7_30.1: ref %i16 = temporary_storage
+// CHECK:STDOUT:   %addr.loc7_30.1: %ptr.251 = addr_of %.loc7_30.1
+// CHECK:STDOUT:   %foo_short__carbon_thunk.call: init %empty_tuple.type = call imports.%foo_short__carbon_thunk.decl(%addr.loc7_30.1)
+// CHECK:STDOUT:   %.loc7_30.2: init %i16 = in_place_init %foo_short__carbon_thunk.call, %.loc7_30.1
 // CHECK:STDOUT:   %.loc7_10: type = splice_block %i16 [concrete = constants.%i16] {
 // CHECK:STDOUT:     %int_16: Core.IntLiteral = int_value 16 [concrete = constants.%int_16]
 // CHECK:STDOUT:     %i16: type = class_type @Int, @Int(constants.%int_16) [concrete = constants.%i16]
 // CHECK:STDOUT:   }
-// CHECK:STDOUT:   %.loc7_30.1: %i16 = value_of_initializer %foo_short.call
-// CHECK:STDOUT:   %.loc7_30.2: %i16 = converted %foo_short.call, %.loc7_30.1
-// CHECK:STDOUT:   %x: %i16 = bind_name x, %.loc7_30.2
+// CHECK:STDOUT:   %.loc7_30.3: %i16 = value_of_initializer %.loc7_30.2
+// CHECK:STDOUT:   %.loc7_30.4: %i16 = converted %.loc7_30.2, %.loc7_30.3
+// CHECK:STDOUT:   %x: %i16 = bind_name x, %.loc7_30.4
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc7_30.1, constants.%Int.as.Destroy.impl.Op.536
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.specific_fn: <specific function> = specific_function constants.%Int.as.Destroy.impl.Op.536, @Int.as.Destroy.impl.Op(constants.%int_16) [concrete = constants.%Int.as.Destroy.impl.Op.specific_fn]
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc7_30.1, %Int.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr.loc7_30.2: %ptr.251 = addr_of %.loc7_30.1
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc7_30.2)
 // CHECK:STDOUT:   return
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @foo_short() -> %i16;
 // CHECK:STDOUT:
+// CHECK:STDOUT: fn @foo_short__carbon_thunk(%return.param: %ptr.251);
+// CHECK:STDOUT:

+ 22 - 4
toolchain/check/testdata/interop/cpp/function/return.carbon

@@ -49,8 +49,13 @@ fn F() {
 // CHECK:STDOUT:   %IngestI32: %IngestI32.type = struct_value () [concrete]
 // CHECK:STDOUT:   %foo1.type: type = fn_type @foo1 [concrete]
 // CHECK:STDOUT:   %foo1: %foo1.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.251: type = ptr_type %i16 [concrete]
+// CHECK:STDOUT:   %foo1__carbon_thunk.type: type = fn_type @foo1__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo1__carbon_thunk: %foo1__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %foo2.type: type = fn_type @foo2 [concrete]
 // CHECK:STDOUT:   %foo2: %foo2.type = struct_value () [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.type.613: type = fn_type @Int.as.Destroy.impl.Op, @Int.as.Destroy.impl(%int_16) [concrete]
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.536: %Int.as.Destroy.impl.Op.type.613 = struct_value () [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -64,6 +69,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo1__carbon_thunk.decl: %foo1__carbon_thunk.type = fn_decl @foo1__carbon_thunk [concrete = constants.%foo1__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT:   %foo2.decl: %foo2.type = fn_decl @foo2 [concrete = constants.%foo2] {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   } {
@@ -76,10 +86,13 @@ fn F() {
 // CHECK:STDOUT:   %IngestI16.ref: %IngestI16.type = name_ref IngestI16, file.%IngestI16.decl [concrete = constants.%IngestI16]
 // CHECK:STDOUT:   %Cpp.ref.loc11: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo1.ref: %foo1.type = name_ref foo1, imports.%foo1.decl [concrete = constants.%foo1]
-// CHECK:STDOUT:   %foo1.call: init %i16 = call %foo1.ref()
-// CHECK:STDOUT:   %.loc11_22.1: %i16 = value_of_initializer %foo1.call
-// CHECK:STDOUT:   %.loc11_22.2: %i16 = converted %foo1.call, %.loc11_22.1
-// CHECK:STDOUT:   %IngestI16.call: init %empty_tuple.type = call %IngestI16.ref(%.loc11_22.2)
+// CHECK:STDOUT:   %.loc11_22.1: ref %i16 = temporary_storage
+// CHECK:STDOUT:   %addr.loc11_22.1: %ptr.251 = addr_of %.loc11_22.1
+// CHECK:STDOUT:   %foo1__carbon_thunk.call: init %empty_tuple.type = call imports.%foo1__carbon_thunk.decl(%addr.loc11_22.1)
+// CHECK:STDOUT:   %.loc11_22.2: init %i16 = in_place_init %foo1__carbon_thunk.call, %.loc11_22.1
+// CHECK:STDOUT:   %.loc11_22.3: %i16 = value_of_initializer %.loc11_22.2
+// CHECK:STDOUT:   %.loc11_22.4: %i16 = converted %.loc11_22.2, %.loc11_22.3
+// CHECK:STDOUT:   %IngestI16.call: init %empty_tuple.type = call %IngestI16.ref(%.loc11_22.4)
 // CHECK:STDOUT:   %IngestI32.ref: %IngestI32.type = name_ref IngestI32, file.%IngestI32.decl [concrete = constants.%IngestI32]
 // CHECK:STDOUT:   %Cpp.ref.loc12: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo2.ref: %foo2.type = name_ref foo2, imports.%foo2.decl [concrete = constants.%foo2]
@@ -87,6 +100,11 @@ fn F() {
 // CHECK:STDOUT:   %.loc12_22.1: %i32 = value_of_initializer %foo2.call
 // CHECK:STDOUT:   %.loc12_22.2: %i32 = converted %foo2.call, %.loc12_22.1
 // CHECK:STDOUT:   %IngestI32.call: init %empty_tuple.type = call %IngestI32.ref(%.loc12_22.2)
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc11_22.1, constants.%Int.as.Destroy.impl.Op.536
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc11_22.1, %Int.as.Destroy.impl.Op.specific_fn
+// CHECK:STDOUT:   %addr.loc11_22.2: %ptr.251 = addr_of %.loc11_22.1
+// CHECK:STDOUT:   %Int.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc11_22.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 32 - 6
toolchain/check/testdata/interop/cpp/function/struct.carbon

@@ -445,13 +445,30 @@ auto foo() -> S;
 
 library "[[@TEST_NAME]]";
 
+// TODO: We should only produce one error here.
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+12]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:4:6: error: calling 'foo' with incomplete return type 'S' [CppInteropParseError]
+// CHECK:STDERR:     4 | auto foo() -> S;
+// CHECK:STDERR:       |      ^~~
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+8]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:4:6: note: 'foo' declared here [CppInteropParseNote]
+// CHECK:STDERR:     4 | auto foo() -> S;
+// CHECK:STDERR:       |      ^
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+4]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:2:8: note: forward declaration of 'S' [CppInteropParseNote]
+// CHECK:STDERR:     2 | struct S;
+// CHECK:STDERR:       |        ^
 import Cpp library "decl_value_return_type.h";
 
 fn F() {
+  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+13]]:3: note: in `Cpp` name lookup for `foo` [InCppNameLookup]
+  // CHECK:STDERR:   Cpp.foo();
+  // CHECK:STDERR:   ^~~~~~~
+  // CHECK:STDERR:
   // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+9]]:3: error: function returns incomplete type `Cpp.S` [IncompleteTypeInFunctionReturnType]
   // CHECK:STDERR:   Cpp.foo();
   // CHECK:STDERR:   ^~~~~~~~~
-  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
+  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE-10]]:10: in file included here [InCppInclude]
   // CHECK:STDERR: ./decl_value_return_type.h:2:8: note: class was forward declared here [ClassForwardDeclaredHere]
   // CHECK:STDERR: struct S;
   // CHECK:STDERR:        ^
@@ -1155,9 +1172,11 @@ fn F() {
 // CHECK:STDOUT:   %S: type = class_type @S [concrete]
 // CHECK:STDOUT:   %foo.type: type = fn_type @foo [concrete]
 // CHECK:STDOUT:   %foo: %foo.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.5c7: type = ptr_type %S [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk.type: type = fn_type @foo__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk: %foo__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.type.642: type = fn_type @T.as.Destroy.impl.Op, @T.as.Destroy.impl(%S) [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.ab5: %T.as.Destroy.impl.Op.type.642 = struct_value () [concrete]
-// CHECK:STDOUT:   %ptr.5c7: type = ptr_type %S [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -1170,6 +1189,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo__carbon_thunk.decl: %foo__carbon_thunk.type = fn_decl @foo__carbon_thunk [concrete = constants.%foo__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F() {
@@ -1177,13 +1201,15 @@ fn F() {
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo.ref: %foo.type = name_ref foo, imports.%foo.decl [concrete = constants.%foo]
 // CHECK:STDOUT:   %.loc8_11.1: ref %S = temporary_storage
-// CHECK:STDOUT:   %foo.call: init %S = call %foo.ref() to %.loc8_11.1
-// CHECK:STDOUT:   %.loc8_11.2: ref %S = temporary %.loc8_11.1, %foo.call
+// CHECK:STDOUT:   %addr.loc8_11.1: %ptr.5c7 = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %foo__carbon_thunk.call: init %empty_tuple.type = call imports.%foo__carbon_thunk.decl(%addr.loc8_11.1)
+// CHECK:STDOUT:   %.loc8_11.2: init %S = in_place_init %foo__carbon_thunk.call, %.loc8_11.1
+// CHECK:STDOUT:   %.loc8_11.3: ref %S = temporary %.loc8_11.1, %.loc8_11.2
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_11.1, constants.%T.as.Destroy.impl.Op.ab5
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_11.1, %T.as.Destroy.impl.Op.specific_fn
-// CHECK:STDOUT:   %addr: %ptr.5c7 = addr_of %.loc8_11.1
-// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr)
+// CHECK:STDOUT:   %addr.loc8_11.2: %ptr.5c7 = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_11.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 50 - 2
toolchain/check/testdata/interop/cpp/function/thunk_ast.carbon

@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // INCLUDE-FILE: toolchain/testing/testdata/min_prelude/int.carbon
-// EXTRA-ARGS: --dump-cpp-ast
+// EXTRA-ARGS: --dump-cpp-ast --target=x86_64-linux-gnu
 // SET-CHECK-SUBSET
 //
 // AUTOUPDATE
@@ -18,7 +18,55 @@
 auto foo(short a) -> void;
 // CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <./thunk_required.h:[[@LINE-1]]:1, col:22> col:6 used foo 'auto (short) -> void'
 // CHECK:STDOUT: | `-ParmVarDecl {{0x[a-f0-9]+}} <col:10, col:16> col:16 a 'short'
-// CHECK:STDOUT: `-FunctionDecl {{0x[a-f0-9]+}} <col:6> col:6 foo__carbon_thunk 'void (short * _Nonnull)' extern
+// CHECK:STDOUT: |-NamespaceDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit std
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator new 'void *(__size_t)'
+// CHECK:STDOUT: | |-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ReturnsNonNullAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit
+// CHECK:STDOUT: | `-AllocSizeAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit 1
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator new 'void *(__size_t, std::align_val_t)'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'std::align_val_t'
+// CHECK:STDOUT: | |-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ReturnsNonNullAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit
+// CHECK:STDOUT: | |-AllocSizeAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit 1
+// CHECK:STDOUT: | `-AllocAlignAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit 2
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator new[] 'void *(__size_t)'
+// CHECK:STDOUT: | |-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ReturnsNonNullAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit
+// CHECK:STDOUT: | `-AllocSizeAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit 1
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator new[] 'void *(__size_t, std::align_val_t)'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'std::align_val_t'
+// CHECK:STDOUT: | |-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ReturnsNonNullAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit
+// CHECK:STDOUT: | |-AllocSizeAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit 1
+// CHECK:STDOUT: | `-AllocAlignAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit 2
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator delete 'void (void *) noexcept'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator delete 'void (void *, std::align_val_t) noexcept'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'std::align_val_t'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'std::align_val_t'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator delete[] 'void (void *) noexcept'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit operator delete[] 'void (void *, std::align_val_t) noexcept'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'std::align_val_t'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'void *'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<invalid sloc>> <invalid sloc> implicit 'std::align_val_t'
+// CHECK:STDOUT: | `-VisibilityAttr {{0x[a-f0-9]+}} <<invalid sloc>> Implicit Default
+// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} <<carbon-internal>:8:1, line:14:1> line:8:7 operator new 'void *(unsigned long, void *) noexcept'
+// CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} <<built-in>:173:23, col:37> <carbon-internal>:8:33 'unsigned long'
+// CHECK:STDOUT: | `-ParmVarDecl {{0x[a-f0-9]+}} <col:35, col:39> col:40 'void *'
+// CHECK:STDOUT: `-FunctionDecl {{0x[a-f0-9]+}} <./thunk_required.h:[[@LINE-51]]:6> col:6 foo__carbon_thunk 'void (short * _Nonnull)' extern
 // CHECK:STDOUT:   |-ParmVarDecl {{0x[a-f0-9]+}} <col:6> col:6 used a 'short * _Nonnull':'short *'
 // CHECK:STDOUT:   |-ReturnStmt {{0x[a-f0-9]+}} <col:6>
 // CHECK:STDOUT:   | `-CallExpr {{0x[a-f0-9]+}} <col:6> 'void'

+ 31 - 6
toolchain/check/testdata/interop/cpp/function/union.carbon

@@ -408,13 +408,29 @@ auto foo() -> U;
 
 library "[[@TEST_NAME]]";
 
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+12]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:4:6: error: calling 'foo' with incomplete return type 'U' [CppInteropParseError]
+// CHECK:STDERR:     4 | auto foo() -> U;
+// CHECK:STDERR:       |      ^~~
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+8]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:4:6: note: 'foo' declared here [CppInteropParseNote]
+// CHECK:STDERR:     4 | auto foo() -> U;
+// CHECK:STDERR:       |      ^
+// CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+4]]:10: in file included here [InCppInclude]
+// CHECK:STDERR: ./decl_value_return_type.h:2:7: note: forward declaration of 'U' [CppInteropParseNote]
+// CHECK:STDERR:     2 | union U;
+// CHECK:STDERR:       |       ^
 import Cpp library "decl_value_return_type.h";
 
 fn F() {
+  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+13]]:3: note: in `Cpp` name lookup for `foo` [InCppNameLookup]
+  // CHECK:STDERR:   Cpp.foo();
+  // CHECK:STDERR:   ^~~~~~~
+  // CHECK:STDERR:
   // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE+9]]:3: error: function returns incomplete type `Cpp.U` [IncompleteTypeInFunctionReturnType]
   // CHECK:STDERR:   Cpp.foo();
   // CHECK:STDERR:   ^~~~~~~~~
-  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE-6]]:10: in file included here [InCppInclude]
+  // CHECK:STDERR: fail_import_decl_value_return_type.carbon:[[@LINE-10]]:10: in file included here [InCppInclude]
   // CHECK:STDERR: ./decl_value_return_type.h:2:7: note: class was forward declared here [ClassForwardDeclaredHere]
   // CHECK:STDERR: union U;
   // CHECK:STDERR:       ^
@@ -1104,9 +1120,11 @@ fn F() {
 // CHECK:STDOUT:   %U: type = class_type @U [concrete]
 // CHECK:STDOUT:   %foo.type: type = fn_type @foo [concrete]
 // CHECK:STDOUT:   %foo: %foo.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.86f: type = ptr_type %U [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk.type: type = fn_type @foo__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk: %foo__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.type.0b7: type = fn_type @T.as.Destroy.impl.Op, @T.as.Destroy.impl(%U) [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.2fa: %T.as.Destroy.impl.Op.type.0b7 = struct_value () [concrete]
-// CHECK:STDOUT:   %ptr.86f: type = ptr_type %U [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -1119,6 +1137,11 @@ fn F() {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo__carbon_thunk.decl: %foo__carbon_thunk.type = fn_decl @foo__carbon_thunk [concrete = constants.%foo__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F() {
@@ -1126,13 +1149,15 @@ fn F() {
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo.ref: %foo.type = name_ref foo, imports.%foo.decl [concrete = constants.%foo]
 // CHECK:STDOUT:   %.loc8_11.1: ref %U = temporary_storage
-// CHECK:STDOUT:   %foo.call: init %U = call %foo.ref() to %.loc8_11.1
-// CHECK:STDOUT:   %.loc8_11.2: ref %U = temporary %.loc8_11.1, %foo.call
+// CHECK:STDOUT:   %addr.loc8_11.1: %ptr.86f = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %foo__carbon_thunk.call: init %empty_tuple.type = call imports.%foo__carbon_thunk.decl(%addr.loc8_11.1)
+// CHECK:STDOUT:   %.loc8_11.2: init %U = in_place_init %foo__carbon_thunk.call, %.loc8_11.1
+// CHECK:STDOUT:   %.loc8_11.3: ref %U = temporary %.loc8_11.1, %.loc8_11.2
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_11.1, constants.%T.as.Destroy.impl.Op.2fa
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_11.1, %T.as.Destroy.impl.Op.specific_fn
-// CHECK:STDOUT:   %addr: %ptr.86f = addr_of %.loc8_11.1
-// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr)
+// CHECK:STDOUT:   %addr.loc8_11.2: %ptr.86f = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_11.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 14 - 5
toolchain/check/testdata/interop/cpp/namespace.carbon

@@ -354,9 +354,11 @@ fn Use(y: Cpp.Y) -> i32 {
 // CHECK:STDOUT:   %X: type = class_type @X [concrete]
 // CHECK:STDOUT:   %foo.type: type = fn_type @foo [concrete]
 // CHECK:STDOUT:   %foo: %foo.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.13d: type = ptr_type %X [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk.type: type = fn_type @foo__carbon_thunk [concrete]
+// CHECK:STDOUT:   %foo__carbon_thunk: %foo__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.type.0e5: type = fn_type @T.as.Destroy.impl.Op, @T.as.Destroy.impl(%X) [concrete]
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.cbf: %T.as.Destroy.impl.Op.type.0e5 = struct_value () [concrete]
-// CHECK:STDOUT:   %ptr.13d: type = ptr_type %X [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -369,6 +371,11 @@ fn Use(y: Cpp.Y) -> i32 {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %foo__carbon_thunk.decl: %foo__carbon_thunk.type = fn_decl @foo__carbon_thunk [concrete = constants.%foo__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @MyF() {
@@ -376,13 +383,15 @@ fn Use(y: Cpp.Y) -> i32 {
 // CHECK:STDOUT:   %Cpp.ref: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
 // CHECK:STDOUT:   %foo.ref: %foo.type = name_ref foo, imports.%foo.decl [concrete = constants.%foo]
 // CHECK:STDOUT:   %.loc8_11.1: ref %X = temporary_storage
-// CHECK:STDOUT:   %foo.call: init %X = call %foo.ref() to %.loc8_11.1
-// CHECK:STDOUT:   %.loc8_11.2: ref %X = temporary %.loc8_11.1, %foo.call
+// CHECK:STDOUT:   %addr.loc8_11.1: %ptr.13d = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %foo__carbon_thunk.call: init %empty_tuple.type = call imports.%foo__carbon_thunk.decl(%addr.loc8_11.1)
+// CHECK:STDOUT:   %.loc8_11.2: init %X = in_place_init %foo__carbon_thunk.call, %.loc8_11.1
+// CHECK:STDOUT:   %.loc8_11.3: ref %X = temporary %.loc8_11.1, %.loc8_11.2
 // CHECK:STDOUT:   %T.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc8_11.1, constants.%T.as.Destroy.impl.Op.cbf
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT:   %bound_method: <bound method> = bound_method %.loc8_11.1, %T.as.Destroy.impl.Op.specific_fn
-// CHECK:STDOUT:   %addr: %ptr.13d = addr_of %.loc8_11.1
-// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr)
+// CHECK:STDOUT:   %addr.loc8_11.2: %ptr.13d = addr_of %.loc8_11.1
+// CHECK:STDOUT:   %T.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method(%addr.loc8_11.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 34 - 0
toolchain/lower/handle_expr_category.cpp

@@ -35,6 +35,40 @@ auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
   }
 }
 
+auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
+                SemIR::InPlaceInit inst) -> void {
+  auto type = context.GetTypeIdOfInst(inst_id);
+  auto* value = context.GetValue(inst.dest_id);
+
+  // If the initializing representation is by-value, and the value
+  // representation is by-copy, then we need to load from the storage. Otherwise
+  // we want a pointer to the result.
+  switch (context.GetInitRepr(type).kind) {
+    case SemIR::InitRepr::None:
+    case SemIR::InitRepr::InPlace:
+      break;
+    case SemIR::InitRepr::ByCopy:
+      switch (context.GetValueRepr(type).repr.kind) {
+        case SemIR::ValueRepr::Unknown:
+          CARBON_FATAL("Unexpected incomplete type");
+        case SemIR::ValueRepr::None:
+        case SemIR::ValueRepr::Pointer:
+          break;
+        case SemIR::ValueRepr::Copy:
+          value = context.builder().CreateLoad(context.GetType(type), value);
+          break;
+        case SemIR::ValueRepr::Custom:
+          CARBON_FATAL(
+              "TODO: Add support for InPlaceInit with custom value rep");
+      }
+      break;
+    case SemIR::InitRepr::Incomplete:
+      CARBON_FATAL("Unexpected incomplete type");
+  }
+
+  context.SetLocal(inst_id, value);
+}
+
 auto HandleInst(FunctionContext& context, SemIR::InstId inst_id,
                 SemIR::Temporary inst) -> void {
   context.FinishInit(context.GetTypeIdOfInst(inst_id), inst.storage_id,

+ 13 - 2
toolchain/lower/testdata/interop/cpp/method.carbon

@@ -68,8 +68,8 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT:
 // CHECK:STDOUT: define i32 @_CUseVal.Main(ptr %a) !dbg !7 {
 // CHECK:STDOUT: entry:
-// CHECK:STDOUT:   %A.by_val.call = call i32 @_ZNK1A6by_valEv(ptr %a), !dbg !10
-// CHECK:STDOUT:   ret i32 %A.by_val.call, !dbg !11
+// CHECK:STDOUT:   %by_val__carbon_thunk.call = call i32 @_ZNK1A6by_valEv.carbon_thunk(ptr %a), !dbg !10
+// CHECK:STDOUT:   ret i32 %by_val__carbon_thunk.call, !dbg !11
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: ; Function Attrs: mustprogress noinline nounwind optnone
@@ -83,7 +83,18 @@ fn Call(n: Cpp.NeedThunk) {
 // CHECK:STDOUT:   ret i32 %0
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local i32 @_ZNK1A6by_valEv.carbon_thunk(ptr %this) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %this.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %this, ptr %this.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %this.addr, align 8
+// CHECK:STDOUT:   %call = call i32 @_ZNK1A6by_valEv(ptr nonnull align 4 dereferenceable(4) %0)
+// CHECK:STDOUT:   ret i32 %call
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
 // CHECK:STDOUT: attributes #0 = { mustprogress noinline nounwind optnone "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="0" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+// CHECK:STDOUT: attributes #1 = { alwaysinline mustprogress "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="0" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
 // CHECK:STDOUT:
 // CHECK:STDOUT: !llvm.module.flags = !{!0, !1, !2, !3, !4}
 // CHECK:STDOUT: !llvm.dbg.cu = !{!5}

+ 2 - 2
toolchain/lower/testdata/interop/cpp/thunk.carbon → toolchain/lower/testdata/interop/cpp/parameters.carbon

@@ -6,9 +6,9 @@
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
-// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/interop/cpp/thunk.carbon
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/interop/cpp/parameters.carbon
 // TIP: To dump output, run:
-// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/interop/cpp/thunk.carbon
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/interop/cpp/parameters.carbon
 
 // ============================================================================
 // Parameter requires a thunk to be generated

+ 465 - 0
toolchain/lower/testdata/interop/cpp/return.carbon

@@ -0,0 +1,465 @@
+// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/primitives.carbon
+// EXTRA-ARGS: --target=x86_64-linux-gnu
+//
+// AUTOUPDATE
+// TIP: To test this file alone, run:
+// TIP:   bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/interop/cpp/return.carbon
+// TIP: To dump output, run:
+// TIP:   bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/interop/cpp/return.carbon
+
+// --- ints.h
+
+unsigned char ReturnU8();
+unsigned short ReturnU16();
+unsigned int ReturnU32();
+unsigned long ReturnU64();
+
+signed char ReturnI8();
+signed short ReturnI16();
+signed int ReturnI32();
+signed long ReturnI64();
+
+// --- import_ints.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp library "ints.h";
+
+fn GetU8() -> u8 { return Cpp.ReturnU8(); }
+fn GetU16() -> u16 { return Cpp.ReturnU16(); }
+fn GetU32() -> u32 { return Cpp.ReturnU32(); }
+fn GetU64() -> u64 { return Cpp.ReturnU64(); }
+
+fn GetI8() -> i8 { return Cpp.ReturnI8(); }
+fn GetI16() -> i16 { return Cpp.ReturnI16(); }
+fn GetI32() -> i32 { return Cpp.ReturnI32(); }
+fn GetI64() -> i64 { return Cpp.ReturnI64(); }
+
+fn Use(a: u8, b: u16, c: u32, d: u64, e: i8, f: i16, g: i32, h: i64);
+
+fn Lets() {
+  let my_u8: u8 = Cpp.ReturnU8();
+  let my_u16: u16 = Cpp.ReturnU16();
+  let my_u32: u32 = Cpp.ReturnU32();
+  let my_u64: u64 = Cpp.ReturnU64();
+
+  let my_i8: i8 = Cpp.ReturnI8();
+  let my_i16: i16 = Cpp.ReturnI16();
+  let my_i32: i32 = Cpp.ReturnI32();
+  let my_i64: i64 = Cpp.ReturnI64();
+
+  Use(my_u8, my_u16, my_u32, my_u64, my_i8, my_i16, my_i32, my_i64);
+}
+
+fn Vars() {
+  var my_u8: u8 = Cpp.ReturnU8();
+  var my_u16: u16 = Cpp.ReturnU16();
+  var my_u32: u32 = Cpp.ReturnU32();
+  var my_u64: u64 = Cpp.ReturnU64();
+
+  var my_i8: i8 = Cpp.ReturnI8();
+  var my_i16: i16 = Cpp.ReturnI16();
+  var my_i32: i32 = Cpp.ReturnI32();
+  var my_i64: i64 = Cpp.ReturnI64();
+
+  Use(my_u8, my_u16, my_u32, my_u64, my_i8, my_i16, my_i32, my_i64);
+}
+
+// --- class.h
+
+struct X {
+  X();
+  X(const X&);
+  ~X();
+
+  void *p, *q;
+};
+
+X Make();
+
+// --- import_class.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp library "class.h";
+
+fn GetX() -> Cpp.X { return Cpp.Make(); }
+
+fn Let() {
+  let x: Cpp.X = Cpp.Make();
+}
+
+fn Var() {
+  var x: Cpp.X = Cpp.Make();
+}
+
+// CHECK:STDOUT: ; ModuleID = 'import_ints.carbon'
+// CHECK:STDOUT: source_filename = "import_ints.carbon"
+// CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+// CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu"
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i8 @_CGetU8.Main() !dbg !7 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc6_40.1.temp = alloca i8, align 1, !dbg !10
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc6_40.1.temp), !dbg !10
+// CHECK:STDOUT:   call void @_Z8ReturnU8v.carbon_thunk(ptr %.loc6_40.1.temp), !dbg !10
+// CHECK:STDOUT:   %.loc6_40.2 = load i8, ptr %.loc6_40.1.temp, align 1, !dbg !10
+// CHECK:STDOUT:   ret i8 %.loc6_40.2, !dbg !11
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i8 @_Z8ReturnU8v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CGetU16.Main() !dbg !12 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc7_43.1.temp = alloca i16, align 2, !dbg !13
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc7_43.1.temp), !dbg !13
+// CHECK:STDOUT:   call void @_Z9ReturnU16v.carbon_thunk(ptr %.loc7_43.1.temp), !dbg !13
+// CHECK:STDOUT:   %.loc7_43.2 = load i16, ptr %.loc7_43.1.temp, align 2, !dbg !13
+// CHECK:STDOUT:   ret i16 %.loc7_43.2, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i16 @_Z9ReturnU16v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CGetU32.Main() !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %ReturnU32.call = call i32 @_Z9ReturnU32v(), !dbg !16
+// CHECK:STDOUT:   ret i32 %ReturnU32.call, !dbg !17
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i32 @_Z9ReturnU32v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i64 @_CGetU64.Main() !dbg !18 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %ReturnU64.call = call i64 @_Z9ReturnU64v(), !dbg !19
+// CHECK:STDOUT:   ret i64 %ReturnU64.call, !dbg !20
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i64 @_Z9ReturnU64v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i8 @_CGetI8.Main() !dbg !21 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc11_40.1.temp = alloca i8, align 1, !dbg !22
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc11_40.1.temp), !dbg !22
+// CHECK:STDOUT:   call void @_Z8ReturnI8v.carbon_thunk(ptr %.loc11_40.1.temp), !dbg !22
+// CHECK:STDOUT:   %.loc11_40.2 = load i8, ptr %.loc11_40.1.temp, align 1, !dbg !22
+// CHECK:STDOUT:   ret i8 %.loc11_40.2, !dbg !23
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i8 @_Z8ReturnI8v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i16 @_CGetI16.Main() !dbg !24 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc12_43.1.temp = alloca i16, align 2, !dbg !25
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc12_43.1.temp), !dbg !25
+// CHECK:STDOUT:   call void @_Z9ReturnI16v.carbon_thunk(ptr %.loc12_43.1.temp), !dbg !25
+// CHECK:STDOUT:   %.loc12_43.2 = load i16, ptr %.loc12_43.1.temp, align 2, !dbg !25
+// CHECK:STDOUT:   ret i16 %.loc12_43.2, !dbg !26
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i16 @_Z9ReturnI16v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i32 @_CGetI32.Main() !dbg !27 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %ReturnI32.call = call i32 @_Z9ReturnI32v(), !dbg !28
+// CHECK:STDOUT:   ret i32 %ReturnI32.call, !dbg !29
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i32 @_Z9ReturnI32v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: define i64 @_CGetI64.Main() !dbg !30 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %ReturnI64.call = call i64 @_Z9ReturnI64v(), !dbg !31
+// CHECK:STDOUT:   ret i64 %ReturnI64.call, !dbg !32
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare i64 @_Z9ReturnI64v()
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_CUse.Main(i8, i16, i32, i64, i8, i16, i32, i64)
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CLets.Main() !dbg !33 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc19_32.1.temp = alloca i8, align 1, !dbg !34
+// CHECK:STDOUT:   %.loc20_35.1.temp = alloca i16, align 2, !dbg !35
+// CHECK:STDOUT:   %.loc24_32.1.temp = alloca i8, align 1, !dbg !36
+// CHECK:STDOUT:   %.loc25_35.1.temp = alloca i16, align 2, !dbg !37
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc19_32.1.temp), !dbg !34
+// CHECK:STDOUT:   call void @_Z8ReturnU8v.carbon_thunk(ptr %.loc19_32.1.temp), !dbg !34
+// CHECK:STDOUT:   %.loc19_32.2 = load i8, ptr %.loc19_32.1.temp, align 1, !dbg !34
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc20_35.1.temp), !dbg !35
+// CHECK:STDOUT:   call void @_Z9ReturnU16v.carbon_thunk(ptr %.loc20_35.1.temp), !dbg !35
+// CHECK:STDOUT:   %.loc20_35.2 = load i16, ptr %.loc20_35.1.temp, align 2, !dbg !35
+// CHECK:STDOUT:   %ReturnU32.call = call i32 @_Z9ReturnU32v(), !dbg !38
+// CHECK:STDOUT:   %ReturnU64.call = call i64 @_Z9ReturnU64v(), !dbg !39
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc24_32.1.temp), !dbg !36
+// CHECK:STDOUT:   call void @_Z8ReturnI8v.carbon_thunk(ptr %.loc24_32.1.temp), !dbg !36
+// CHECK:STDOUT:   %.loc24_32.2 = load i8, ptr %.loc24_32.1.temp, align 1, !dbg !36
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc25_35.1.temp), !dbg !37
+// CHECK:STDOUT:   call void @_Z9ReturnI16v.carbon_thunk(ptr %.loc25_35.1.temp), !dbg !37
+// CHECK:STDOUT:   %.loc25_35.2 = load i16, ptr %.loc25_35.1.temp, align 2, !dbg !37
+// CHECK:STDOUT:   %ReturnI32.call = call i32 @_Z9ReturnI32v(), !dbg !40
+// CHECK:STDOUT:   %ReturnI64.call = call i64 @_Z9ReturnI64v(), !dbg !41
+// CHECK:STDOUT:   call void @_CUse.Main(i8 %.loc19_32.2, i16 %.loc20_35.2, i32 %ReturnU32.call, i64 %ReturnU64.call, i8 %.loc24_32.2, i16 %.loc25_35.2, i32 %ReturnI32.call, i64 %ReturnI64.call), !dbg !42
+// CHECK:STDOUT:   ret void, !dbg !43
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CVars.Main() !dbg !44 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %my_u8.var = alloca i8, align 1, !dbg !45
+// CHECK:STDOUT:   %.loc33_32.1.temp = alloca i8, align 1, !dbg !46
+// CHECK:STDOUT:   %my_u16.var = alloca i16, align 2, !dbg !47
+// CHECK:STDOUT:   %.loc34_35.1.temp = alloca i16, align 2, !dbg !48
+// CHECK:STDOUT:   %my_u32.var = alloca i32, align 4, !dbg !49
+// CHECK:STDOUT:   %my_u64.var = alloca i64, align 8, !dbg !50
+// CHECK:STDOUT:   %my_i8.var = alloca i8, align 1, !dbg !51
+// CHECK:STDOUT:   %.loc38_32.1.temp = alloca i8, align 1, !dbg !52
+// CHECK:STDOUT:   %my_i16.var = alloca i16, align 2, !dbg !53
+// CHECK:STDOUT:   %.loc39_35.1.temp = alloca i16, align 2, !dbg !54
+// CHECK:STDOUT:   %my_i32.var = alloca i32, align 4, !dbg !55
+// CHECK:STDOUT:   %my_i64.var = alloca i64, align 8, !dbg !56
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_u8.var), !dbg !45
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc33_32.1.temp), !dbg !46
+// CHECK:STDOUT:   call void @_Z8ReturnU8v.carbon_thunk(ptr %.loc33_32.1.temp), !dbg !46
+// CHECK:STDOUT:   %.loc33_32.2 = load i8, ptr %.loc33_32.1.temp, align 1, !dbg !46
+// CHECK:STDOUT:   store i8 %.loc33_32.2, ptr %my_u8.var, align 1, !dbg !45
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_u16.var), !dbg !47
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc34_35.1.temp), !dbg !48
+// CHECK:STDOUT:   call void @_Z9ReturnU16v.carbon_thunk(ptr %.loc34_35.1.temp), !dbg !48
+// CHECK:STDOUT:   %.loc34_35.2 = load i16, ptr %.loc34_35.1.temp, align 2, !dbg !48
+// CHECK:STDOUT:   store i16 %.loc34_35.2, ptr %my_u16.var, align 2, !dbg !47
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_u32.var), !dbg !49
+// CHECK:STDOUT:   %ReturnU32.call = call i32 @_Z9ReturnU32v(), !dbg !57
+// CHECK:STDOUT:   store i32 %ReturnU32.call, ptr %my_u32.var, align 4, !dbg !49
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_u64.var), !dbg !50
+// CHECK:STDOUT:   %ReturnU64.call = call i64 @_Z9ReturnU64v(), !dbg !58
+// CHECK:STDOUT:   store i64 %ReturnU64.call, ptr %my_u64.var, align 4, !dbg !50
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_i8.var), !dbg !51
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc38_32.1.temp), !dbg !52
+// CHECK:STDOUT:   call void @_Z8ReturnI8v.carbon_thunk(ptr %.loc38_32.1.temp), !dbg !52
+// CHECK:STDOUT:   %.loc38_32.2 = load i8, ptr %.loc38_32.1.temp, align 1, !dbg !52
+// CHECK:STDOUT:   store i8 %.loc38_32.2, ptr %my_i8.var, align 1, !dbg !51
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_i16.var), !dbg !53
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc39_35.1.temp), !dbg !54
+// CHECK:STDOUT:   call void @_Z9ReturnI16v.carbon_thunk(ptr %.loc39_35.1.temp), !dbg !54
+// CHECK:STDOUT:   %.loc39_35.2 = load i16, ptr %.loc39_35.1.temp, align 2, !dbg !54
+// CHECK:STDOUT:   store i16 %.loc39_35.2, ptr %my_i16.var, align 2, !dbg !53
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_i32.var), !dbg !55
+// CHECK:STDOUT:   %ReturnI32.call = call i32 @_Z9ReturnI32v(), !dbg !59
+// CHECK:STDOUT:   store i32 %ReturnI32.call, ptr %my_i32.var, align 4, !dbg !55
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %my_i64.var), !dbg !56
+// CHECK:STDOUT:   %ReturnI64.call = call i64 @_Z9ReturnI64v(), !dbg !60
+// CHECK:STDOUT:   store i64 %ReturnI64.call, ptr %my_i64.var, align 4, !dbg !56
+// CHECK:STDOUT:   %.loc43_7 = load i8, ptr %my_u8.var, align 1, !dbg !61
+// CHECK:STDOUT:   %.loc43_14 = load i16, ptr %my_u16.var, align 2, !dbg !62
+// CHECK:STDOUT:   %.loc43_22 = load i32, ptr %my_u32.var, align 4, !dbg !63
+// CHECK:STDOUT:   %.loc43_30 = load i64, ptr %my_u64.var, align 4, !dbg !64
+// CHECK:STDOUT:   %.loc43_38 = load i8, ptr %my_i8.var, align 1, !dbg !65
+// CHECK:STDOUT:   %.loc43_45 = load i16, ptr %my_i16.var, align 2, !dbg !66
+// CHECK:STDOUT:   %.loc43_53 = load i32, ptr %my_i32.var, align 4, !dbg !67
+// CHECK:STDOUT:   %.loc43_61 = load i64, ptr %my_i64.var, align 4, !dbg !68
+// CHECK:STDOUT:   call void @_CUse.Main(i8 %.loc43_7, i16 %.loc43_14, i32 %.loc43_22, i64 %.loc43_30, i8 %.loc43_38, i16 %.loc43_45, i32 %.loc43_53, i64 %.loc43_61), !dbg !69
+// CHECK:STDOUT:   ret void, !dbg !70
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #0
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local void @_Z8ReturnU8v.carbon_thunk(ptr %return) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %return.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %return, ptr %return.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %return.addr, align 8
+// CHECK:STDOUT:   %call = call zeroext i8 @_Z8ReturnU8v()
+// CHECK:STDOUT:   store i8 %call, ptr %0, align 1
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local void @_Z9ReturnU16v.carbon_thunk(ptr %return) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %return.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %return, ptr %return.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %return.addr, align 8
+// CHECK:STDOUT:   %call = call zeroext i16 @_Z9ReturnU16v()
+// CHECK:STDOUT:   store i16 %call, ptr %0, align 2
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local void @_Z8ReturnI8v.carbon_thunk(ptr %return) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %return.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %return, ptr %return.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %return.addr, align 8
+// CHECK:STDOUT:   %call = call signext i8 @_Z8ReturnI8v()
+// CHECK:STDOUT:   store i8 %call, ptr %0, align 1
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local void @_Z9ReturnI16v.carbon_thunk(ptr %return) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %return.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %return, ptr %return.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %return.addr, align 8
+// CHECK:STDOUT:   %call = call signext i16 @_Z9ReturnI16v()
+// CHECK:STDOUT:   store i16 %call, ptr %0, align 2
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; uselistorder directives
+// CHECK:STDOUT: uselistorder ptr @llvm.lifetime.start.p0, { 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT: attributes #1 = { alwaysinline mustprogress "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="0" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1, !2, !3, !4}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!5}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = !{i32 1, !"wchar_size", i32 4}
+// CHECK:STDOUT: !3 = !{i32 8, !"PIC Level", i32 0}
+// CHECK:STDOUT: !4 = !{i32 7, !"PIE Level", i32 2}
+// CHECK:STDOUT: !5 = distinct !DICompileUnit(language: DW_LANG_C, file: !6, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !6 = !DIFile(filename: "import_ints.carbon", directory: "")
+// CHECK:STDOUT: !7 = distinct !DISubprogram(name: "GetU8", linkageName: "_CGetU8.Main", scope: null, file: !6, line: 6, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !8 = !DISubroutineType(types: !9)
+// CHECK:STDOUT: !9 = !{}
+// CHECK:STDOUT: !10 = !DILocation(line: 6, column: 27, scope: !7)
+// CHECK:STDOUT: !11 = !DILocation(line: 6, column: 20, scope: !7)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "GetU16", linkageName: "_CGetU16.Main", scope: null, file: !6, line: 7, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !13 = !DILocation(line: 7, column: 29, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 7, column: 22, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "GetU32", linkageName: "_CGetU32.Main", scope: null, file: !6, line: 8, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !16 = !DILocation(line: 8, column: 29, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 8, column: 22, scope: !15)
+// CHECK:STDOUT: !18 = distinct !DISubprogram(name: "GetU64", linkageName: "_CGetU64.Main", scope: null, file: !6, line: 9, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !19 = !DILocation(line: 9, column: 29, scope: !18)
+// CHECK:STDOUT: !20 = !DILocation(line: 9, column: 22, scope: !18)
+// CHECK:STDOUT: !21 = distinct !DISubprogram(name: "GetI8", linkageName: "_CGetI8.Main", scope: null, file: !6, line: 11, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !22 = !DILocation(line: 11, column: 27, scope: !21)
+// CHECK:STDOUT: !23 = !DILocation(line: 11, column: 20, scope: !21)
+// CHECK:STDOUT: !24 = distinct !DISubprogram(name: "GetI16", linkageName: "_CGetI16.Main", scope: null, file: !6, line: 12, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !25 = !DILocation(line: 12, column: 29, scope: !24)
+// CHECK:STDOUT: !26 = !DILocation(line: 12, column: 22, scope: !24)
+// CHECK:STDOUT: !27 = distinct !DISubprogram(name: "GetI32", linkageName: "_CGetI32.Main", scope: null, file: !6, line: 13, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !28 = !DILocation(line: 13, column: 29, scope: !27)
+// CHECK:STDOUT: !29 = !DILocation(line: 13, column: 22, scope: !27)
+// CHECK:STDOUT: !30 = distinct !DISubprogram(name: "GetI64", linkageName: "_CGetI64.Main", scope: null, file: !6, line: 14, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !31 = !DILocation(line: 14, column: 29, scope: !30)
+// CHECK:STDOUT: !32 = !DILocation(line: 14, column: 22, scope: !30)
+// CHECK:STDOUT: !33 = distinct !DISubprogram(name: "Lets", linkageName: "_CLets.Main", scope: null, file: !6, line: 18, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !34 = !DILocation(line: 19, column: 19, scope: !33)
+// CHECK:STDOUT: !35 = !DILocation(line: 20, column: 21, scope: !33)
+// CHECK:STDOUT: !36 = !DILocation(line: 24, column: 19, scope: !33)
+// CHECK:STDOUT: !37 = !DILocation(line: 25, column: 21, scope: !33)
+// CHECK:STDOUT: !38 = !DILocation(line: 21, column: 21, scope: !33)
+// CHECK:STDOUT: !39 = !DILocation(line: 22, column: 21, scope: !33)
+// CHECK:STDOUT: !40 = !DILocation(line: 26, column: 21, scope: !33)
+// CHECK:STDOUT: !41 = !DILocation(line: 27, column: 21, scope: !33)
+// CHECK:STDOUT: !42 = !DILocation(line: 29, column: 3, scope: !33)
+// CHECK:STDOUT: !43 = !DILocation(line: 18, column: 1, scope: !33)
+// CHECK:STDOUT: !44 = distinct !DISubprogram(name: "Vars", linkageName: "_CVars.Main", scope: null, file: !6, line: 32, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !45 = !DILocation(line: 33, column: 3, scope: !44)
+// CHECK:STDOUT: !46 = !DILocation(line: 33, column: 19, scope: !44)
+// CHECK:STDOUT: !47 = !DILocation(line: 34, column: 3, scope: !44)
+// CHECK:STDOUT: !48 = !DILocation(line: 34, column: 21, scope: !44)
+// CHECK:STDOUT: !49 = !DILocation(line: 35, column: 3, scope: !44)
+// CHECK:STDOUT: !50 = !DILocation(line: 36, column: 3, scope: !44)
+// CHECK:STDOUT: !51 = !DILocation(line: 38, column: 3, scope: !44)
+// CHECK:STDOUT: !52 = !DILocation(line: 38, column: 19, scope: !44)
+// CHECK:STDOUT: !53 = !DILocation(line: 39, column: 3, scope: !44)
+// CHECK:STDOUT: !54 = !DILocation(line: 39, column: 21, scope: !44)
+// CHECK:STDOUT: !55 = !DILocation(line: 40, column: 3, scope: !44)
+// CHECK:STDOUT: !56 = !DILocation(line: 41, column: 3, scope: !44)
+// CHECK:STDOUT: !57 = !DILocation(line: 35, column: 21, scope: !44)
+// CHECK:STDOUT: !58 = !DILocation(line: 36, column: 21, scope: !44)
+// CHECK:STDOUT: !59 = !DILocation(line: 40, column: 21, scope: !44)
+// CHECK:STDOUT: !60 = !DILocation(line: 41, column: 21, scope: !44)
+// CHECK:STDOUT: !61 = !DILocation(line: 43, column: 7, scope: !44)
+// CHECK:STDOUT: !62 = !DILocation(line: 43, column: 14, scope: !44)
+// CHECK:STDOUT: !63 = !DILocation(line: 43, column: 22, scope: !44)
+// CHECK:STDOUT: !64 = !DILocation(line: 43, column: 30, scope: !44)
+// CHECK:STDOUT: !65 = !DILocation(line: 43, column: 38, scope: !44)
+// CHECK:STDOUT: !66 = !DILocation(line: 43, column: 45, scope: !44)
+// CHECK:STDOUT: !67 = !DILocation(line: 43, column: 53, scope: !44)
+// CHECK:STDOUT: !68 = !DILocation(line: 43, column: 61, scope: !44)
+// CHECK:STDOUT: !69 = !DILocation(line: 43, column: 3, scope: !44)
+// CHECK:STDOUT: !70 = !DILocation(line: 32, column: 1, scope: !44)
+// CHECK:STDOUT: ; ModuleID = 'import_class.carbon'
+// CHECK:STDOUT: source_filename = "import_class.carbon"
+// CHECK:STDOUT: target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128"
+// CHECK:STDOUT: target triple = "x86_64-unknown-linux-gnu"
+// CHECK:STDOUT:
+// CHECK:STDOUT: %struct.X = type { ptr, ptr }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CGetX.Main(ptr sret([16 x i8]) %return) !dbg !7 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   call void @_Z4Makev.carbon_thunk(ptr %return), !dbg !10
+// CHECK:STDOUT:   ret void, !dbg !11
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_Z4Makev(ptr sret([16 x i8]))
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CLet.Main() !dbg !12 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc9_27.1.temp = alloca [16 x i8], align 1, !dbg !13
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc9_27.1.temp), !dbg !13
+// CHECK:STDOUT:   call void @_Z4Makev.carbon_thunk(ptr %.loc9_27.1.temp), !dbg !13
+// CHECK:STDOUT:   ret void, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CVar.Main() !dbg !15 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %x.var = alloca [16 x i8], align 1, !dbg !16
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %x.var), !dbg !16
+// CHECK:STDOUT:   call void @_Z4Makev.carbon_thunk(ptr %x.var), !dbg !17
+// CHECK:STDOUT:   ret void, !dbg !18
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite)
+// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #0
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local void @_Z4Makev.carbon_thunk(ptr %return) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %return.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %return, ptr %return.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %return.addr, align 8
+// CHECK:STDOUT:   call void @_Z4Makev(ptr dead_on_unwind writable sret(%struct.X) align 8 %0)
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; uselistorder directives
+// CHECK:STDOUT: uselistorder ptr @llvm.lifetime.start.p0, { 1, 0 }
+// CHECK:STDOUT:
+// CHECK:STDOUT: attributes #0 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) }
+// CHECK:STDOUT: attributes #1 = { alwaysinline mustprogress "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="0" "target-cpu"="x86-64" "target-features"="+cmov,+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
+// CHECK:STDOUT:
+// CHECK:STDOUT: !llvm.module.flags = !{!0, !1, !2, !3, !4}
+// CHECK:STDOUT: !llvm.dbg.cu = !{!5}
+// CHECK:STDOUT:
+// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5}
+// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3}
+// CHECK:STDOUT: !2 = !{i32 1, !"wchar_size", i32 4}
+// CHECK:STDOUT: !3 = !{i32 8, !"PIC Level", i32 0}
+// CHECK:STDOUT: !4 = !{i32 7, !"PIE Level", i32 2}
+// CHECK:STDOUT: !5 = distinct !DICompileUnit(language: DW_LANG_C, file: !6, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug)
+// CHECK:STDOUT: !6 = !DIFile(filename: "import_class.carbon", directory: "")
+// CHECK:STDOUT: !7 = distinct !DISubprogram(name: "GetX", linkageName: "_CGetX.Main", scope: null, file: !6, line: 6, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !8 = !DISubroutineType(types: !9)
+// CHECK:STDOUT: !9 = !{}
+// CHECK:STDOUT: !10 = !DILocation(line: 6, column: 29, scope: !7)
+// CHECK:STDOUT: !11 = !DILocation(line: 6, column: 22, scope: !7)
+// CHECK:STDOUT: !12 = distinct !DISubprogram(name: "Let", linkageName: "_CLet.Main", scope: null, file: !6, line: 8, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !13 = !DILocation(line: 9, column: 18, scope: !12)
+// CHECK:STDOUT: !14 = !DILocation(line: 8, column: 1, scope: !12)
+// CHECK:STDOUT: !15 = distinct !DISubprogram(name: "Var", linkageName: "_CVar.Main", scope: null, file: !6, line: 12, type: !8, spFlags: DISPFlagDefinition, unit: !5)
+// CHECK:STDOUT: !16 = !DILocation(line: 13, column: 3, scope: !15)
+// CHECK:STDOUT: !17 = !DILocation(line: 13, column: 18, scope: !15)
+// CHECK:STDOUT: !18 = !DILocation(line: 12, column: 1, scope: !15)

+ 7 - 0
toolchain/sem_ir/expr_info.cpp

@@ -221,6 +221,7 @@ auto GetExprCategory(const File& file, InstId inst_id) -> ExprCategory {
       case ArrayInit::Kind:
       case Call::Kind:
       case InitializeFrom::Kind:
+      case InPlaceInit::Kind:
       case ClassInit::Kind:
       case StructInit::Kind:
       case TupleInit::Kind:
@@ -273,6 +274,12 @@ auto FindReturnSlotArgForInitializer(const File& sem_ir, InstId init_id)
       case CARBON_KIND(InitializeFrom init): {
         return init.dest_id;
       }
+      case CARBON_KIND(InPlaceInit init): {
+        if (!ReturnTypeInfo::ForType(sem_ir, init.type_id).has_return_slot()) {
+          return InstId::None;
+        }
+        return init.dest_id;
+      }
       case CARBON_KIND(Call call): {
         if (!ReturnTypeInfo::ForType(sem_ir, call.type_id).has_return_slot()) {
           return InstId::None;

+ 1 - 0
toolchain/sem_ir/inst_kind.def

@@ -86,6 +86,7 @@ CARBON_SEM_IR_INST_KIND(ImportCppDecl)
 CARBON_SEM_IR_INST_KIND(ImportDecl)
 CARBON_SEM_IR_INST_KIND(ImportRefLoaded)
 CARBON_SEM_IR_INST_KIND(ImportRefUnloaded)
+CARBON_SEM_IR_INST_KIND(InPlaceInit)
 CARBON_SEM_IR_INST_KIND(InitializeFrom)
 CARBON_SEM_IR_INST_KIND(InstType)
 CARBON_SEM_IR_INST_KIND(InstValue)

+ 20 - 0
toolchain/sem_ir/typed_insts.h

@@ -1017,6 +1017,26 @@ struct ImportRefLoaded {
   EntityNameId entity_name_id;
 };
 
+// Tracks that an object has been initialized in-place to form the result of
+// this expression, even if its type's initializing representation is not
+// normally in-place. If the type does not use in-place initialization,
+// initialization from this expression will copy the value out of the
+// destination.
+//
+// This is used to model the initialization performed by C++ thunks, where
+// in-place initialization is used even for types that would normally have a
+// copy initializing representation.
+struct InPlaceInit {
+  static constexpr auto Kind = InstKind::InPlaceInit.Define<Parse::NodeId>(
+      {.ir_name = "in_place_init", .constant_kind = InstConstantKind::Never});
+
+  TypeId type_id;
+  // Used only to track the source of the initialization; this has no semantic
+  // meaning.
+  InstId src_id;
+  DestInstId dest_id;
+};
+
 // Finalizes the initialization of `dest_id` from the initializer expression
 // `src_id`, by performing a final copy from source to destination, for types
 // whose initialization is not in-place.