Explorar o código

Support for building thunks for C++ methods. (#5972)

Also tweak how we import C++ methods to properly handle C++23's explicit
object parameters.
Richard Smith hai 8 meses
pai
achega
8c9080801c

+ 198 - 91
toolchain/check/cpp_thunk.cpp

@@ -6,6 +6,7 @@
 
 #include "clang/AST/GlobalDecl.h"
 #include "clang/AST/Mangle.h"
+#include "clang/Sema/Lookup.h"
 #include "clang/Sema/Sema.h"
 #include "toolchain/check/call.h"
 #include "toolchain/check/context.h"
@@ -73,11 +74,6 @@ auto IsCppThunkRequired(Context& context, const SemIR::Function& function)
     return false;
   }
 
-  if (function.self_param_id.has_value()) {
-    // TODO: Support member methods.
-    return false;
-  }
-
   SemIR::TypeId return_type_id =
       function.GetDeclaredReturnType(context.sem_ir());
   if (return_type_id.has_value()) {
@@ -119,16 +115,64 @@ static auto IsSimpleAbiType(clang::ASTContext& ast_context,
   return false;
 }
 
+namespace {
+// Information about the callee of a thunk.
+struct CalleeFunctionInfo {
+  explicit CalleeFunctionInfo(clang::FunctionDecl* decl) : decl(decl) {
+    const auto* method_decl = dyn_cast<clang::CXXMethodDecl>(decl);
+    has_object_parameter = method_decl && !method_decl->isStatic() &&
+                           !isa<clang::CXXConstructorDecl>(method_decl);
+    if (has_object_parameter && method_decl->isImplicitObjectMemberFunction()) {
+      implicit_this_type = method_decl->getThisType();
+    }
+  }
+
+  // Returns whether this callee has an implicit `this` parameter.
+  auto has_implicit_object_parameter() const -> bool {
+    return !implicit_this_type.isNull();
+  }
+
+  // Returns whether this callee has an explicit `this` parameter.
+  auto has_explicit_object_parameter() const -> bool {
+    return has_object_parameter && !has_implicit_object_parameter();
+  }
+
+  // Returns the number of parameters the thunk should have.
+  auto num_thunk_params() const -> unsigned {
+    return has_implicit_object_parameter() + decl->getNumParams();
+  }
+
+  auto GetThunkParamIndex(unsigned callee_param_index) const -> unsigned {
+    return has_implicit_object_parameter() + callee_param_index;
+  }
+
+  // The callee function.
+  clang::FunctionDecl* decl;
+
+  // Whether the callee has an object parameter, which might be explicit or
+  // implicit.
+  bool has_object_parameter;
+
+  // If the callee has an implicit object parameter, the corresponding `this`
+  // type. Otherwise a null type.
+  clang::QualType implicit_this_type;
+};
+}  // namespace
+
 // Creates the thunk parameter types given the callee function.
-static auto BuildThunkParameterTypes(
-    clang::ASTContext& ast_context,
-    const clang::FunctionDecl& callee_function_decl)
+static auto BuildThunkParameterTypes(clang::ASTContext& ast_context,
+                                     CalleeFunctionInfo callee_info)
     -> llvm::SmallVector<clang::QualType> {
   llvm::SmallVector<clang::QualType> thunk_param_types;
-  thunk_param_types.reserve(callee_function_decl.getNumParams());
+  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));
+  }
 
   for (const clang::ParmVarDecl* callee_param :
-       callee_function_decl.parameters()) {
+       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) {
@@ -139,54 +183,61 @@ static auto BuildThunkParameterTypes(
     thunk_param_types.push_back(param_type);
   }
 
+  CARBON_CHECK(thunk_param_types.size() == callee_info.num_thunk_params());
   return thunk_param_types;
 }
 
 // Returns the thunk parameters using the callee function parameter identifiers.
-static auto BuildThunkParameters(
-    clang::ASTContext& ast_context,
-    const clang::FunctionDecl& callee_function_decl,
-    clang::FunctionDecl* thunk_function_decl)
+static auto BuildThunkParameters(clang::ASTContext& ast_context,
+                                 CalleeFunctionInfo callee_info,
+                                 clang::FunctionDecl* thunk_function_decl)
     -> llvm::SmallVector<clang::ParmVarDecl*> {
-  clang::SourceLocation clang_loc = callee_function_decl.getLocation();
-
-  unsigned num_params = thunk_function_decl->getNumParams();
-  CARBON_CHECK(callee_function_decl.getNumParams() == num_params);
+  clang::SourceLocation clang_loc = callee_info.decl->getLocation();
 
   const auto* thunk_function_proto_type =
       thunk_function_decl->getFunctionType()->getAs<clang::FunctionProtoType>();
 
   llvm::SmallVector<clang::ParmVarDecl*> thunk_params;
-  thunk_params.reserve(num_params);
-  for (unsigned i = 0; i < num_params; ++i) {
+  unsigned num_thunk_params = thunk_function_decl->getNumParams();
+  thunk_params.reserve(num_thunk_params);
+
+  if (callee_info.has_implicit_object_parameter()) {
+    clang::ParmVarDecl* thunk_param =
+        clang::ParmVarDecl::Create(ast_context, thunk_function_decl, clang_loc,
+                                   clang_loc, &ast_context.Idents.get("this"),
+                                   thunk_function_proto_type->getParamType(0),
+                                   nullptr, clang::SC_None, nullptr);
+    thunk_params.push_back(thunk_param);
+  }
+
+  for (unsigned i : llvm::seq(callee_info.decl->getNumParams())) {
     clang::ParmVarDecl* thunk_param = clang::ParmVarDecl::Create(
         ast_context, thunk_function_decl, clang_loc, clang_loc,
-        callee_function_decl.getParamDecl(i)->getIdentifier(),
-        thunk_function_proto_type->getParamType(i), nullptr, clang::SC_None,
-        nullptr);
+        callee_info.decl->getParamDecl(i)->getIdentifier(),
+        thunk_function_proto_type->getParamType(
+            callee_info.GetThunkParamIndex(i)),
+        nullptr, clang::SC_None, nullptr);
     thunk_params.push_back(thunk_param);
   }
+
+  CARBON_CHECK(thunk_params.size() == num_thunk_params);
   return thunk_params;
 }
 
 // Returns the thunk function declaration given the callee function and the
 // thunk parameter types.
 static auto CreateThunkFunctionDecl(
-    Context& context, const clang::FunctionDecl& callee_function_decl,
+    Context& context, CalleeFunctionInfo callee_info,
     llvm::ArrayRef<clang::QualType> thunk_param_types) -> clang::FunctionDecl* {
   clang::ASTContext& ast_context = context.ast_context();
-  clang::SourceLocation clang_loc = callee_function_decl.getLocation();
+  clang::SourceLocation clang_loc = callee_info.decl->getLocation();
 
   clang::IdentifierInfo& identifier_info = ast_context.Idents.get(
-      callee_function_decl.getNameAsString() + "__carbon_thunk");
-
-  const auto* callee_function_type = callee_function_decl.getFunctionType()
-                                         ->castAs<clang::FunctionProtoType>();
+      callee_info.decl->getNameAsString() + "__carbon_thunk");
 
-  // TODO: Check whether we need to modify `ExtParameterInfo` in `ExtProtoInfo`.
+  auto ext_proto_info = clang::FunctionProtoType::ExtProtoInfo();
   clang::QualType thunk_function_type = ast_context.getFunctionType(
-      callee_function_decl.getReturnType(), thunk_param_types,
-      callee_function_type->getExtProtoInfo());
+      callee_info.decl->getReturnType(), thunk_param_types, ext_proto_info);
 
   clang::DeclContext* decl_context = ast_context.getTranslationUnitDecl();
   // TODO: Thunks should not have external linkage, consider using `SC_Static`.
@@ -196,8 +247,8 @@ static auto CreateThunkFunctionDecl(
       /*TInfo=*/nullptr, clang::SC_Extern);
   decl_context->addDecl(thunk_function_decl);
 
-  thunk_function_decl->setParams(BuildThunkParameters(
-      ast_context, callee_function_decl, thunk_function_decl));
+  thunk_function_decl->setParams(
+      BuildThunkParameters(ast_context, callee_info, thunk_function_decl));
 
   // Set always_inline.
   thunk_function_decl->addAttr(
@@ -207,42 +258,67 @@ static auto CreateThunkFunctionDecl(
   thunk_function_decl->addAttr(clang::AsmLabelAttr::CreateImplicit(
       ast_context,
       GenerateThunkMangledName(*context.sem_ir().clang_mangle_context(),
-                               callee_function_decl),
+                               *callee_info.decl),
       clang_loc));
 
   return thunk_function_decl;
 }
 
-// Takes the thunk function parameters and for each one creates an arg for the
-// callee function which is the thunk parameter or its address.
+// Builds a reference to the given parameter thunk, which is carrying a value of
+// the given type.
+static auto BuildThunkParamRef(clang::Sema& sema,
+                               clang::FunctionDecl* thunk_function_decl,
+                               unsigned thunk_index, clang::QualType type)
+    -> clang::Expr* {
+  clang::ParmVarDecl* thunk_param =
+      thunk_function_decl->getParamDecl(thunk_index);
+  clang::SourceLocation clang_loc = thunk_param->getLocation();
+
+  clang::Expr* call_arg = sema.BuildDeclRefExpr(
+      thunk_param, thunk_param->getType().getNonReferenceType(),
+      clang::VK_LValue, clang_loc);
+  if (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
+    // depending on whether we're moving from each parameter.
+    clang::ExprResult deref_result =
+        sema.BuildUnaryOp(nullptr, clang_loc, clang::UO_Deref, call_arg);
+    CARBON_CHECK(deref_result.isUsable());
+    call_arg = deref_result.get();
+  }
+  return call_arg;
+}
+
+// Builds a reference to the parameter thunk parameter corresponding to the
+// given callee parameter index.
+static auto BuildParamRefForCalleeArg(clang::Sema& sema,
+                                      clang::FunctionDecl* thunk_function_decl,
+                                      CalleeFunctionInfo callee_info,
+                                      unsigned callee_index) -> clang::Expr* {
+  unsigned thunk_index = callee_info.GetThunkParamIndex(callee_index);
+  return BuildThunkParamRef(
+      sema, thunk_function_decl, thunk_index,
+      callee_info.decl->getParamDecl(callee_index)->getType());
+}
+
+// Builds an argument list for the callee function by creating suitable uses of
+// the corresponding thunk parameters.
 static auto BuildCalleeArgs(clang::Sema& sema,
                             clang::FunctionDecl* thunk_function_decl,
-                            const clang::FunctionDecl& callee_function_decl)
+                            CalleeFunctionInfo callee_info)
     -> llvm::SmallVector<clang::Expr*> {
   llvm::SmallVector<clang::Expr*> call_args;
-  size_t num_params = thunk_function_decl->getNumParams();
-  CARBON_CHECK(callee_function_decl.getNumParams() == num_params);
-  call_args.reserve(num_params);
-  for (unsigned i = 0; i < num_params; ++i) {
-    clang::ParmVarDecl* thunk_param = thunk_function_decl->getParamDecl(i);
-    clang::SourceLocation clang_loc = thunk_param->getLocation();
-
-    clang::Expr* call_arg = sema.BuildDeclRefExpr(
-        thunk_param, thunk_param->getType(), clang::VK_LValue, clang_loc);
-    if (thunk_param->getType() !=
-        callee_function_decl.getParamDecl(i)->getType()) {
-      // 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
-      // depending on whether we're moving from each parameter.
-      clang::ExprResult deref_result =
-          sema.BuildUnaryOp(nullptr, clang_loc, clang::UO_Deref, call_arg);
-      CARBON_CHECK(deref_result.isUsable());
-      call_arg = deref_result.get();
-    }
-    call_args.push_back(call_arg);
+  // The object parameter is always passed as `self`, not in the callee argument
+  // list, so the first argument corresponds to the second parameter if there is
+  // an explicit object parameter and the first parameter otherwise.
+  unsigned first_param = callee_info.has_explicit_object_parameter();
+  unsigned num_params = callee_info.decl->getNumParams();
+  call_args.reserve(num_params - first_param);
+  for (unsigned callee_index : llvm::seq(first_param, num_params)) {
+    call_args.push_back(BuildParamRefForCalleeArg(sema, thunk_function_decl,
+                                                  callee_info, callee_index));
   }
-
   return call_args;
 }
 
@@ -250,25 +326,56 @@ static auto BuildCalleeArgs(clang::Sema& sema,
 // args and returns the callee function return value. Returns nullptr on
 // failure.
 static auto BuildThunkBody(clang::Sema& sema,
-                           clang::FunctionDecl* callee_function_decl,
-                           llvm::MutableArrayRef<clang::Expr*> call_args)
-    -> clang::Stmt* {
-  clang::SourceLocation clang_loc = callee_function_decl->getLocation();
-
-  clang::DeclRefExpr* callee_function_ref = sema.BuildDeclRefExpr(
-      callee_function_decl, callee_function_decl->getType(), clang::VK_PRValue,
-      clang_loc);
-
-  clang::ExprResult call_result = sema.BuildCallExpr(
-      nullptr, callee_function_ref, clang_loc, call_args, clang_loc);
-  if (!call_result.isUsable()) {
-    return nullptr;
+                           clang::FunctionDecl* thunk_function_decl,
+                           CalleeFunctionInfo callee_info)
+    -> clang::StmtResult {
+  clang::SourceLocation clang_loc = callee_info.decl->getLocation();
+
+  // If the callee has an object parameter, build a member access expression as
+  // the callee. Otherwise, build a regular reference to the function.
+  clang::ExprResult callee;
+  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());
+    bool is_arrow = callee_info.has_implicit_object_parameter();
+    auto object =
+        sema.PerformMemberExprBaseConversion(object_param_ref, is_arrow);
+    if (object.isInvalid()) {
+      return clang::StmtError();
+    }
+    callee = sema.BuildMemberExpr(
+        object.get(), is_arrow, clang_loc, clang::NestedNameSpecifierLoc(),
+        clang::SourceLocation(), callee_info.decl,
+        clang::DeclAccessPair::make(callee_info.decl, clang::AS_public),
+        /*HadMultipleCandidates=*/false, clang::DeclarationNameInfo(),
+        sema.getASTContext().BoundMemberTy, clang::VK_PRValue,
+        clang::OK_Ordinary);
+  } else {
+    callee =
+        sema.BuildDeclRefExpr(callee_info.decl, callee_info.decl->getType(),
+                              clang::VK_PRValue, clang_loc);
   }
-  clang::Expr* call = call_result.get();
 
-  clang::StmtResult return_result = sema.BuildReturnStmt(clang_loc, call);
-  CARBON_CHECK(return_result.isUsable());
-  return return_result.get();
+  if (callee.isInvalid()) {
+    return clang::StmtError();
+  }
+
+  // Build the argument list.
+  llvm::SmallVector<clang::Expr*> call_args =
+      BuildCalleeArgs(sema, thunk_function_decl, callee_info);
+
+  clang::ExprResult call = sema.BuildCallExpr(nullptr, callee.get(), clang_loc,
+                                              call_args, clang_loc);
+  if (!call.isUsable()) {
+    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());
 }
 
 auto BuildCppThunk(Context& context, const SemIR::Function& callee_function)
@@ -280,22 +387,22 @@ auto BuildCppThunk(Context& context, const SemIR::Function& callee_function)
           .decl->getAsFunction();
   CARBON_CHECK(callee_function_decl);
 
+  CalleeFunctionInfo callee_info(callee_function_decl);
+
   // Build the thunk function declaration.
   auto thunk_param_types =
-      BuildThunkParameterTypes(context.ast_context(), *callee_function_decl);
-  clang::FunctionDecl* thunk_function_decl = CreateThunkFunctionDecl(
-      context, *callee_function_decl, thunk_param_types);
+      BuildThunkParameterTypes(context.ast_context(), callee_info);
+  clang::FunctionDecl* thunk_function_decl =
+      CreateThunkFunctionDecl(context, callee_info, thunk_param_types);
 
   // Build the thunk function body.
   clang::Sema& sema = context.sem_ir().clang_ast_unit()->getSema();
   clang::Sema::ContextRAII context_raii(sema, thunk_function_decl);
   sema.ActOnStartOfFunctionDef(nullptr, thunk_function_decl);
-
-  llvm::SmallVector<clang::Expr*> call_args =
-      BuildCalleeArgs(sema, thunk_function_decl, *callee_function_decl);
-  clang::Stmt* body = BuildThunkBody(sema, callee_function_decl, call_args);
-  sema.ActOnFinishFunctionBody(thunk_function_decl, body);
-  if (!body) {
+  clang::StmtResult body =
+      BuildThunkBody(sema, thunk_function_decl, callee_info);
+  sema.ActOnFinishFunctionBody(thunk_function_decl, body.get());
+  if (body.isInvalid()) {
     return nullptr;
   }
 
@@ -332,16 +439,16 @@ auto PerformCppThunkCall(Context& context, SemIR::LocId loc_id,
 
     SemIR::InstId arg_id = callee_arg_id;
     if (callee_param_type_id != thunk_param_type_id) {
-      CARBON_CHECK(thunk_param_type_id ==
-                   GetPointerType(context, context.types().GetInstId(
-                                               callee_param_type_id)));
-
       arg_id = Convert(context, loc_id, arg_id,
                        {.kind = ConversionTarget::CppThunkRef,
                         .type_id = callee_param_type_id});
       arg_id = AddInst<SemIR::AddrOf>(
           context, loc_id,
-          {.type_id = thunk_param_type_id, .lvalue_id = arg_id});
+          {.type_id = GetPointerType(
+               context, context.types().GetInstId(callee_param_type_id)),
+           .lvalue_id = arg_id});
+      arg_id =
+          ConvertToValueOfType(context, loc_id, arg_id, thunk_param_type_id);
     }
     thunk_arg_ids.push_back(arg_id);
   }

+ 3 - 2
toolchain/check/import_cpp.cpp

@@ -1277,8 +1277,9 @@ static auto MakeParamPatternsBlockId(Context& context, SemIR::LocId loc_id,
     return SemIR::InstBlockId::Empty;
   }
   llvm::SmallVector<SemIR::InstId> params;
-  params.reserve(clang_decl.parameters().size());
-  for (const clang::ParmVarDecl* param : clang_decl.parameters()) {
+  params.reserve(clang_decl.getNumNonObjectParams());
+  for (unsigned i : llvm::seq(clang_decl.getNumNonObjectParams())) {
+    const auto* param = clang_decl.getNonObjectParameter(i);
     // TODO: Get the parameter type from the function, not from the
     // `ParmVarDecl`. The type of the `ParmVarDecl` is the type within the
     // function, and isn't in general the same as the type that's exposed to

+ 27 - 2
toolchain/check/testdata/interop/cpp/class/base.carbon

@@ -407,8 +407,15 @@ class V {
 // CHECK:STDOUT:   %Base: type = class_type @Base [concrete]
 // CHECK:STDOUT:   %Base.f.type: type = fn_type @Base.f [concrete]
 // CHECK:STDOUT:   %Base.f: %Base.f.type = struct_value () [concrete]
+// CHECK:STDOUT:   %const: type = const_type %Base [concrete]
+// CHECK:STDOUT:   %ptr.a97: type = ptr_type %const [concrete]
+// CHECK:STDOUT:   %f__carbon_thunk.type: type = fn_type @f__carbon_thunk [concrete]
+// CHECK:STDOUT:   %f__carbon_thunk: %f__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.fb2: type = ptr_type %Base [concrete]
 // CHECK:STDOUT:   %Base.g.type: type = fn_type @Base.g [concrete]
 // CHECK:STDOUT:   %Base.g: %Base.g.type = struct_value () [concrete]
+// CHECK:STDOUT:   %g__carbon_thunk.type: type = fn_type @g__carbon_thunk [concrete]
+// CHECK:STDOUT:   %g__carbon_thunk: %g__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: imports {
@@ -424,11 +431,21 @@ class V {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %f__carbon_thunk.decl: %f__carbon_thunk.type = fn_decl @f__carbon_thunk [concrete = constants.%f__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT:   %Base.g.decl: %Base.g.type = fn_decl @Base.g [concrete = constants.%Base.g] {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %g__carbon_thunk.decl: %g__carbon_thunk.type = fn_decl @g__carbon_thunk [concrete = constants.%g__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @CallDirect(%d.param: %Derived) {
@@ -439,7 +456,11 @@ class V {
 // CHECK:STDOUT:   %.loc8_3.1: ref %Base = class_element_access %d.ref, element0
 // CHECK:STDOUT:   %.loc8_3.2: ref %Base = converted %d.ref, %.loc8_3.1
 // CHECK:STDOUT:   %.loc8_3.3: %Base = bind_value %.loc8_3.2
-// CHECK:STDOUT:   %Base.f.call: init %empty_tuple.type = call %Base.f.bound(%.loc8_3.3)
+// CHECK:STDOUT:   %.loc8_3.4: ref %Base = value_as_ref %.loc8_3.3
+// CHECK:STDOUT:   %addr: %ptr.fb2 = addr_of %.loc8_3.4
+// CHECK:STDOUT:   %.loc8_7.1: %ptr.a97 = as_compatible %addr
+// CHECK:STDOUT:   %.loc8_7.2: %ptr.a97 = converted %addr, %.loc8_7.1
+// CHECK:STDOUT:   %f__carbon_thunk.call: init %empty_tuple.type = call imports.%f__carbon_thunk.decl(%.loc8_7.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -453,7 +474,11 @@ class V {
 // CHECK:STDOUT:   %.loc14_3.1: ref %Base = class_element_access %d.ref, element0
 // CHECK:STDOUT:   %.loc14_3.2: ref %Base = converted %d.ref, %.loc14_3.1
 // CHECK:STDOUT:   %.loc14_3.3: %Base = bind_value %.loc14_3.2
-// CHECK:STDOUT:   %Base.g.call: init %empty_tuple.type = call %Base.g.bound(%.loc14_3.3)
+// CHECK:STDOUT:   %.loc14_3.4: ref %Base = value_as_ref %.loc14_3.3
+// CHECK:STDOUT:   %addr: %ptr.fb2 = addr_of %.loc14_3.4
+// CHECK:STDOUT:   %.loc14_18.1: %ptr.a97 = as_compatible %addr
+// CHECK:STDOUT:   %.loc14_18.2: %ptr.a97 = converted %addr, %.loc14_18.1
+// CHECK:STDOUT:   %g__carbon_thunk.call: init %empty_tuple.type = call imports.%g__carbon_thunk.decl(%.loc14_18.2)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 145 - 5
toolchain/check/testdata/interop/cpp/class/method.carbon

@@ -2,7 +2,8 @@
 // Exceptions. See /LICENSE for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
-// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/convert.carbon
+// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/int.carbon
+// EXTRA-ARGS: --clang-arg=-std=c++23
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
@@ -136,6 +137,31 @@ fn Ref(p: Cpp.HasQualifiers*) {
   p->const_ref_ref_this();
 }
 
+// --- explicit_object_param.h
+
+struct Another {
+};
+
+struct ExplicitObjectParam {
+  void F(this ExplicitObjectParam);
+  void G(this int);
+  void H(this Another);
+};
+
+// --- call_explicit_object_param.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp library "explicit_object_param.h";
+
+fn Call(e: Cpp.ExplicitObjectParam, n: i32, a: Cpp.Another) {
+  //@dump-sem-ir-begin
+  e.F();
+  n.(Cpp.ExplicitObjectParam.G)();
+  a.(Cpp.ExplicitObjectParam.H)();
+  //@dump-sem-ir-end
+}
+
 // CHECK:STDOUT: --- use_object_param_qualifiers.carbon
 // CHECK:STDOUT:
 // CHECK:STDOUT: constants {
@@ -144,8 +170,14 @@ fn Ref(p: Cpp.HasQualifiers*) {
 // CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
 // CHECK:STDOUT:   %HasQualifiers.const_this.type: type = fn_type @HasQualifiers.const_this [concrete]
 // CHECK:STDOUT:   %HasQualifiers.const_this: %HasQualifiers.const_this.type = struct_value () [concrete]
+// CHECK:STDOUT:   %const: type = const_type %HasQualifiers [concrete]
+// CHECK:STDOUT:   %ptr.2cb: type = ptr_type %const [concrete]
+// CHECK:STDOUT:   %const_this__carbon_thunk.type: type = fn_type @const_this__carbon_thunk [concrete]
+// CHECK:STDOUT:   %const_this__carbon_thunk: %const_this__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %HasQualifiers.const_ref_this.type: type = fn_type @HasQualifiers.const_ref_this [concrete]
 // CHECK:STDOUT:   %HasQualifiers.const_ref_this: %HasQualifiers.const_ref_this.type = struct_value () [concrete]
+// CHECK:STDOUT:   %const_ref_this__carbon_thunk.type: type = fn_type @const_ref_this__carbon_thunk [concrete]
+// CHECK:STDOUT:   %const_ref_this__carbon_thunk: %const_ref_this__carbon_thunk.type = struct_value () [concrete]
 // CHECK:STDOUT:   %HasQualifiers.plain.type: type = fn_type @HasQualifiers.plain [concrete]
 // CHECK:STDOUT:   %HasQualifiers.plain: %HasQualifiers.plain.type = struct_value () [concrete]
 // CHECK:STDOUT:   %HasQualifiers.ref_this.type: type = fn_type @HasQualifiers.ref_this [concrete]
@@ -158,11 +190,21 @@ fn Ref(p: Cpp.HasQualifiers*) {
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %const_this__carbon_thunk.decl: %const_this__carbon_thunk.type = fn_decl @const_this__carbon_thunk [concrete = constants.%const_this__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT:   %HasQualifiers.const_ref_this.decl: %HasQualifiers.const_ref_this.type = fn_decl @HasQualifiers.const_ref_this [concrete = constants.%HasQualifiers.const_ref_this] {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %const_ref_this__carbon_thunk.decl: %const_ref_this__carbon_thunk.type = fn_decl @const_ref_this__carbon_thunk [concrete = constants.%const_ref_this__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT:   %HasQualifiers.plain.decl: %HasQualifiers.plain.type = fn_decl @HasQualifiers.plain [concrete = constants.%HasQualifiers.plain] {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   } {
@@ -180,11 +222,19 @@ fn Ref(p: Cpp.HasQualifiers*) {
 // CHECK:STDOUT:   %v.ref.loc8: %HasQualifiers = name_ref v, %v
 // CHECK:STDOUT:   %const_this.ref.loc8: %HasQualifiers.const_this.type = name_ref const_this, imports.%HasQualifiers.const_this.decl [concrete = constants.%HasQualifiers.const_this]
 // CHECK:STDOUT:   %HasQualifiers.const_this.bound.loc8: <bound method> = bound_method %v.ref.loc8, %const_this.ref.loc8
-// CHECK:STDOUT:   %HasQualifiers.const_this.call.loc8: init %empty_tuple.type = call %HasQualifiers.const_this.bound.loc8(%v.ref.loc8)
+// CHECK:STDOUT:   %.loc8_3: ref %HasQualifiers = value_as_ref %v.ref.loc8
+// CHECK:STDOUT:   %addr.loc8: %ptr.ec3 = addr_of %.loc8_3
+// CHECK:STDOUT:   %.loc8_16.1: %ptr.2cb = as_compatible %addr.loc8
+// CHECK:STDOUT:   %.loc8_16.2: %ptr.2cb = converted %addr.loc8, %.loc8_16.1
+// CHECK:STDOUT:   %const_this__carbon_thunk.call.loc8: init %empty_tuple.type = call imports.%const_this__carbon_thunk.decl(%.loc8_16.2)
 // CHECK:STDOUT:   %v.ref.loc9: %HasQualifiers = name_ref v, %v
 // CHECK:STDOUT:   %const_ref_this.ref.loc9: %HasQualifiers.const_ref_this.type = name_ref const_ref_this, imports.%HasQualifiers.const_ref_this.decl [concrete = constants.%HasQualifiers.const_ref_this]
 // CHECK:STDOUT:   %HasQualifiers.const_ref_this.bound.loc9: <bound method> = bound_method %v.ref.loc9, %const_ref_this.ref.loc9
-// CHECK:STDOUT:   %HasQualifiers.const_ref_this.call.loc9: init %empty_tuple.type = call %HasQualifiers.const_ref_this.bound.loc9(%v.ref.loc9)
+// CHECK:STDOUT:   %.loc9_3: ref %HasQualifiers = value_as_ref %v.ref.loc9
+// CHECK:STDOUT:   %addr.loc9: %ptr.ec3 = addr_of %.loc9_3
+// CHECK:STDOUT:   %.loc9_20.1: %ptr.2cb = as_compatible %addr.loc9
+// CHECK:STDOUT:   %.loc9_20.2: %ptr.2cb = converted %addr.loc9, %.loc9_20.1
+// CHECK:STDOUT:   %const_ref_this__carbon_thunk.call.loc9: init %empty_tuple.type = call imports.%const_ref_this__carbon_thunk.decl(%.loc9_20.2)
 // CHECK:STDOUT:   %p.ref.loc11: %ptr.ec3 = name_ref p, %p
 // CHECK:STDOUT:   %.loc11: ref %HasQualifiers = deref %p.ref.loc11
 // CHECK:STDOUT:   %plain.ref: %HasQualifiers.plain.type = name_ref plain, imports.%HasQualifiers.plain.decl [concrete = constants.%HasQualifiers.plain]
@@ -202,13 +252,103 @@ fn Ref(p: Cpp.HasQualifiers*) {
 // CHECK:STDOUT:   %const_this.ref.loc13: %HasQualifiers.const_this.type = name_ref const_this, imports.%HasQualifiers.const_this.decl [concrete = constants.%HasQualifiers.const_this]
 // CHECK:STDOUT:   %HasQualifiers.const_this.bound.loc13: <bound method> = bound_method %.loc13_4.1, %const_this.ref.loc13
 // CHECK:STDOUT:   %.loc13_4.2: %HasQualifiers = bind_value %.loc13_4.1
-// CHECK:STDOUT:   %HasQualifiers.const_this.call.loc13: init %empty_tuple.type = call %HasQualifiers.const_this.bound.loc13(%.loc13_4.2)
+// CHECK:STDOUT:   %.loc13_4.3: ref %HasQualifiers = value_as_ref %.loc13_4.2
+// CHECK:STDOUT:   %addr.loc13: %ptr.ec3 = addr_of %.loc13_4.3
+// CHECK:STDOUT:   %.loc13_17.1: %ptr.2cb = as_compatible %addr.loc13
+// CHECK:STDOUT:   %.loc13_17.2: %ptr.2cb = converted %addr.loc13, %.loc13_17.1
+// CHECK:STDOUT:   %const_this__carbon_thunk.call.loc13: init %empty_tuple.type = call imports.%const_this__carbon_thunk.decl(%.loc13_17.2)
 // CHECK:STDOUT:   %p.ref.loc14: %ptr.ec3 = name_ref p, %p
 // CHECK:STDOUT:   %.loc14_4.1: ref %HasQualifiers = deref %p.ref.loc14
 // CHECK:STDOUT:   %const_ref_this.ref.loc14: %HasQualifiers.const_ref_this.type = name_ref const_ref_this, imports.%HasQualifiers.const_ref_this.decl [concrete = constants.%HasQualifiers.const_ref_this]
 // CHECK:STDOUT:   %HasQualifiers.const_ref_this.bound.loc14: <bound method> = bound_method %.loc14_4.1, %const_ref_this.ref.loc14
 // CHECK:STDOUT:   %.loc14_4.2: %HasQualifiers = bind_value %.loc14_4.1
-// CHECK:STDOUT:   %HasQualifiers.const_ref_this.call.loc14: init %empty_tuple.type = call %HasQualifiers.const_ref_this.bound.loc14(%.loc14_4.2)
+// CHECK:STDOUT:   %.loc14_4.3: ref %HasQualifiers = value_as_ref %.loc14_4.2
+// CHECK:STDOUT:   %addr.loc14: %ptr.ec3 = addr_of %.loc14_4.3
+// CHECK:STDOUT:   %.loc14_21.1: %ptr.2cb = as_compatible %addr.loc14
+// CHECK:STDOUT:   %.loc14_21.2: %ptr.2cb = converted %addr.loc14, %.loc14_21.1
+// CHECK:STDOUT:   %const_ref_this__carbon_thunk.call.loc14: init %empty_tuple.type = call imports.%const_ref_this__carbon_thunk.decl(%.loc14_21.2)
+// CHECK:STDOUT:   <elided>
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: --- call_explicit_object_param.carbon
+// CHECK:STDOUT:
+// CHECK:STDOUT: constants {
+// CHECK:STDOUT:   %ExplicitObjectParam: type = class_type @ExplicitObjectParam [concrete]
+// CHECK:STDOUT:   %int_32: Core.IntLiteral = int_value 32 [concrete]
+// CHECK:STDOUT:   %empty_tuple.type: type = tuple_type () [concrete]
+// CHECK:STDOUT:   %i32: type = class_type @Int, @Int(%int_32) [concrete]
+// CHECK:STDOUT:   %Another: type = class_type @Another [concrete]
+// CHECK:STDOUT:   %ExplicitObjectParam.F.type: type = fn_type @ExplicitObjectParam.F [concrete]
+// CHECK:STDOUT:   %ExplicitObjectParam.F: %ExplicitObjectParam.F.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.7f5: type = ptr_type %ExplicitObjectParam [concrete]
+// CHECK:STDOUT:   %F__carbon_thunk.type: type = fn_type @F__carbon_thunk [concrete]
+// CHECK:STDOUT:   %F__carbon_thunk: %F__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ExplicitObjectParam.G.type: type = fn_type @ExplicitObjectParam.G [concrete]
+// CHECK:STDOUT:   %ExplicitObjectParam.G: %ExplicitObjectParam.G.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ExplicitObjectParam.H.type: type = fn_type @ExplicitObjectParam.H [concrete]
+// CHECK:STDOUT:   %ExplicitObjectParam.H: %ExplicitObjectParam.H.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.289: type = ptr_type %Another [concrete]
+// CHECK:STDOUT:   %H__carbon_thunk.type: type = fn_type @H__carbon_thunk [concrete]
+// CHECK:STDOUT:   %H__carbon_thunk: %H__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: imports {
+// CHECK:STDOUT:   %Cpp: <namespace> = namespace file.%Cpp.import_cpp, [concrete] {
+// CHECK:STDOUT:     .ExplicitObjectParam = %ExplicitObjectParam.decl
+// CHECK:STDOUT:     .Another = %Another.decl
+// CHECK:STDOUT:     import Cpp//...
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %ExplicitObjectParam.decl: type = class_decl @ExplicitObjectParam [concrete = constants.%ExplicitObjectParam] {} {}
+// CHECK:STDOUT:   %Another.decl: type = class_decl @Another [concrete = constants.%Another] {} {}
+// CHECK:STDOUT:   %ExplicitObjectParam.F.decl: %ExplicitObjectParam.F.type = fn_decl @ExplicitObjectParam.F [concrete = constants.%ExplicitObjectParam.F] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %F__carbon_thunk.decl: %F__carbon_thunk.type = fn_decl @F__carbon_thunk [concrete = constants.%F__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %ExplicitObjectParam.G.decl: %ExplicitObjectParam.G.type = fn_decl @ExplicitObjectParam.G [concrete = constants.%ExplicitObjectParam.G] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %ExplicitObjectParam.H.decl: %ExplicitObjectParam.H.type = fn_decl @ExplicitObjectParam.H [concrete = constants.%ExplicitObjectParam.H] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT:   %H__carbon_thunk.decl: %H__carbon_thunk.type = fn_decl @H__carbon_thunk [concrete = constants.%H__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: fn @Call(%e.param: %ExplicitObjectParam, %n.param: %i32, %a.param: %Another) {
+// CHECK:STDOUT: !entry:
+// CHECK:STDOUT:   %e.ref: %ExplicitObjectParam = name_ref e, %e
+// CHECK:STDOUT:   %F.ref: %ExplicitObjectParam.F.type = name_ref F, imports.%ExplicitObjectParam.F.decl [concrete = constants.%ExplicitObjectParam.F]
+// CHECK:STDOUT:   %ExplicitObjectParam.F.bound: <bound method> = bound_method %e.ref, %F.ref
+// CHECK:STDOUT:   %.loc8: ref %ExplicitObjectParam = value_as_ref %e.ref
+// CHECK:STDOUT:   %addr.loc8: %ptr.7f5 = addr_of %.loc8
+// CHECK:STDOUT:   %F__carbon_thunk.call: init %empty_tuple.type = call imports.%F__carbon_thunk.decl(%addr.loc8)
+// CHECK:STDOUT:   %n.ref: %i32 = name_ref n, %n
+// CHECK:STDOUT:   %Cpp.ref.loc9: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
+// CHECK:STDOUT:   %ExplicitObjectParam.ref.loc9: type = name_ref ExplicitObjectParam, imports.%ExplicitObjectParam.decl [concrete = constants.%ExplicitObjectParam]
+// CHECK:STDOUT:   %G.ref: %ExplicitObjectParam.G.type = name_ref G, imports.%ExplicitObjectParam.G.decl [concrete = constants.%ExplicitObjectParam.G]
+// CHECK:STDOUT:   %ExplicitObjectParam.G.bound: <bound method> = bound_method %n.ref, %G.ref
+// CHECK:STDOUT:   %ExplicitObjectParam.G.call: init %empty_tuple.type = call %ExplicitObjectParam.G.bound(%n.ref)
+// CHECK:STDOUT:   %a.ref: %Another = name_ref a, %a
+// CHECK:STDOUT:   %Cpp.ref.loc10: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
+// CHECK:STDOUT:   %ExplicitObjectParam.ref.loc10: type = name_ref ExplicitObjectParam, imports.%ExplicitObjectParam.decl [concrete = constants.%ExplicitObjectParam]
+// CHECK:STDOUT:   %H.ref: %ExplicitObjectParam.H.type = name_ref H, imports.%ExplicitObjectParam.H.decl [concrete = constants.%ExplicitObjectParam.H]
+// CHECK:STDOUT:   %ExplicitObjectParam.H.bound: <bound method> = bound_method %a.ref, %H.ref
+// CHECK:STDOUT:   %.loc10: ref %Another = value_as_ref %a.ref
+// CHECK:STDOUT:   %addr.loc10: %ptr.289 = addr_of %.loc10
+// CHECK:STDOUT:   %H__carbon_thunk.call: init %empty_tuple.type = call imports.%H__carbon_thunk.decl(%addr.loc10)
 // CHECK:STDOUT:   <elided>
 // CHECK:STDOUT: }
 // CHECK:STDOUT:

+ 18 - 4
toolchain/check/testdata/interop/cpp/class/template.carbon

@@ -91,8 +91,13 @@ var y: Cpp.Xint.r#type = 0;
 // CHECK:STDOUT:   %A.elem.c3f: type = unbound_element_type %A.0bedf0.1, %i32 [concrete]
 // CHECK:STDOUT:   %A.f.type: type = fn_type @A.f [concrete]
 // CHECK:STDOUT:   %A.f: %A.f.type = struct_value () [concrete]
+// CHECK:STDOUT:   %const: type = const_type %A.0bedf0.1 [concrete]
+// CHECK:STDOUT:   %ptr.703: type = ptr_type %const [concrete]
+// CHECK:STDOUT:   %f__carbon_thunk.type: type = fn_type @f__carbon_thunk [concrete]
+// CHECK:STDOUT:   %f__carbon_thunk: %f__carbon_thunk.type = struct_value () [concrete]
+// CHECK:STDOUT:   %ptr.270828.1: type = ptr_type %A.0bedf0.1 [concrete]
 // CHECK:STDOUT:   %A.0bedf0.2: type = class_type @A.2 [concrete]
-// CHECK:STDOUT:   %ptr.270: type = ptr_type %A.0bedf0.2 [concrete]
+// CHECK:STDOUT:   %ptr.270828.2: type = ptr_type %A.0bedf0.2 [concrete]
 // CHECK:STDOUT:   %ptr.fb2: type = ptr_type %Base [concrete]
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
@@ -102,6 +107,11 @@ var y: Cpp.Xint.r#type = 0;
 // CHECK:STDOUT:   } {
 // CHECK:STDOUT:     <elided>
 // CHECK:STDOUT:   }
+// CHECK:STDOUT:   %f__carbon_thunk.decl: %f__carbon_thunk.type = fn_decl @f__carbon_thunk [concrete = constants.%f__carbon_thunk] {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   } {
+// CHECK:STDOUT:     <elided>
+// CHECK:STDOUT:   }
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
 // CHECK:STDOUT: fn @F(%x.param: %A.0bedf0.1) -> %i32 {
@@ -109,7 +119,11 @@ var y: Cpp.Xint.r#type = 0;
 // CHECK:STDOUT:   %x.ref.loc9: %A.0bedf0.1 = name_ref x, %x
 // CHECK:STDOUT:   %f.ref: %A.f.type = name_ref f, imports.%A.f.decl [concrete = constants.%A.f]
 // CHECK:STDOUT:   %A.f.bound: <bound method> = bound_method %x.ref.loc9, %f.ref
-// CHECK:STDOUT:   %A.f.call: init %empty_tuple.type = call %A.f.bound(%x.ref.loc9)
+// CHECK:STDOUT:   %.loc9_3: ref %A.0bedf0.1 = value_as_ref %x.ref.loc9
+// CHECK:STDOUT:   %addr: %ptr.270828.1 = addr_of %.loc9_3
+// CHECK:STDOUT:   %.loc9_7.1: %ptr.703 = as_compatible %addr
+// CHECK:STDOUT:   %.loc9_7.2: %ptr.703 = converted %addr, %.loc9_7.1
+// CHECK:STDOUT:   %f__carbon_thunk.call: init %empty_tuple.type = call imports.%f__carbon_thunk.decl(%.loc9_7.2)
 // CHECK:STDOUT:   %x.ref.loc10: %A.0bedf0.1 = name_ref x, %x
 // CHECK:STDOUT:   %n.ref: %A.elem.c3f = name_ref n, @A.1.%.2 [concrete = @A.1.%.2]
 // CHECK:STDOUT:   %.loc10_11.1: ref %i32 = class_element_access %x.ref.loc10, element1
@@ -117,9 +131,9 @@ var y: Cpp.Xint.r#type = 0;
 // CHECK:STDOUT:   return %.loc10_11.2
 // CHECK:STDOUT: }
 // CHECK:STDOUT:
-// CHECK:STDOUT: fn @G(%p.param: %ptr.270) -> %ptr.fb2 {
+// CHECK:STDOUT: fn @G(%p.param: %ptr.270828.2) -> %ptr.fb2 {
 // CHECK:STDOUT: !entry:
-// CHECK:STDOUT:   %p.ref: %ptr.270 = name_ref p, %p
+// CHECK:STDOUT:   %p.ref: %ptr.270828.2 = name_ref p, %p
 // CHECK:STDOUT:   %.loc17_11.1: ref %A.0bedf0.2 = deref %p.ref
 // CHECK:STDOUT:   %.loc17_11.2: ref %Base = class_element_access %.loc17_11.1, element0
 // CHECK:STDOUT:   %addr: %ptr.fb2 = addr_of %.loc17_11.2

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

@@ -18,7 +18,7 @@
 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 'auto (short * _Nonnull) -> void' extern
+// CHECK:STDOUT: `-FunctionDecl {{0x[a-f0-9]+}} <col: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'

+ 99 - 0
toolchain/lower/testdata/interop/cpp/method.carbon

@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 // INCLUDE-FILE: toolchain/testing/testdata/min_prelude/int.carbon
+// EXTRA-ARGS: --clang-arg=-std=c++26
 //
 // AUTOUPDATE
 // TIP: To test this file alone, run:
@@ -38,6 +39,24 @@ fn UseVal(a: Cpp.A*) -> i32 {
   return a->by_ref();
 }
 
+// --- thunk.h
+
+struct NeedThunk {
+  void Implicit(signed char c) const;
+  void Explicit(this NeedThunk, signed char c);
+};
+
+// --- call_thunk.carbon
+
+library "[[@TEST_NAME]]";
+
+import Cpp library "thunk.h";
+
+fn Call(n: Cpp.NeedThunk) {
+  n.Implicit(1);
+  n.Explicit(1);
+}
+
 // CHECK:STDOUT: ; ModuleID = 'call_by_val.carbon'
 // CHECK:STDOUT: source_filename = "call_by_val.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"
@@ -124,3 +143,83 @@ fn UseVal(a: Cpp.A*) -> i32 {
 // CHECK:STDOUT: !9 = !{}
 // CHECK:STDOUT: !10 = !DILocation(line: 7, column: 10, scope: !7)
 // CHECK:STDOUT: !11 = !DILocation(line: 7, column: 3, scope: !7)
+// CHECK:STDOUT: ; ModuleID = 'call_thunk.carbon'
+// CHECK:STDOUT: source_filename = "call_thunk.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.NeedThunk = type { i8 }
+// CHECK:STDOUT:
+// CHECK:STDOUT: define void @_CCall.Main(ptr %n) !dbg !7 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.loc7_14.3.temp = alloca i8, align 1, !dbg !10
+// CHECK:STDOUT:   %.loc8_14.3.temp = alloca i8, align 1, !dbg !11
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc7_14.3.temp), !dbg !10
+// CHECK:STDOUT:   store i8 1, ptr %.loc7_14.3.temp, align 1, !dbg !10
+// CHECK:STDOUT:   call void @_ZNK9NeedThunk8ImplicitEa.carbon_thunk(ptr %n, ptr %.loc7_14.3.temp), !dbg !12
+// CHECK:STDOUT:   call void @llvm.lifetime.start.p0(ptr %.loc8_14.3.temp), !dbg !11
+// CHECK:STDOUT:   store i8 1, ptr %.loc8_14.3.temp, align 1, !dbg !11
+// CHECK:STDOUT:   call void @_ZNH9NeedThunk8ExplicitES_a.carbon_thunk(ptr %n, ptr %.loc8_14.3.temp), !dbg !13
+// CHECK:STDOUT:   ret void, !dbg !14
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_ZNK9NeedThunk8ImplicitEa(ptr, i8)
+// CHECK:STDOUT:
+// CHECK:STDOUT: declare void @_ZNH9NeedThunk8ExplicitES_a(ptr, i8)
+// 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 @_ZNK9NeedThunk8ImplicitEa.carbon_thunk(ptr %this, ptr %c) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %this.addr = alloca ptr, align 8
+// CHECK:STDOUT:   %c.addr = alloca ptr, align 8
+// CHECK:STDOUT:   store ptr %this, ptr %this.addr, align 8
+// CHECK:STDOUT:   store ptr %c, ptr %c.addr, align 8
+// CHECK:STDOUT:   %0 = load ptr, ptr %this.addr, align 8
+// CHECK:STDOUT:   %1 = load ptr, ptr %c.addr, align 8
+// CHECK:STDOUT:   %2 = load i8, ptr %1, align 1
+// CHECK:STDOUT:   call void @_ZNK9NeedThunk8ImplicitEa(ptr nonnull align 1 dereferenceable(1) %0, i8 signext %2)
+// CHECK:STDOUT:   ret void
+// CHECK:STDOUT: }
+// CHECK:STDOUT:
+// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress
+// CHECK:STDOUT: define dso_local void @_ZNH9NeedThunk8ExplicitES_a.carbon_thunk(ptr %0, ptr %c) #1 {
+// CHECK:STDOUT: entry:
+// CHECK:STDOUT:   %.addr = alloca ptr, align 8
+// CHECK:STDOUT:   %c.addr = alloca ptr, align 8
+// CHECK:STDOUT:   %agg.tmp = alloca %struct.NeedThunk, align 1
+// CHECK:STDOUT:   store ptr %0, ptr %.addr, align 8
+// CHECK:STDOUT:   store ptr %c, ptr %c.addr, align 8
+// CHECK:STDOUT:   %1 = load ptr, ptr %.addr, align 8
+// CHECK:STDOUT:   %2 = load ptr, ptr %c.addr, align 8
+// CHECK:STDOUT:   %3 = load i8, ptr %2, align 1
+// CHECK:STDOUT:   call void @_ZNH9NeedThunk8ExplicitES_a(i8 signext %3)
+// 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: "call_thunk.carbon", directory: "")
+// CHECK:STDOUT: !7 = distinct !DISubprogram(name: "Call", linkageName: "_CCall.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: 7, column: 14, scope: !7)
+// CHECK:STDOUT: !11 = !DILocation(line: 8, column: 14, scope: !7)
+// CHECK:STDOUT: !12 = !DILocation(line: 7, column: 3, scope: !7)
+// CHECK:STDOUT: !13 = !DILocation(line: 8, column: 3, scope: !7)
+// CHECK:STDOUT: !14 = !DILocation(line: 6, column: 1, scope: !7)