Export methods taking self as const member functions (#7577)

A method declared with `self` does not modify the object, but it was
exported to C++ as a non-const member function, so calling it on a const
reference would fail.

```carbon
class C {
  fn Get(self);
}

inline Cpp '''
void F(const Carbon::C& c) {
  c.Get();
}
''';
```
```
error: 'this' argument to member function 'Get' has type 'const Carbon::C', but function is not marked const
```

Import already maps `f() const` to `fn f(self)`, and this PR implements
the same behavior for exporting. No ref-qualifier is added, since that
maps to `ref self`, so that is unchanged.

`GetThisArg()` now builds `this` from the method instead of the parent
record, so that it picks up the method's const-qualifier.
This commit is contained in:
arhwx
2026-07-29 17:28:54 +00:00
committed by GitHub
parent f51c075f8b
commit 6ea2087f0a
4 changed files with 58 additions and 16 deletions
+17 -10
View File
@@ -716,8 +716,15 @@ static auto BuildCppToCarbonThunkFunctionType(Context& context,
} }
auto ext_proto_info = clang::FunctionProtoType::ExtProtoInfo(); auto ext_proto_info = clang::FunctionProtoType::ExtProtoInfo();
if (target.self_param && target.self_param->kind == ParamPatternKind::Ref) { if (target.self_param) {
ext_proto_info.RefQualifier = clang::RQ_LValue; if (target.self_param->kind == ParamPatternKind::Ref) {
ext_proto_info.RefQualifier = clang::RQ_LValue;
} else {
// A method with `self` doesn't modify the object, so export it as
// `const`. Unlike `ref self`, `self` doesn't require a reference
// expression, so no ref-qualifier is added.
ext_proto_info.TypeQuals.addConst();
}
} }
return context.ast_context() return context.ast_context()
.getFunctionType(cpp_return_type, thunk_param_types, ext_proto_info) .getFunctionType(cpp_return_type, thunk_param_types, ext_proto_info)
@@ -821,11 +828,11 @@ static auto BuildCppToCarbonThunkDecl(Context& context, SemIR::LocId loc_id,
// Get an expr for accessing `this` in a method. // Get an expr for accessing `this` in a method.
static auto GetThisArg(clang::Sema& sema, clang::SourceLocation clang_loc, static auto GetThisArg(clang::Sema& sema, clang::SourceLocation clang_loc,
clang::CXXRecordDecl* record_decl) -> clang::Expr* { const clang::CXXMethodDecl* method_decl)
clang::QualType class_type = -> clang::Expr* {
sema.getASTContext().getCanonicalTagType(record_decl); // These pick up the method's `const` qualifier, if any.
auto class_ptr_type = sema.getASTContext().getPointerType(class_type); clang::QualType class_type = method_decl->getFunctionObjectParameterType();
auto* this_expr = sema.BuildCXXThisExpr(clang_loc, class_ptr_type, auto* this_expr = sema.BuildCXXThisExpr(clang_loc, method_decl->getThisType(),
/*IsImplicit=*/true); /*IsImplicit=*/true);
return clang::UnaryOperator::Create( return clang::UnaryOperator::Create(
sema.getASTContext(), this_expr, clang::UO_Deref, class_type, sema.getASTContext(), this_expr, clang::UO_Deref, class_type,
@@ -874,8 +881,8 @@ static auto BuildCppToCarbonThunkBody(Context& context,
llvm::SmallVector<clang::Expr*> call_args; llvm::SmallVector<clang::Expr*> call_args;
// For methods, pass the `this` pointer as the first argument to the callee. // For methods, pass the `this` pointer as the first argument to the callee.
if (target.self_param) { if (target.self_param) {
auto* parent_class = cast<clang::CXXRecordDecl>(target.decl_context); call_args.push_back(
call_args.push_back(GetThisArg(sema, clang_loc, parent_class)); GetThisArg(sema, clang_loc, cast<clang::CXXMethodDecl>(function_decl)));
} }
for (auto* param : function_decl->parameters()) { for (auto* param : function_decl->parameters()) {
clang::Expr* call_arg = clang::Expr* call_arg =
@@ -1354,7 +1361,7 @@ auto ExportDestructorToCpp(Context& context, const SemIR::Class& class_info,
sema.BuildDeclRefExpr(cpp_function_decl, cpp_function_decl->getType(), sema.BuildDeclRefExpr(cpp_function_decl, cpp_function_decl->getType(),
clang::VK_PRValue, clang_loc); clang::VK_PRValue, clang_loc);
llvm::SmallVector<clang::Expr*> call_args; llvm::SmallVector<clang::Expr*> call_args;
call_args.push_back(GetThisArg(sema, clang_loc, record_decl)); call_args.push_back(GetThisArg(sema, clang_loc, cpp_destructor_decl));
clang::ExprResult call = sema.BuildCallExpr(nullptr, callee.get(), clang_loc, clang::ExprResult call = sema.BuildCallExpr(nullptr, callee.get(), clang_loc,
call_args, clang_loc); call_args, clang_loc);
@@ -53,6 +53,20 @@ void F() {
} }
'''; ''';
// --- method_const_lvalue.carbon
library "[[@TEST_NAME]]";
import Cpp;
class C {
fn M(self);
}
inline Cpp '''
void F(const Carbon::C& c) {
c.M();
}
''';
// --- ref_method_lvalue.carbon // --- ref_method_lvalue.carbon
library "[[@TEST_NAME]]"; library "[[@TEST_NAME]]";
import Cpp; import Cpp;
@@ -89,6 +103,27 @@ void F() {
} }
'''; ''';
// --- fail_ref_method_const_lvalue.carbon
library "[[@TEST_NAME]]";
import Cpp;
class C {
fn M(ref self);
}
inline Cpp '''
void F(const Carbon::C& c) {
// CHECK:STDERR: fail_ref_method_const_lvalue.carbon:[[@LINE+7]]:3: error: 'this' argument to member function 'M' has type 'const Carbon::C', but function is not marked const [CppInteropParseError]
// CHECK:STDERR: 17 | c.M();
// CHECK:STDERR: | ^
// CHECK:STDERR: fail_ref_method_const_lvalue.carbon:[[@LINE-8]]:17: note: 'M' declared here [CppInteropParseNote]
// CHECK:STDERR: 5 | fn M(ref self);
// CHECK:STDERR: | ^
// CHECK:STDERR:
c.M();
}
''';
// --- method_alias.carbon // --- method_alias.carbon
library "[[@TEST_NAME]]"; library "[[@TEST_NAME]]";
import Cpp; import Cpp;
@@ -78,7 +78,7 @@ fn CallCallF() { Cpp.CallF(); }
// CHECK:STDOUT: entry: // CHECK:STDOUT: entry:
// CHECK:STDOUT: %ref.tmp = alloca %"class.Carbon::A", align 1 // CHECK:STDOUT: %ref.tmp = alloca %"class.Carbon::A", align 1
// CHECK:STDOUT: call void @llvm.lifetime.start.p0(ptr %ref.tmp) #4 // CHECK:STDOUT: call void @llvm.lifetime.start.p0(ptr %ref.tmp) #4
// CHECK:STDOUT: call void @_ZN6Carbon1A1FEv(ptr noundef nonnull align 1 %ref.tmp) // CHECK:STDOUT: call void @_ZNK6Carbon1A1FEv(ptr noundef nonnull align 1 %ref.tmp)
// CHECK:STDOUT: call void @_ZN6Carbon1AD2Ev(ptr noundef nonnull align 1 %ref.tmp) #4 // CHECK:STDOUT: call void @_ZN6Carbon1AD2Ev(ptr noundef nonnull align 1 %ref.tmp) #4
// CHECK:STDOUT: call void @llvm.lifetime.end.p0(ptr %ref.tmp) #4 // CHECK:STDOUT: call void @llvm.lifetime.end.p0(ptr %ref.tmp) #4
// CHECK:STDOUT: ret void // CHECK:STDOUT: ret void
@@ -88,7 +88,7 @@ fn CallCallF() { Cpp.CallF(); }
// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1 // CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
// CHECK:STDOUT: // CHECK:STDOUT:
// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable // CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable
// CHECK:STDOUT: define internal void @_ZN6Carbon1A1FEv(ptr noundef nonnull align 1 %this) #2 align 2 { // CHECK:STDOUT: define internal void @_ZNK6Carbon1A1FEv(ptr noundef nonnull align 1 %this) #2 align 2 {
// CHECK:STDOUT: entry: // CHECK:STDOUT: entry:
// CHECK:STDOUT: %this.addr = alloca ptr, align 8 // CHECK:STDOUT: %this.addr = alloca ptr, align 8
// CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8, !tbaa !12 // CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8, !tbaa !12
@@ -197,7 +197,7 @@ fn CallCallF() { Cpp.CallF(); }
// CHECK:STDOUT: entry: // CHECK:STDOUT: entry:
// CHECK:STDOUT: %ref.tmp = alloca %"class.Carbon::A", align 1 // CHECK:STDOUT: %ref.tmp = alloca %"class.Carbon::A", align 1
// CHECK:STDOUT: call void @llvm.lifetime.start.p0(ptr %ref.tmp) #4 // CHECK:STDOUT: call void @llvm.lifetime.start.p0(ptr %ref.tmp) #4
// CHECK:STDOUT: %call = call noundef i32 @_ZN6Carbon1A1FEi(ptr noundef nonnull align 1 %ref.tmp, i32 noundef 123) // CHECK:STDOUT: %call = call noundef i32 @_ZNK6Carbon1A1FEi(ptr noundef nonnull align 1 %ref.tmp, i32 noundef 123)
// CHECK:STDOUT: call void @_ZN6Carbon1AD2Ev(ptr noundef nonnull align 1 %ref.tmp) #4 // CHECK:STDOUT: call void @_ZN6Carbon1AD2Ev(ptr noundef nonnull align 1 %ref.tmp) #4
// CHECK:STDOUT: call void @llvm.lifetime.end.p0(ptr %ref.tmp) #4 // CHECK:STDOUT: call void @llvm.lifetime.end.p0(ptr %ref.tmp) #4
// CHECK:STDOUT: ret i32 %call // CHECK:STDOUT: ret i32 %call
@@ -207,7 +207,7 @@ fn CallCallF() { Cpp.CallF(); }
// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1 // CHECK:STDOUT: declare void @llvm.lifetime.start.p0(ptr captures(none)) #1
// CHECK:STDOUT: // CHECK:STDOUT:
// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable // CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable
// CHECK:STDOUT: define internal noundef i32 @_ZN6Carbon1A1FEi(ptr noundef nonnull align 1 %this, i32 noundef %0) #2 align 2 { // CHECK:STDOUT: define internal noundef i32 @_ZNK6Carbon1A1FEi(ptr noundef nonnull align 1 %this, i32 noundef %0) #2 align 2 {
// CHECK:STDOUT: entry: // CHECK:STDOUT: entry:
// CHECK:STDOUT: %retval = alloca i32, align 4 // CHECK:STDOUT: %retval = alloca i32, align 4
// CHECK:STDOUT: %this.addr = alloca ptr, align 8 // CHECK:STDOUT: %this.addr = alloca ptr, align 8
@@ -63,7 +63,7 @@ fn DoThing() {
// CHECK:STDOUT: // CHECK:STDOUT:
// CHECK:STDOUT: @_ZTV4Base = available_externally constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr @_ZTI4Base, ptr @_ZN11FurtherBase17further_base_funcEv, ptr @_ZNR4Base4funcEv] }, align 8 // CHECK:STDOUT: @_ZTV4Base = available_externally constant { [4 x ptr] } { [4 x ptr] [ptr null, ptr @_ZTI4Base, ptr @_ZN11FurtherBase17further_base_funcEv, ptr @_ZNR4Base4funcEv] }, align 8
// CHECK:STDOUT: @_ZTV11FurtherBase = available_externally constant { [3 x ptr] } { [3 x ptr] [ptr null, ptr @_ZTI11FurtherBase, ptr @_ZN11FurtherBase17further_base_funcEv] }, align 8 // CHECK:STDOUT: @_ZTV11FurtherBase = available_externally constant { [3 x ptr] } { [3 x ptr] [ptr null, ptr @_ZTI11FurtherBase, ptr @_ZN11FurtherBase17further_base_funcEv] }, align 8
// CHECK:STDOUT: @_ZTVN6Carbon7DerivedE = linkonce_odr dso_local constant { [5 x ptr] } { [5 x ptr] [ptr null, ptr @_ZTIN6Carbon7DerivedE, ptr @_ZN11FurtherBase17further_base_funcEv, ptr @_ZNR6Carbon7Derived4funcEv, ptr @_ZN6Carbon7Derived10other_funcEv] }, comdat, align 8 // CHECK:STDOUT: @_ZTVN6Carbon7DerivedE = linkonce_odr dso_local constant { [5 x ptr] } { [5 x ptr] [ptr null, ptr @_ZTIN6Carbon7DerivedE, ptr @_ZN11FurtherBase17further_base_funcEv, ptr @_ZNR6Carbon7Derived4funcEv, ptr @_ZNK6Carbon7Derived10other_funcEv] }, comdat, align 8
// CHECK:STDOUT: @_ZTI4Base = external constant ptr // CHECK:STDOUT: @_ZTI4Base = external constant ptr
// CHECK:STDOUT: @_ZTI11FurtherBase = external constant ptr // CHECK:STDOUT: @_ZTI11FurtherBase = external constant ptr
// CHECK:STDOUT: @_ZTIN6Carbon7DerivedE = linkonce_odr dso_local constant { ptr, ptr, ptr } { ptr getelementptr inbounds (ptr, ptr @_ZTVN10__cxxabiv120__si_class_type_infoE, i64 2), ptr @_ZTSN6Carbon7DerivedE, ptr @_ZTI4Base }, comdat, align 8 // CHECK:STDOUT: @_ZTIN6Carbon7DerivedE = linkonce_odr dso_local constant { ptr, ptr, ptr } { ptr getelementptr inbounds (ptr, ptr @_ZTVN10__cxxabiv120__si_class_type_infoE, i64 2), ptr @_ZTSN6Carbon7DerivedE, ptr @_ZTI4Base }, comdat, align 8
@@ -180,7 +180,7 @@ fn DoThing() {
// CHECK:STDOUT: } // CHECK:STDOUT: }
// CHECK:STDOUT: // CHECK:STDOUT:
// CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable // CHECK:STDOUT: ; Function Attrs: alwaysinline mustprogress nounwind uwtable
// CHECK:STDOUT: define internal void @_ZN6Carbon7Derived10other_funcEv(ptr noundef nonnull align 8 dereferenceable(12) %this) unnamed_addr #4 align 2 { // CHECK:STDOUT: define internal void @_ZNK6Carbon7Derived10other_funcEv(ptr noundef nonnull align 8 dereferenceable(12) %this) unnamed_addr #4 align 2 {
// CHECK:STDOUT: entry: // CHECK:STDOUT: entry:
// CHECK:STDOUT: %this.addr = alloca ptr, align 8 // CHECK:STDOUT: %this.addr = alloca ptr, align 8
// CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8, !tbaa !59 // CHECK:STDOUT: store ptr %this, ptr %this.addr, align 8, !tbaa !59