llvm-project/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp
Andy Kaylor 5b56352757
[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.
2026-04-03 08:38:06 -07:00

560 lines
22 KiB
C++

//===--- CIRGenCleanup.cpp - Bookkeeping and code emission for cleanups ---===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file contains code dealing with the IR generation for cleanups
// and related information.
//
// A "cleanup" is a piece of code which needs to be executed whenever
// control transfers out of a particular scope. This can be
// conditionalized to occur only on exceptional control flow, only on
// normal control flow, or both.
//
//===----------------------------------------------------------------------===//
#include "CIRGenCleanup.h"
#include "CIRGenFunction.h"
#include "clang/CIR/MissingFeatures.h"
using namespace clang;
using namespace clang::CIRGen;
//===----------------------------------------------------------------------===//
// CIRGenFunction cleanup related
//===----------------------------------------------------------------------===//
/// Emits all the code to cause the given temporary to be cleaned up.
void CIRGenFunction::emitCXXTemporary(const CXXTemporary *temporary,
QualType tempType, Address ptr) {
pushDestroy(NormalAndEHCleanup, ptr, tempType, destroyCXXObject);
}
//===----------------------------------------------------------------------===//
// EHScopeStack
//===----------------------------------------------------------------------===//
void EHScopeStack::Cleanup::anchor() {}
EHScopeStack::stable_iterator
EHScopeStack::getInnermostActiveNormalCleanup() const {
stable_iterator si = getInnermostNormalCleanup();
stable_iterator se = stable_end();
while (si != se) {
EHCleanupScope &cleanup = llvm::cast<EHCleanupScope>(*find(si));
if (cleanup.isActive())
return si;
si = cleanup.getEnclosingNormalCleanup();
}
return stable_end();
}
/// Push an entry of the given size onto this protected-scope stack.
char *EHScopeStack::allocate(size_t size) {
size = llvm::alignTo(size, ScopeStackAlignment);
if (!startOfBuffer) {
unsigned capacity = llvm::PowerOf2Ceil(std::max<size_t>(size, 1024ul));
startOfBuffer = std::make_unique<char[]>(capacity);
startOfData = endOfBuffer = startOfBuffer.get() + capacity;
} else if (static_cast<size_t>(startOfData - startOfBuffer.get()) < size) {
unsigned currentCapacity = endOfBuffer - startOfBuffer.get();
unsigned usedCapacity =
currentCapacity - (startOfData - startOfBuffer.get());
unsigned requiredCapacity = usedCapacity + size;
// We know from the 'else if' condition that requiredCapacity is greater
// than currentCapacity.
unsigned newCapacity = llvm::PowerOf2Ceil(requiredCapacity);
std::unique_ptr<char[]> newStartOfBuffer =
std::make_unique<char[]>(newCapacity);
char *newEndOfBuffer = newStartOfBuffer.get() + newCapacity;
char *newStartOfData = newEndOfBuffer - usedCapacity;
memcpy(newStartOfData, startOfData, usedCapacity);
startOfBuffer.swap(newStartOfBuffer);
endOfBuffer = newEndOfBuffer;
startOfData = newStartOfData;
}
assert(startOfBuffer.get() + size <= startOfData);
startOfData -= size;
return startOfData;
}
void EHScopeStack::deallocate(size_t size) {
startOfData += llvm::alignTo(size, ScopeStackAlignment);
}
void *EHScopeStack::pushCleanup(CleanupKind kind, size_t size) {
char *buffer = allocate(EHCleanupScope::getSizeForCleanupSize(size));
bool isNormalCleanup = kind & NormalCleanup;
bool isEHCleanup = kind & EHCleanup;
bool isLifetimeMarker = kind & LifetimeMarker;
bool skipCleanupScope = false;
cir::CleanupKind cleanupKind = cir::CleanupKind::All;
if (isEHCleanup && cgf->getLangOpts().Exceptions) {
cleanupKind =
isNormalCleanup ? cir::CleanupKind::All : cir::CleanupKind::EH;
} else {
if (isNormalCleanup)
cleanupKind = cir::CleanupKind::Normal;
else
skipCleanupScope = true;
}
cir::CleanupScopeOp cleanupScope = nullptr;
if (!skipCleanupScope) {
CIRGenBuilderTy &builder = cgf->getBuilder();
mlir::Location loc = builder.getUnknownLoc();
cleanupScope = cir::CleanupScopeOp::create(
builder, loc, cleanupKind,
/*bodyBuilder=*/
[&](mlir::OpBuilder &b, mlir::Location loc) {
// Terminations will be handled in popCleanup
},
/*cleanupBuilder=*/
[&](mlir::OpBuilder &b, mlir::Location loc) {
// Terminations will be handled after emiting cleanup
});
builder.setInsertionPointToEnd(&cleanupScope.getBodyRegion().back());
}
// Per C++ [except.terminate], it is implementation-defined whether none,
// some, or all cleanups are called before std::terminate. Thus, when
// terminate is the current EH scope, we may skip adding any EH cleanup
// scopes.
if (innermostEHScope != stable_end() &&
find(innermostEHScope)->getKind() == EHScope::Terminate)
isEHCleanup = false;
EHCleanupScope *scope = new (buffer)
EHCleanupScope(isNormalCleanup, isEHCleanup, size, cleanupScope,
innermostNormalCleanup, innermostEHScope);
if (isNormalCleanup)
innermostNormalCleanup = stable_begin();
if (isEHCleanup)
innermostEHScope = stable_begin();
if (isLifetimeMarker)
cgf->cgm.errorNYI("push lifetime marker cleanup");
// With Windows -EHa, Invoke llvm.seh.scope.begin() for EHCleanup
if (cgf->getLangOpts().EHAsynch && isEHCleanup && !isLifetimeMarker &&
cgf->getTarget().getCXXABI().isMicrosoft())
cgf->cgm.errorNYI("push seh cleanup");
return scope->getCleanupBuffer();
}
void EHScopeStack::popCleanup() {
assert(!empty() && "popping exception stack when not empty");
assert(isa<EHCleanupScope>(*begin()));
EHCleanupScope &cleanup = cast<EHCleanupScope>(*begin());
innermostNormalCleanup = cleanup.getEnclosingNormalCleanup();
innermostEHScope = cleanup.getEnclosingEHScope();
deallocate(cleanup.getAllocatedSize());
cir::CleanupScopeOp cleanupScope = cleanup.getCleanupScopeOp();
if (cleanupScope) {
auto *block = &cleanupScope.getBodyRegion().back();
if (!block->mightHaveTerminator()) {
mlir::OpBuilder::InsertionGuard guard(cgf->getBuilder());
cgf->getBuilder().setInsertionPointToEnd(block);
cir::YieldOp::create(cgf->getBuilder(),
cgf->getBuilder().getUnknownLoc());
}
cgf->getBuilder().setInsertionPointAfter(cleanupScope);
}
// Destroy the cleanup.
cleanup.destroy();
}
bool EHScopeStack::requiresCatchOrCleanup() const {
for (stable_iterator si = getInnermostEHScope(); si != stable_end();) {
if (auto *cleanup = dyn_cast<EHCleanupScope>(&*find(si))) {
if (cleanup->isLifetimeMarker()) {
// Skip lifetime markers and continue from the enclosing EH scope
assert(!cir::MissingFeatures::emitLifetimeMarkers());
continue;
}
}
return true;
}
return false;
}
/// The given cleanup block is being deactivated. Configure a cleanup variable
/// if necessary.
static void setupCleanupBlockDeactivation(CIRGenFunction &cgf,
EHScopeStack::stable_iterator c,
mlir::Operation *dominatingIP) {
EHCleanupScope &scope = cast<EHCleanupScope>(*cgf.ehStack.find(c));
assert((scope.isNormalCleanup() || scope.isEHCleanup()) &&
"cleanup block is neither normal nor EH?");
if (scope.isNormalCleanup())
scope.setTestFlagInNormalCleanup();
if (scope.isEHCleanup())
scope.setTestFlagInEHCleanup();
CIRGenBuilderTy &builder = cgf.getBuilder();
// If the cleanup block doesn't exist yet, create it and set its initial
// value to `true`. If we are inside a conditional branch, the value must be
// initialized before the conditional branch begins.
Address var = scope.getActiveFlag();
if (!var.isValid()) {
mlir::Location loc = builder.getUnknownLoc();
var = cgf.createTempAllocaWithoutCast(builder.getBoolTy(), CharUnits::One(),
loc, "cleanup.isactive");
scope.setActiveFlag(var);
assert(dominatingIP && "no existing variable and no dominating IP!");
if (cgf.isInConditionalBranch()) {
mlir::Value val = builder.getBool(true, loc);
cgf.setBeforeOutermostConditional(val, var);
} else {
mlir::OpBuilder::InsertionGuard guard(builder);
builder.setInsertionPoint(dominatingIP);
builder.createFlagStore(loc, true, var.getPointer());
}
}
// The code above sets the `isActive` flag to `true` as its initial state
// at the point where the variable is created. The code below sets it to
// `false` at the point where the cleanup is deactivated.
mlir::Location loc = builder.getUnknownLoc();
builder.createFlagStore(loc, false, var.getPointer());
}
/// Deactive a cleanup that was created in an active state.
void CIRGenFunction::deactivateCleanupBlock(EHScopeStack::stable_iterator c,
mlir::Operation *dominatingIP) {
assert(c != ehStack.stable_end() && "deactivating bottom of stack?");
EHCleanupScope &scope = cast<EHCleanupScope>(*ehStack.find(c));
assert(scope.isActive() && "double deactivation");
// If it's the top of the stack, just pop it, but do so only if it belongs
// to the current RunCleanupsScope.
if (c == ehStack.stable_begin() &&
currentCleanupStackDepth.strictlyEncloses(c)) {
popCleanupBlock();
return;
}
// Otherwise, follow the general case.
setupCleanupBlockDeactivation(*this, c, dominatingIP);
scope.setActive(false);
}
static void emitCleanup(CIRGenFunction &cgf, cir::CleanupScopeOp cleanupScope,
EHScopeStack::Cleanup *cleanup,
EHScopeStack::Cleanup::Flags flags,
Address activeFlag) {
CIRGenBuilderTy &builder = cgf.getBuilder();
mlir::Block &block = cleanupScope.getCleanupRegion().back();
mlir::OpBuilder::InsertionGuard guard(builder);
builder.setInsertionPointToStart(&block);
// Ask the cleanup to emit itself.
assert(cgf.haveInsertPoint() && "expected insertion point");
if (activeFlag.isValid()) {
mlir::Location loc = cleanupScope.getLoc();
mlir::Value isActive = builder.createFlagLoad(loc, activeFlag.getPointer());
cir::IfOp::create(builder, loc, isActive,
/*withElseRegion=*/false,
/*thenBuilder=*/
[&](mlir::OpBuilder &, mlir::Location) {
cleanup->emit(cgf, flags);
assert(cgf.haveInsertPoint() &&
"cleanup ended with no insertion point?");
builder.createYield(loc);
});
} else {
cleanup->emit(cgf, flags);
assert(cgf.haveInsertPoint() && "cleanup ended with no insertion point?");
}
mlir::Block &cleanupRegionLastBlock = cleanupScope.getCleanupRegion().back();
if (cleanupRegionLastBlock.empty() ||
!cleanupRegionLastBlock.back().hasTrait<mlir::OpTrait::IsTerminator>()) {
mlir::OpBuilder::InsertionGuard guardCase(builder);
builder.setInsertionPointToEnd(&cleanupRegionLastBlock);
builder.createYield(cleanupScope.getLoc());
}
}
static mlir::Block *createNormalEntry(CIRGenFunction &cgf,
EHCleanupScope &scope) {
assert(scope.isNormalCleanup());
mlir::Block *entry = scope.getNormalBlock();
if (!entry) {
mlir::OpBuilder::InsertionGuard guard(cgf.getBuilder());
entry = cgf.curLexScope->getOrCreateCleanupBlock(cgf.getBuilder());
scope.setNormalBlock(entry);
}
return entry;
}
void CIRGenFunction::popCleanupBlock() {
assert(!ehStack.empty() && "cleanup stack is empty!");
assert(isa<EHCleanupScope>(*ehStack.begin()) && "top not a cleanup!");
EHCleanupScope &scope = cast<EHCleanupScope>(*ehStack.begin());
cir::CleanupScopeOp cleanupScope = scope.getCleanupScopeOp();
assert(cleanupScope && "CleanupScopeOp is nullptr");
// Remember activation information.
bool isActive = scope.isActive();
Address normalActiveFlag = scope.shouldTestFlagInNormalCleanup()
? scope.getActiveFlag()
: Address::invalid();
Address ehActiveFlag = scope.shouldTestFlagInEHCleanup()
? scope.getActiveFlag()
: Address::invalid();
// - whether there's a fallthrough
mlir::Block *fallthroughSource = builder.getInsertionBlock();
bool hasFallthrough = fallthroughSource != nullptr && isActive;
bool requiresNormalCleanup = scope.isNormalCleanup() && hasFallthrough;
bool requiresEHCleanup = scope.isEHCleanup() && hasFallthrough;
// Even if we don't need the normal cleanup, we still need to emit the
// cleanup code if there's an active flag, since the EH path may still
// need to conditionally execute it.
bool hasActiveFlag = normalActiveFlag.isValid() || ehActiveFlag.isValid();
// If we don't need the cleanup at all, we're done.
assert(!cir::MissingFeatures::ehCleanupScopeRequiresEHCleanup());
if (!requiresNormalCleanup && !requiresEHCleanup && !hasActiveFlag) {
ehStack.popCleanup();
return;
}
// Copy the cleanup emission data out. This uses either a stack
// array or malloc'd memory, depending on the size, which is
// behavior that SmallVector would provide, if we could use it
// here. Unfortunately, if you ask for a SmallVector<char>, the
// alignment isn't sufficient.
auto *cleanupSource = reinterpret_cast<char *>(scope.getCleanupBuffer());
alignas(EHScopeStack::ScopeStackAlignment) char
cleanupBufferStack[8 * sizeof(void *)];
std::unique_ptr<char[]> cleanupBufferHeap;
size_t cleanupSize = scope.getCleanupSize();
EHScopeStack::Cleanup *cleanup;
// This is necessary because we are going to deallocate the cleanup
// (in popCleanup) before we emit it.
if (cleanupSize <= sizeof(cleanupBufferStack)) {
memcpy(cleanupBufferStack, cleanupSource, cleanupSize);
cleanup = reinterpret_cast<EHScopeStack::Cleanup *>(cleanupBufferStack);
} else {
cleanupBufferHeap.reset(new char[cleanupSize]);
memcpy(cleanupBufferHeap.get(), cleanupSource, cleanupSize);
cleanup =
reinterpret_cast<EHScopeStack::Cleanup *>(cleanupBufferHeap.get());
}
EHScopeStack::Cleanup::Flags cleanupFlags;
if (scope.isNormalCleanup())
cleanupFlags.setIsNormalCleanupKind();
if (scope.isEHCleanup())
cleanupFlags.setIsEHCleanupKind();
// Determine the active flag for the cleanup handler.
Address cleanupActiveFlag = normalActiveFlag.isValid() ? normalActiveFlag
: ehActiveFlag.isValid() ? ehActiveFlag
: Address::invalid();
// If we have a fallthrough and no other need for the cleanup,
// emit it directly.
if (hasFallthrough) {
assert(!cir::MissingFeatures::ehCleanupScopeRequiresEHCleanup());
ehStack.popCleanup();
scope.markEmitted();
emitCleanup(*this, cleanupScope, cleanup, cleanupFlags, cleanupActiveFlag);
} else if (!requiresNormalCleanup && !requiresEHCleanup) {
// The cleanup is inactive but has an active flag. We still need to emit
// the cleanup handler guarded by the flag, since the EH path may need it.
ehStack.popCleanup();
scope.markEmitted();
emitCleanup(*this, cleanupScope, cleanup, cleanupFlags, cleanupActiveFlag);
} else {
// Otherwise, the best approach is to thread everything through
// the cleanup block and then try to clean up after ourselves.
// Force the entry block to exist.
mlir::Block *normalEntry = createNormalEntry(*this, scope);
// I. Set up the fallthrough edge in.
mlir::OpBuilder::InsertPoint savedInactiveFallthroughIP;
// If we have a fallthrough source, but this cleanup is inactive,
// save and clear the IP.
if (!hasFallthrough && fallthroughSource) {
assert(!isActive && "source without fallthrough for active cleanup");
savedInactiveFallthroughIP = builder.saveInsertionPoint();
}
// II. Emit the entry block. This implicitly branches to it if
// we have fallthrough. All the fixups and existing branches
// should already be branched to it.
builder.setInsertionPointToEnd(normalEntry);
// intercept normal cleanup to mark SEH scope end
assert(!cir::MissingFeatures::ehCleanupScopeRequiresEHCleanup());
// III. Figure out where we're going and build the cleanup
// epilogue.
bool hasEnclosingCleanups =
(scope.getEnclosingNormalCleanup() != ehStack.stable_end());
// Compute the branch-through dest if we need it:
// - if there are branch-throughs threaded through the scope
// - if fall-through is a branch-through
// - if there are fixups that will be optimistically forwarded
// to the enclosing cleanup
assert(!cir::MissingFeatures::cleanupBranchThrough());
if (hasEnclosingCleanups)
cgm.errorNYI("cleanup branch-through dest");
mlir::Block *fallthroughDest = nullptr;
// If there's exactly one branch-after and no other threads,
// we can route it without a switch.
// Skip for SEH, since ExitSwitch is used to generate code to indicate
// abnormal termination. (SEH: Except _leave and fall-through at
// the end, all other exits in a _try (return/goto/continue/break)
// are considered as abnormal terminations, using NormalCleanupDestSlot
// to indicate abnormal termination)
assert(!cir::MissingFeatures::cleanupBranchThrough());
assert(!cir::MissingFeatures::ehCleanupScopeRequiresEHCleanup());
// IV. Pop the cleanup and emit it.
scope.markEmitted();
ehStack.popCleanup();
assert(ehStack.hasNormalCleanups() == hasEnclosingCleanups);
emitCleanup(*this, cleanupScope, cleanup, cleanupFlags, cleanupActiveFlag);
// Append the prepared cleanup prologue from above.
assert(!cir::MissingFeatures::cleanupAppendInsts());
// V. Set up the fallthrough edge out.
// Case 1: a fallthrough source exists but doesn't branch to the
// cleanup because the cleanup is inactive.
if (!hasFallthrough && fallthroughSource) {
// Prebranched fallthrough was forwarded earlier.
// Non-prebranched fallthrough doesn't need to be forwarded.
// Either way, all we need to do is restore the IP we cleared before.
assert(!isActive);
cgm.errorNYI("cleanup inactive fallthrough");
// Case 2: a fallthrough source exists and should branch to the
// cleanup, but we're not supposed to branch through to the next
// cleanup.
} else if (hasFallthrough && fallthroughDest) {
cgm.errorNYI("cleanup fallthrough destination");
// Case 3: a fallthrough source exists and should branch to the
// cleanup and then through to the next.
} else if (hasFallthrough) {
// Everything is already set up for this.
// Case 4: no fallthrough source exists.
} else {
// FIXME(cir): should we clear insertion point here?
}
// VI. Assorted cleaning.
// Check whether we can merge NormalEntry into a single predecessor.
// This might invalidate (non-IR) pointers to NormalEntry.
//
// If it did invalidate those pointers, and normalEntry was the same
// as NormalExit, go back and patch up the fixups.
assert(!cir::MissingFeatures::simplifyCleanupEntry());
}
}
/// Pops cleanup blocks until the given savepoint is reached.
void CIRGenFunction::popCleanupBlocks(
EHScopeStack::stable_iterator oldCleanupStackDepth,
ArrayRef<mlir::Value *> valuesToReload) {
// If the current stack depth is the same as the cleanup stack depth,
// we won't be exiting any cleanup scopes, so we don't need to reload
// any values.
bool requiresCleanup = false;
for (auto it = ehStack.begin(), ie = ehStack.find(oldCleanupStackDepth);
it != ie; ++it) {
if (isa<EHCleanupScope>(&*it)) {
requiresCleanup = true;
break;
}
}
// If there are values that we need to keep live, spill them now before
// we pop the cleanup blocks. These are passed as pointers to mlir::Value
// because we're going to replace them with the reloaded value.
SmallVector<Address> tempAllocas;
if (requiresCleanup) {
for (mlir::Value *valPtr : valuesToReload) {
mlir::Value val = *valPtr;
if (!val)
continue;
// TODO(cir): Check for static allocas.
Address temp = createDefaultAlignTempAlloca(val.getType(), val.getLoc(),
"tmp.exprcleanup");
tempAllocas.push_back(temp);
builder.createStore(val.getLoc(), val, temp);
}
}
// Pop cleanup blocks until we reach the base stack depth for the
// current scope.
while (ehStack.stable_begin() != oldCleanupStackDepth)
popCleanupBlock();
// Reload the values that we spilled, if necessary.
if (requiresCleanup) {
for (auto [addr, valPtr] : llvm::zip(tempAllocas, valuesToReload)) {
mlir::Location loc = valPtr->getLoc();
*valPtr = builder.createLoad(loc, addr);
}
}
}
/// 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);
}