Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CIR][CIRGen] Implement delegating constructors #821

Merged
merged 1 commit into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 48 additions & 3 deletions clang/lib/CIR/CodeGen/CIRGenClass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ bool CIRGenFunction::IsConstructorDelegationValid(

// FIXME: Decide if we can do a delegation of a delegating constructor.
if (Ctor->isDelegatingConstructor())
llvm_unreachable("NYI");
return false;

return true;
}
Expand Down Expand Up @@ -585,7 +585,7 @@ void CIRGenFunction::buildCtorPrologue(const CXXConstructorDecl *CD,
CXXCtorType CtorType,
FunctionArgList &Args) {
if (CD->isDelegatingConstructor())
llvm_unreachable("NYI");
return buildDelegatingCXXConstructorCall(CD, Args);

const CXXRecordDecl *ClassDecl = CD->getParent();

Expand Down Expand Up @@ -1379,6 +1379,51 @@ void CIRGenFunction::EnterDtorCleanups(const CXXDestructorDecl *DD,
assert(!MissingFeatures::sanitizeDtor());
}

namespace {
struct CallDelegatingCtorDtor final : EHScopeStack::Cleanup {
const CXXDestructorDecl *Dtor;
Address Addr;
CXXDtorType Type;

CallDelegatingCtorDtor(const CXXDestructorDecl *D, Address Addr,
CXXDtorType Type)
: Dtor(D), Addr(Addr), Type(Type) {}

void Emit(CIRGenFunction &CGF, Flags flags) override {
// We are calling the destructor from within the constructor.
// Therefore, "this" should have the expected type.
QualType ThisTy = Dtor->getFunctionObjectParameterType();
CGF.buildCXXDestructorCall(Dtor, Type, /*ForVirtualBase=*/false,
/*Delegating=*/true, Addr, ThisTy);
}
};
} // end anonymous namespace

void CIRGenFunction::buildDelegatingCXXConstructorCall(
const CXXConstructorDecl *Ctor, const FunctionArgList &Args) {
assert(Ctor->isDelegatingConstructor());

Address ThisPtr = LoadCXXThisAddress();

AggValueSlot AggSlot = AggValueSlot::forAddr(
ThisPtr, Qualifiers(), AggValueSlot::IsDestructed,
AggValueSlot::DoesNotNeedGCBarriers, AggValueSlot::IsNotAliased,
AggValueSlot::MayOverlap, AggValueSlot::IsNotZeroed,
// Checks are made by the code that calls constructor.
AggValueSlot::IsSanitizerChecked);

buildAggExpr(Ctor->init_begin()[0]->getInit(), AggSlot);

const CXXRecordDecl *ClassDecl = Ctor->getParent();
if (CGM.getLangOpts().Exceptions && !ClassDecl->hasTrivialDestructor()) {
CXXDtorType Type =
CurGD.getCtorType() == Ctor_Complete ? Dtor_Complete : Dtor_Base;

EHStack.pushCleanup<CallDelegatingCtorDtor>(
EHCleanup, ClassDecl->getDestructor(), ThisPtr, Type);
}
}

void CIRGenFunction::buildCXXDestructorCall(const CXXDestructorDecl *DD,
CXXDtorType Type,
bool ForVirtualBase,
Expand Down Expand Up @@ -1710,4 +1755,4 @@ void CIRGenFunction::buildCXXAggrConstructorCall(

if (constantCount.use_empty())
constantCount.erase();
}
}
4 changes: 3 additions & 1 deletion clang/lib/CIR/CodeGen/CIRGenExprCXX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,9 @@ void CIRGenFunction::buildCXXConstructExpr(const CXXConstructExpr *E,
Type = Ctor_Complete;
break;
case CXXConstructionKind::Delegating:
llvm_unreachable("NYI");
// We should be emitting a constructor; GlobalDecl will assert this
Type = CurGD.getCtorType();
Delegating = true;
break;
case CXXConstructionKind::VirtualBase:
ForVirtualBase = true;
Expand Down
7 changes: 7 additions & 0 deletions clang/lib/CIR/CodeGen/CIRGenFunction.h
Original file line number Diff line number Diff line change
Expand Up @@ -1649,6 +1649,13 @@ class CIRGenFunction : public CIRGenTypeCache {
const FunctionArgList &Args,
clang::SourceLocation Loc);

// It's important not to confuse this and the previous function. Delegating
// constructors are the C++11 feature. The constructor delegate optimization
// is used to reduce duplication in the base and complete constructors where
// they are substantially the same.
void buildDelegatingCXXConstructorCall(const CXXConstructorDecl *Ctor,
const FunctionArgList &Args);

/// We are performing a delegate call; that is, the current function is
/// delegating to another one. Produce a r-value suitable for passing the
/// given parameter.
Expand Down
88 changes: 88 additions & 0 deletions clang/test/CIR/CodeGen/delegating-ctor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir -fexceptions -fcxx-exceptions %s -o %t.cir
// RUN: FileCheck --input-file=%t.cir %s

struct Delegating {
Delegating();
Delegating(int);
};

// Check that the constructor being delegated to is called with the correct
// arguments.
Delegating::Delegating() : Delegating(0) {}

// CHECK-LABEL: cir.func @_ZN10DelegatingC2Ev(%arg0: !cir.ptr<!ty_22Delegating22> {{.*}}) {{.*}} {
// CHECK-NEXT: %0 = cir.alloca !cir.ptr<!ty_22Delegating22>, !cir.ptr<!cir.ptr<!ty_22Delegating22>>, ["this", init] {alignment = 8 : i64}
// CHECK-NEXT: cir.store %arg0, %0 : !cir.ptr<!ty_22Delegating22>, !cir.ptr<!cir.ptr<!ty_22Delegating22>>
// CHECK-NEXT: %1 = cir.load %0 : !cir.ptr<!cir.ptr<!ty_22Delegating22>>, !cir.ptr<!ty_22Delegating22>
// CHECK-NEXT: %2 = cir.const #cir.int<0> : !s32i
// CHECK-NEXT: cir.call @_ZN10DelegatingC2Ei(%1, %2) : (!cir.ptr<!ty_22Delegating22>, !s32i) -> ()
// CHECK-NEXT: cir.return
// CHECK-NEXT: }

struct DelegatingWithZeroing {
int i;
DelegatingWithZeroing() = default;
DelegatingWithZeroing(int);
};

// Check that the delegating constructor performs zero-initialization here.
// FIXME: we should either emit the trivial default constructor or remove the
Copy link
Collaborator Author

@smeenai smeenai Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To elaborate on this: CodeGen omits calls to default trivial constructors:

// If this is a call to a trivial default constructor, do nothing.
if (CD->isTrivial() && CD->isDefaultConstructor())
return;

CIRGen purposely skips that logic:

// If this is a call to a trivial default constructor:
// In LLVM: do nothing.
// In CIR: emit as a regular call, other later passes should lower the
// ctor call into trivial initialization.
// if (CD->isTrivial() && CD->isDefaultConstructor())
// return;

There doesn't actually seem to be a pass that lowers the ctor call into trivial initialization though (or at least I couldn't get such a pass to run), so the emitted LLVM IR still contains the constructor call, but the constructor is never defined, so we'll get a link error.

I tried triggering the same behavior in another way without any luck. E.g. for https://godbolt.org/z/n43n6h185, we're not emitting the default constructor call; but that's because we end up in here instead:

constant = ConstantEmitter(*this).tryEmitAbstractForInitializer(D);

The comments in that function about retaining ctor calls also seem potentially inaccurate though, or I'm misunderstanding their intent.

Copy link
Member

@bcardosolopes bcardosolopes Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't actually seem to be a pass that lowers the ctor call into trivial initialization though (or at least I couldn't get such a pass to run), so the emitted LLVM IR still contains the constructor call, but the constructor is never defined, so we'll get a link error.

That's right, we need to add support in LoweringPrepare for those. Similar / related to things tracked with should* in missing features.

The initial plan here was to add a cir.ctor_call operation, which could have attributes to designate triviality, and keep some other properties around (via AST or directly), such operation would be folded away in LoweringPrepare. It could also be emitted directly out of CIRGen or added via the idiom recognizer (I prefer the former).

None of this has been implemented yet. An alternative solution is to omit the ctor call directly (like OG codegen does) and add an assert on missing feature. Such that we can track and come back to this whenever we care about an analysis that uses this information - but in the meantime we get LLVM codegen parity.

Right now you'd have to look at a regular cir.call and try to see if it was a ctor call + determine if it's trivial, which isn't ideal since we have CIR for the very reason (raising ).

The comments in that function about retaining ctor calls also seem potentially inaccurate though, or I'm misunderstanding their intent.

The part on retaining ctor calls is legit, for purposes of static analsys we might want the allocas to be tagged properly based on ctors touching them, like the lifetime checker does. We need to keep the source semantics close first out of CIRGen.

// call to it in a lowering pass.
DelegatingWithZeroing::DelegatingWithZeroing(int) : DelegatingWithZeroing() {}

// CHECK-LABEL: cir.func @_ZN21DelegatingWithZeroingC2Ei(%arg0: !cir.ptr<!ty_22DelegatingWithZeroing22> {{.*}}, %arg1: !s32i {{.*}}) {{.*}} {
// CHECK-NEXT: %0 = cir.alloca !cir.ptr<!ty_22DelegatingWithZeroing22>, !cir.ptr<!cir.ptr<!ty_22DelegatingWithZeroing22>>, ["this", init] {alignment = 8 : i64}
// CHECK-NEXT: %1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["", init] {alignment = 4 : i64}
// CHECK-NEXT: cir.store %arg0, %0 : !cir.ptr<!ty_22DelegatingWithZeroing22>, !cir.ptr<!cir.ptr<!ty_22DelegatingWithZeroing22>>
// CHECK-NEXT: cir.store %arg1, %1 : !s32i, !cir.ptr<!s32i>
// CHECK-NEXT: %2 = cir.load %0 : !cir.ptr<!cir.ptr<!ty_22DelegatingWithZeroing22>>, !cir.ptr<!ty_22DelegatingWithZeroing22>
// CHECK-NEXT: %3 = cir.const #cir.zero : !ty_22DelegatingWithZeroing22
// CHECK-NEXT: cir.store %3, %2 : !ty_22DelegatingWithZeroing22, !cir.ptr<!ty_22DelegatingWithZeroing22>
// CHECK-NEXT: cir.call @_ZN21DelegatingWithZeroingC2Ev(%2) : (!cir.ptr<!ty_22DelegatingWithZeroing22>) -> () extra(#fn_attr1)
// CHECK-NEXT: cir.return
// CHECK-NEXT: }

void canThrow();
struct HasNonTrivialDestructor {
HasNonTrivialDestructor();
HasNonTrivialDestructor(int);
~HasNonTrivialDestructor();
};

// Check that we call the destructor whenever a cleanup is needed.
// FIXME: enable and check this when exceptions are fully supported.
#if 0
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure of the best way to handle a case like this, where it'll currently NYI but we want to run the test once other support is in place. I could just not include these tests at all, but then we have to remember to add them later. By including them with an #if 0 and FIXME comments, there's at least some indication of missing coverage, though we still have to remember to enable them later.

The alternative would be creating a separate test that's currently marked XFAIL, which should start passing once the NYIs are implemented, at which point an XFAIL test passing would error out and give us a loud reminder to come back to this. The problem is that we then put the burden of fixing the test up on the person implementing the NYI, which isn't ideal either (e.g. you shouldn't have to also flesh out delegating constructor support at the same time as implementing virtual bases). Suggestions are welcome here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure of the best way to handle a case like this, where it'll currently NYI but we want to run the test once other support is in place. I could just not include these tests at all, but then we have to remember to add them later. By including them with an #if 0 and FIXME comments, there's at least some indication of missing coverage, though we still have to remember to enable them later.

The alternative would be creating a separate test that's currently marked XFAIL, which should start passing once the NYIs are implemented, at which point an XFAIL test passing would error out and give us a loud reminder to come back to this. The problem is that we then put the burden of fixing the test up on the person implementing the NYI, which isn't ideal either (e.g. you shouldn't have to also flesh out delegating constructor support at the same time as implementing virtual bases). Suggestions are welcome here.

I don't think there's a silver bullet and there might a best judgment call depending on the case, some testcases also have commented code (e.g. the massive aarch64 builtins), which are very much like #if 0s. I'm fine with the #if 0 solution here, cause I know you might work on it at some point soon, so I'm not too worried. I'm usually more keen to the XFAIL idea, even though you have valid points, IMO some parts of the language / missing features are more tricky and will require some extra burden on the person implementing to get things right.

HasNonTrivialDestructor::HasNonTrivialDestructor(int)
: HasNonTrivialDestructor() {
canThrow();
}
#endif

// From clang/test/CodeGenCXX/cxx0x-delegating-ctors.cpp, check that virtual
// inheritance and delegating constructors interact correctly.
// FIXME: enable and check this when virtual inheritance is fully supported.
#if 0
namespace PR14588 {
void other();

class Base {
public:
Base() { squawk(); }
virtual ~Base() {}

virtual void squawk() { other(); }
};

class Foo : public virtual Base {
public:
Foo();
Foo(const void *inVoid);
virtual ~Foo() {}

virtual void squawk() { other(); }
};

Foo::Foo() : Foo(nullptr) { other(); }
Foo::Foo(const void *inVoid) { squawk(); }
} // namespace PR14588
#endif