[CIR] Implement cleanups for temporaries with automatic duration (#189754)

This implements handling for cleanup of temporary variables with
automatic storage duration. This is a simplified implementation that
doesn't yet handle the possibility of exceptions being thrown within
this cleanup scope or the cleanup scope being inside a conditional
operation. Support for those cases will be added later.
This commit is contained in:
Andy Kaylor 2026-04-03 08:38:06 -07:00 committed by GitHub
parent 62bbe3fffc
commit 5b56352757
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 126 additions and 5 deletions

View File

@ -542,3 +542,18 @@ void CIRGenFunction::popCleanupBlocks(
}
}
}
/// Pops cleanup blocks until the given savepoint is reached, then add the
/// cleanups from the given savepoint in the lifetime-extended cleanups stack.
void CIRGenFunction::popCleanupBlocks(
EHScopeStack::stable_iterator oldCleanupStackDepth,
size_t oldLifetimeExtendedSize, ArrayRef<mlir::Value *> valuesToReload) {
popCleanupBlocks(oldCleanupStackDepth, valuesToReload);
// Promote deferred lifetime-extended cleanups onto the EH scope stack.
for (const LifetimeExtendedCleanupEntry &cleanup : llvm::make_range(
lifetimeExtendedCleanupStack.begin() + oldLifetimeExtendedSize,
lifetimeExtendedCleanupStack.end()))
pushLifetimeExtendedCleanupToEHStack(cleanup);
lifetimeExtendedCleanupStack.truncate(oldLifetimeExtendedSize);
}

View File

@ -1007,6 +1007,37 @@ void CIRGenFunction::pushDestroy(CleanupKind cleanupKind, Address addr,
pushFullExprCleanup<DestroyObject>(cleanupKind, addr, type, destroyer);
}
void CIRGenFunction::pushLifetimeExtendedDestroy(CleanupKind cleanupKind,
Address addr, QualType type,
Destroyer *destroyer,
bool useEHCleanupForArray) {
if (isInConditionalBranch()) {
cgm.errorNYI("conditional lifetime-extended destroy");
return;
}
// Classic codegen also uses pushDestroyAndDeferDeactivation here to push an
// EH cleanup that protects the temporary during the rest of the full
// expression, then deactivates it when the full expression ends. We don't
// have deferred deactivation yet, so we only queue the lifetime-extended
// cleanup below. When deferred deactivation is implemented, add the
// pushDestroyAndDeferDeactivation call here.
if (getLangOpts().Exceptions) {
cgm.errorNYI("lifetime-extended cleanup with exceptions enabled");
return;
}
assert(!cir::MissingFeatures::useEHCleanupForArray());
pushCleanupAfterFullExpr(cleanupKind, addr, type, destroyer);
}
void CIRGenFunction::pushLifetimeExtendedCleanupToEHStack(
const LifetimeExtendedCleanupEntry &entry) {
ehStack.pushCleanup<DestroyObject>(entry.kind, entry.addr, entry.type,
entry.destroyer);
}
/// Destroys all the elements of the given array, beginning from last to first.
/// The array cannot be zero-length.
///

View File

@ -1719,8 +1719,9 @@ static void pushTemporaryCleanup(CIRGenFunction &cgf,
break;
case SD_Automatic:
cgf.cgm.errorNYI(e->getSourceRange(),
"pushTemporaryCleanup: automatic storage duration");
cgf.pushLifetimeExtendedDestroy(
NormalAndEHCleanup, referenceTemporary, e->getType(),
CIRGenFunction::destroyCXXObject, cgf.getLangOpts().Exceptions);
break;
case SD_Dynamic:

View File

@ -92,6 +92,25 @@ public:
/// Tracks function scope overall cleanup handling.
EHScopeStack ehStack;
typedef void Destroyer(CIRGenFunction &cgf, Address addr, QualType ty);
/// An entry in the lifetime-extended cleanup stack. Each entry represents a
/// cleanup that was deferred past a full-expression boundary (e.g.,
/// destroying a temporary bound to a local reference). When the enclosing
/// scope exits, these entries are promoted to the EH scope stack.
///
/// Currently only DestroyObject cleanups are lifetime-extended. When other
/// cleanup types are needed (e.g., CallLifetimeEnd), this struct can be
/// extended with a std::variant of cleanup data types.
struct LifetimeExtendedCleanupEntry {
CleanupKind kind;
Address addr;
QualType type;
Destroyer *destroyer;
};
llvm::SmallVector<LifetimeExtendedCleanupEntry> lifetimeExtendedCleanupStack;
GlobalDecl curSEHParent;
/// A mapping from NRVO variables to the flags used to indicate
@ -970,6 +989,12 @@ public:
/// that have been added.
void popCleanupBlocks(EHScopeStack::stable_iterator oldCleanupStackDepth,
ArrayRef<mlir::Value *> valuesToReload = {});
/// Pops cleanup blocks until the given savepoint is reached, then adds the
/// cleanups from the given savepoint in the lifetime-extended cleanups stack.
void popCleanupBlocks(EHScopeStack::stable_iterator oldCleanupStackDepth,
size_t oldLifetimeExtendedSize,
ArrayRef<mlir::Value *> valuesToReload = {});
void popCleanupBlock();
void terminateStructuredRegionBody(mlir::Region &r, mlir::Location loc);
@ -997,10 +1022,19 @@ public:
cgm.errorNYI("pushFullExprCleanup in conditional branch");
}
/// Queue a cleanup to be pushed after finishing the current full-expression.
/// When the enclosing RunCleanupsScope exits, popCleanupBlocks promotes these
/// entries onto the EH scope stack for the enclosing scope.
void pushCleanupAfterFullExpr(CleanupKind kind, Address addr, QualType type,
Destroyer *destroyer) {
lifetimeExtendedCleanupStack.push_back({kind, addr, type, destroyer});
}
/// Enters a new scope for capturing cleanups, all of which
/// will be executed once the scope is exited.
class RunCleanupsScope {
EHScopeStack::stable_iterator cleanupStackDepth, oldCleanupStackDepth;
size_t lifetimeExtendedCleanupStackSize;
protected:
bool performCleanup;
@ -1018,6 +1052,8 @@ public:
explicit RunCleanupsScope(CIRGenFunction &cgf)
: performCleanup(true), cgf(cgf) {
cleanupStackDepth = cgf.ehStack.stable_begin();
lifetimeExtendedCleanupStackSize =
cgf.lifetimeExtendedCleanupStack.size();
oldDidCallStackSave = cgf.didCallStackSave;
cgf.didCallStackSave = false;
oldCleanupStackDepth = cgf.currentCleanupStackDepth;
@ -1035,7 +1071,8 @@ public:
void forceCleanup(ArrayRef<mlir::Value *> valuesToReload = {}) {
assert(performCleanup && "Already forced cleanup");
cgf.didCallStackSave = oldDidCallStackSave;
cgf.popCleanupBlocks(cleanupStackDepth, valuesToReload);
cgf.popCleanupBlocks(cleanupStackDepth, lifetimeExtendedCleanupStackSize,
valuesToReload);
performCleanup = false;
cgf.currentCleanupStackDepth = oldCleanupStackDepth;
}
@ -1247,8 +1284,6 @@ public:
LexicalScope *curLexScope = nullptr;
typedef void Destroyer(CIRGenFunction &cgf, Address addr, QualType ty);
static Destroyer destroyCXXObject;
void pushDestroy(QualType::DestructionKind dtorKind, Address addr,
@ -1257,6 +1292,15 @@ public:
void pushDestroy(CleanupKind kind, Address addr, QualType type,
Destroyer *destroyer);
void pushLifetimeExtendedDestroy(CleanupKind kind, Address addr,
QualType type, Destroyer *destroyer,
bool useEHCleanupForArray);
/// Promote a single lifetime-extended cleanup entry onto the EH scope stack.
/// Defined in CIRGenDecl.cpp where the concrete cleanup types are visible.
void pushLifetimeExtendedCleanupToEHStack(
const LifetimeExtendedCleanupEntry &entry);
Destroyer *getDestroyer(clang::QualType::DestructionKind kind);
/// Start generating a thunk function.

View File

@ -206,6 +206,21 @@ public:
return new (buffer) T(n, a...);
}
/// Push a cleanup by copying a serialized cleanup object from the
/// LifetimeExtendedCleanupStack onto the EH scope stack. This is used when
/// a full-expression's RunCleanupsScope exits: cleanups that were deferred
/// for lifetime extension (e.g. destroying a temporary bound to a local
/// reference) are promoted from the byte buffer to the enclosing scope's
/// EH stack so they run when that scope ends.
///
/// The memcpy is safe because Cleanup subclasses are required to be POD-like
/// (see the Cleanup class comment), and the vtable pointer is part of the
/// copied bytes, so the clone dispatches to the correct emit() override.
void pushCopyOfCleanup(CleanupKind kind, const void *cleanup, size_t size) {
void *buffer = pushCleanup(kind, size);
std::memcpy(buffer, cleanup, size);
}
void setCGF(CIRGenFunction *inCGF) { cgf = inCGF; }
/// Pops a cleanup scope off the stack. This is private to CIRGenCleanup.cpp.

View File

@ -149,3 +149,18 @@ void complex_expr_with_cleanup_inside_cleanupscope() {
// CHECK: }
// CHECK: %[[RELOAD:.*]] = cir.load {{.*}} %[[TEMP_ADDR]] : !cir.ptr<!cir.complex<!s32i>>, !cir.complex<!s32i>
// CHECK: cir.store {{.*}} %[[RELOAD]], %[[RESULT]] : !cir.complex<!s32i>, !cir.ptr<!cir.complex<!s32i>>
void test_cleanup_with_automatic_storage_duration() {
const Struk &ref = Struk{};
}
// CHECK: cir.func{{.*}} @_Z44test_cleanup_with_automatic_storage_durationv()
// CHECK: %[[REF_TMP:.*]] = cir.alloca !rec_Struk, !cir.ptr<!rec_Struk>, ["ref.tmp0"]
// CHECK: %[[REF:.*]] = cir.alloca !cir.ptr<!rec_Struk>, !cir.ptr<!cir.ptr<!rec_Struk>>, ["ref", init, const]
// CHECK: cir.cleanup.scope {
// CHECK: cir.store{{.*}} %[[REF_TMP]], %[[REF]]
// CHECK: cir.yield
// CHECK: } cleanup normal {
// CHECK: cir.call @_ZN5StrukD1Ev(%[[REF_TMP]]) nothrow
// CHECK: cir.yield
// CHECK: }