[clang] 6ca5490 - [CIR] Implement initial flattening of cleanup scopes with EH (#182636)
via cfe-commits
cfe-commits at lists.llvm.org
Mon Feb 23 12:54:45 PST 2026
Author: Andy Kaylor
Date: 2026-02-23T20:54:40Z
New Revision: 6ca5490b31e83852e83460520e9df135d750cc69
URL: https://github.com/llvm/llvm-project/commit/6ca5490b31e83852e83460520e9df135d750cc69
DIFF: https://github.com/llvm/llvm-project/commit/6ca5490b31e83852e83460520e9df135d750cc69.diff
LOG: [CIR] Implement initial flattening of cleanup scopes with EH (#182636)
This implements flattening of cir.cleanup_scope operations that require
exception handling. Calls within the cleanup scope body that may throw
an exception are replaced by cir.try_call operations that unwind to an
exception handling block that executes the operations in the cleanup
scope's cleanup region and then unwinds to the caller using a cir.resume
operation.
If the cleanup scope is nested within a try operation, the flattening of
the try op will be responsible for updating the resume operation to flow
through its dispatch block. However, that is not yet implemented, so
only the case of a try op with no handlers is accepted after this PR.
Flattening of cleanup scopes that require exception handling nested
within other cleanup scopes is not yet implemented.
Substantial amounts of this PR were created using agentic AI tools, but
I have carefully reviewed the code, comments, and tests and made changes
as needed.
Added:
clang/test/CIR/Transforms/flatten-cleanup-scope-eh.cir
Modified:
clang/include/clang/CIR/Dialect/IR/CIROps.td
clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir
Removed:
################################################################################
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 99d0d7e457b50..09b5ab5197a32 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -1021,24 +1021,42 @@ def CIR_ContinueOp : CIR_Op<"continue", [Terminator]> {
//===----------------------------------------------------------------------===//
def CIR_ResumeOp : CIR_Op<"resume", [
- ReturnLike, Terminator, HasParent<"cir::TryOp">
+ ReturnLike, Terminator,
+ ParentOneOf<["cir::TryOp", "cir::CleanupScopeOp", "cir::FuncOp"]>
]> {
let summary = "Resumes execution after not catching exceptions";
let description = [{
The `cir.resume` operation handles an uncaught exception scenario.
- Used as the terminator of a `CatchUnwind` region of `cir.try`, where it
- does not receive any arguments (implied from the `cir.try` scope).
+ Before CFG flattening, this operation is used as the terminator of a
+ `CatchUnwind` region of `cir.try`, where it does not receive any arguments
+ (implied from the `cir.try` scope).
- This operation is used only before the CFG flatterning pass.
+ During CFG flattening, this operartion may appear within a `cir.try`
+ operation where it indicates that exception unwinding should continue
+ from that point. When the `cir.try` operation is flattened, the resume
+ operation will be replaced with a branch to the flattened try operation's
+ dispatch block.
+
+ After CFG flattening, this operation appears at the function level (inside
+ `cir.func`) to indicate that the exception should be re-thrown to the
+ caller after cleanup code has been executed.
Examples:
```mlir
+ // Before CFG flattening (in try unwind region)
cir.try {
cir.yield
} unwind {
cir.resume
}
+
+ // After CFG flattening (at function level, after cleanup)
+ ^eh_cleanup(%eh_token : !cir.eh_token):
+ %ct = cir.begin_cleanup %eh_token : !cir.eh_token -> !cir.cleanup_token
+ cir.call @destructor() : () -> ()
+ cir.end_cleanup %ct : !cir.cleanup_token
+ cir.resume
```
}];
diff --git a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
index 4845c1d9c76c9..617acd47599bf 100644
--- a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
@@ -883,6 +883,201 @@ class CIRCleanupScopeOpFlattening
});
}
+ // Collect all function calls in the cleanup scope body that may throw
+ // exceptions and need to be replaced with try_call operations. Skips calls
+ // that are marked nothrow and calls inside nested TryOps (the latter will be
+ // handled by the TryOp's own flattening).
+ void collectThrowingCalls(
+ mlir::Region &bodyRegion,
+ llvm::SmallVectorImpl<cir::CallOp> &callsToRewrite) const {
+ bodyRegion.walk<mlir::WalkOrder::PreOrder>([&](mlir::Operation *op) {
+ // Skip calls inside nested TryOps - those are handled by TryOp
+ // flattening.
+ if (isa<cir::TryOp>(op))
+ return mlir::WalkResult::skip();
+
+ if (auto callOp = dyn_cast<cir::CallOp>(op)) {
+ if (!callOp.getNothrow())
+ callsToRewrite.push_back(callOp);
+ }
+ return mlir::WalkResult::advance();
+ });
+ }
+
+#ifndef NDEBUG
+ // Check that no block other than the last one in a region exits the region.
+ static bool regionExitsOnlyFromLastBlock(mlir::Region ®ion) {
+ for (mlir::Block &block : region) {
+ if (&block == ®ion.back())
+ continue;
+ bool expectedTerminator =
+ llvm::TypeSwitch<mlir::Operation *, bool>(block.getTerminator())
+ // It is theoretically possible to have a cleanup block with
+ // any of the following exits in non-final blocks, but we won't
+ // currently generate any CIR that does that, and being able to
+ // assume that it doesn't happen simplifies the implementation.
+ // If we ever need to handle this case, the code will need to
+ // be updated to handle it.
+ .Case<cir::YieldOp, cir::ReturnOp, cir::ResumeFlatOp,
+ cir::ContinueOp, cir::BreakOp, cir::GotoOp>(
+ [](auto) { return false; })
+ // We expect that call operations have not yet been rewritten
+ // as try_call operations. A call can unwind out of the cleanup
+ // scope, but we will be handling that during flattening. The
+ // only case where a try_call could be present inside an
+ // unflattened cleanup region is if the cleanup contained a
+ // nested try-catch region, and that isn't expected as of the
+ // time of this implementation. If it does, this could be
+ // updated to tolerate it.
+ .Case<cir::TryCallOp>([](auto) { return false; })
+ // Likewise, we don't expect to find an EH dispatch operation
+ // because we weren't expecting try-catch regions nested in the
+ // cleanup region.
+ .Case<cir::EhDispatchOp>([](auto) { return false; })
+ // In theory, it would be possible to have a flattened switch
+ // operation that does not exit the cleanup region. For now,
+ // that's not happening.
+ .Case<cir::SwitchFlatOp>([](auto) { return false; })
+ // These aren't expected either, but if they occur, they don't
+ // exit the region, so that's OK.
+ .Case<cir::UnreachableOp, cir::TrapOp>([](auto) { return true; })
+ // Indirect branches are not expected.
+ .Case<cir::IndirectBrOp>([](auto) { return false; })
+ // We do expect branches, but we don't expect them to leave
+ // the region.
+ .Case<cir::BrOp>([&](cir::BrOp brOp) {
+ assert(brOp.getDest()->getParent() == ®ion &&
+ "branch destination is not in the region");
+ return true;
+ })
+ .Case<cir::BrCondOp>([&](cir::BrCondOp brCondOp) {
+ assert(brCondOp.getDestTrue()->getParent() == ®ion &&
+ "branch destination is not in the region");
+ assert(brCondOp.getDestFalse()->getParent() == ®ion &&
+ "branch destination is not in the region");
+ return true;
+ })
+ // What else could there be?
+ .Default([](mlir::Operation *) -> bool {
+ llvm_unreachable("unexpected terminator in cleanup region");
+ });
+ if (!expectedTerminator)
+ return false;
+ }
+ return true;
+ }
+#endif
+
+ // Build the EH cleanup block structure by cloning the cleanup region. The
+ // cloned entry block gets an !cir.eh_token argument and a cir.begin_cleanup
+ // inserted at the top. All cir.yield terminators that might exit the cleanup
+ // region are replaced with cir.end_cleanup + cir.resume.
+ //
+ // For a single-block cleanup region, this produces:
+ //
+ // ^eh_cleanup(%eh_token : !cir.eh_token):
+ // %ct = cir.begin_cleanup %eh_token : !cir.eh_token -> !cir.cleanup_token
+ // <cloned cleanup operations>
+ // cir.end_cleanup %ct : !cir.cleanup_token
+ // cir.resume
+ //
+ // For a multi-block cleanup region (e.g. containing a flattened cir.if),
+ // the same wrapping is applied around the cloned block structure: the entry
+ // block gets begin_cleanup and all exit blocks (those terminated by yield)
+ // get end_cleanup + resume.
+ //
+ // If this cleanup scope is nested within a TryOp, the resume will be updated
+ // to branch to the catch dispatch block of the enclosing try operation when
+ // the TryOp is flattened.
+ mlir::Block *buildEHCleanupBlocks(cir::CleanupScopeOp cleanupOp,
+ mlir::Location loc,
+ mlir::Block *insertBefore,
+ mlir::PatternRewriter &rewriter) const {
+ assert(regionExitsOnlyFromLastBlock(cleanupOp.getCleanupRegion()) &&
+ "cleanup region has exits in non-final blocks");
+
+ // Track the block before the insertion point so we can find the cloned
+ // blocks after cloning.
+ mlir::Block *blockBeforeClone = insertBefore->getPrevNode();
+
+ // Clone the entire cleanup region before insertBefore.
+ rewriter.cloneRegionBefore(cleanupOp.getCleanupRegion(), insertBefore);
+
+ // Find the first cloned block.
+ mlir::Block *clonedEntry = blockBeforeClone
+ ? blockBeforeClone->getNextNode()
+ : &insertBefore->getParent()->front();
+
+ // Add the eh_token argument to the cloned entry block and insert
+ // begin_cleanup at the top.
+ auto ehTokenType = cir::EhTokenType::get(rewriter.getContext());
+ mlir::Value ehToken = clonedEntry->addArgument(ehTokenType, loc);
+
+ rewriter.setInsertionPointToStart(clonedEntry);
+ auto beginCleanup = cir::BeginCleanupOp::create(rewriter, loc, ehToken);
+
+ // Replace the yield terminator in the last cloned block with
+ // end_cleanup + resume.
+ mlir::Block *lastClonedBlock = insertBefore->getPrevNode();
+ auto yieldOp =
+ mlir::dyn_cast<cir::YieldOp>(lastClonedBlock->getTerminator());
+ if (yieldOp) {
+ rewriter.setInsertionPoint(yieldOp);
+ cir::EndCleanupOp::create(rewriter, loc, beginCleanup.getCleanupToken());
+ rewriter.replaceOpWithNewOp<cir::ResumeOp>(yieldOp);
+ } else {
+ cleanupOp->emitError("Not yet implemented: cleanup region terminated "
+ "with non-yield operation");
+ }
+
+ return clonedEntry;
+ }
+
+ // Create a shared unwind destination block for all calls within the same
+ // cleanup scope. The unwind block contains a cir.eh.initiate operation
+ // (with the cleanup attribute) and a branch to the EH cleanup block.
+ mlir::Block *buildUnwindBlock(mlir::Block *ehCleanupBlock, mlir::Location loc,
+ mlir::Block *insertBefore,
+ mlir::PatternRewriter &rewriter) const {
+ mlir::Block *unwindBlock = rewriter.createBlock(insertBefore);
+ rewriter.setInsertionPointToEnd(unwindBlock);
+ auto ehInitiate =
+ cir::EhInitiateOp::create(rewriter, loc, /*cleanup=*/true);
+ cir::BrOp::create(rewriter, loc, mlir::ValueRange{ehInitiate.getEhToken()},
+ ehCleanupBlock);
+ return unwindBlock;
+ }
+
+ // Replace a cir.call with a cir.try_call that unwinds to the `unwindDest`
+ // block if an exception is thrown.
+ void replaceCallWithTryCall(cir::CallOp callOp, mlir::Block *unwindDest,
+ mlir::Location loc,
+ mlir::PatternRewriter &rewriter) const {
+ mlir::Block *callBlock = callOp->getBlock();
+
+ assert(!callOp.getNothrow() && "call is not expected to throw");
+
+ // Split the block after the call - remaining ops become the normal
+ // destination.
+ mlir::Block *normalDest =
+ rewriter.splitBlock(callBlock, std::next(callOp->getIterator()));
+
+ // Build the try_call to replace the original call.
+ rewriter.setInsertionPoint(callOp);
+ mlir::Type resType = callOp->getNumResults() > 0
+ ? callOp->getResult(0).getType()
+ : mlir::Type();
+ auto tryCallOp =
+ cir::TryCallOp::create(rewriter, loc, callOp.getCalleeAttr(), resType,
+ normalDest, unwindDest, callOp.getArgOperands());
+
+ // Replace uses of the call result with the try_call result.
+ if (callOp->getNumResults() > 0)
+ callOp->getResult(0).replaceAllUsesWith(tryCallOp.getResult());
+
+ rewriter.eraseOp(callOp);
+ }
+
// Flatten a cleanup scope. The body region's exits branch to the cleanup
// block, and the cleanup block branches to destination blocks whose contents
// depend on the type of operation that exited the body region. Yield becomes
@@ -897,17 +1092,33 @@ class CIRCleanupScopeOpFlattening
// the destination slot and a branch to cleanup. An operation is appended to
// the to branch to a dispatch block that loads the destination slot and uses
// switch.flat to branch to the correct destination.
- mlir::LogicalResult flattenCleanup(cir::CleanupScopeOp cleanupOp,
- llvm::SmallVectorImpl<CleanupExit> &exits,
- mlir::PatternRewriter &rewriter) const {
+ //
+ // If the cleanup scope requires EH cleanup, any call operations in the body
+ // that may throw are replaced with cir.try_call operations that unwind to an
+ // EH cleanup block. The cleanup block(s) will be terminated with a cir.resume
+ // operation. If this cleanup scope is enclosed by a try operation, the
+ // flattening of the try operation flattening will replace the cir.resume with
+ // a branch to a catch dispatch block. Otherwise, the cir.resume operation
+ // remains in place and will unwind to the caller.
+ mlir::LogicalResult
+ flattenCleanup(cir::CleanupScopeOp cleanupOp,
+ llvm::SmallVectorImpl<CleanupExit> &exits,
+ llvm::SmallVectorImpl<cir::CallOp> &callsToRewrite,
+ mlir::PatternRewriter &rewriter) const {
mlir::Location loc = cleanupOp.getLoc();
+ cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind();
+ bool hasNormalCleanup = cleanupKind == cir::CleanupKind::Normal ||
+ cleanupKind == cir::CleanupKind::All;
+ bool hasEHCleanup = cleanupKind == cir::CleanupKind::EH ||
+ cleanupKind == cir::CleanupKind::All;
bool isMultiExit = exits.size() > 1;
// Get references to region blocks before inlining.
mlir::Block *bodyEntry = &cleanupOp.getBodyRegion().front();
mlir::Block *cleanupEntry = &cleanupOp.getCleanupRegion().front();
mlir::Block *cleanupExit = &cleanupOp.getCleanupRegion().back();
-
+ assert(regionExitsOnlyFromLastBlock(cleanupOp.getCleanupRegion()) &&
+ "cleanup region has exits in non-final blocks");
auto cleanupYield = dyn_cast<cir::YieldOp>(cleanupExit->getTerminator());
if (!cleanupYield) {
return rewriter.notifyMatchFailure(cleanupOp,
@@ -915,10 +1126,12 @@ class CIRCleanupScopeOpFlattening
"terminated with non-yield operation");
}
- // For multiple exits, get or create a destination slot at function entry.
- // The slot is shared across all cleanup scopes in the function.
+ // For multiple exits from the body region, get or create a destination slot
+ // at function entry. The slot is shared across all cleanup scopes in the
+ // function. This is only needed if the cleanup scope requires normal
+ // cleanup.
cir::AllocaOp destSlot;
- if (isMultiExit) {
+ if (isMultiExit && hasNormalCleanup) {
auto funcOp = cleanupOp->getParentOfType<cir::FuncOp>();
if (!funcOp)
return cleanupOp->emitError("cleanup scope not inside a function");
@@ -930,93 +1143,135 @@ class CIRCleanupScopeOpFlattening
mlir::Block *continueBlock =
rewriter.splitBlock(currentBlock, rewriter.getInsertionPoint());
+ // Build EH cleanup blocks if needed. This must be done before inlining
+ // the cleanup region since buildEHCleanupBlocks clones from it. The unwind
+ // block is inserted before the EH cleanup entry so that the final layout
+ // is: body -> normal cleanup -> exit ->unwind -> EH cleanup -> continue.
+ // If there are no throwing calls, we don't need to EH cleanup blocks.
+ mlir::Block *unwindBlock = nullptr;
+ mlir::Block *ehCleanupEntry = nullptr;
+ if (hasEHCleanup && !callsToRewrite.empty()) {
+ ehCleanupEntry =
+ buildEHCleanupBlocks(cleanupOp, loc, continueBlock, rewriter);
+ unwindBlock =
+ buildUnwindBlock(ehCleanupEntry, loc, ehCleanupEntry, rewriter);
+ }
+
+ // All normal flow blocks are inserted before this point — either before
+ // the unwind block (if EH cleanup exists) or before the continue block.
+ mlir::Block *normalInsertPt = unwindBlock ? unwindBlock : continueBlock;
+
// Inline the body region.
- rewriter.inlineRegionBefore(cleanupOp.getBodyRegion(), continueBlock);
+ rewriter.inlineRegionBefore(cleanupOp.getBodyRegion(), normalInsertPt);
- // Inline the cleanup region after the body.
- rewriter.inlineRegionBefore(cleanupOp.getCleanupRegion(), continueBlock);
+ // Inline the cleanup region for the normal cleanup path.
+ if (hasNormalCleanup)
+ rewriter.inlineRegionBefore(cleanupOp.getCleanupRegion(), normalInsertPt);
// Branch from current block to body entry.
rewriter.setInsertionPointToEnd(currentBlock);
cir::BrOp::create(rewriter, loc, bodyEntry);
- // Create the exit/dispatch block (after cleanup, before continue).
- mlir::Block *exitBlock = rewriter.createBlock(continueBlock);
+ // Handle normal exits.
+ mlir::LogicalResult result = mlir::success();
+ if (hasNormalCleanup) {
+ // Create the exit/dispatch block (after cleanup, before continue).
+ mlir::Block *exitBlock = rewriter.createBlock(normalInsertPt);
- // Rewrite the cleanup region's yield to branch to exit block.
- rewriter.setInsertionPoint(cleanupYield);
- rewriter.replaceOpWithNewOp<cir::BrOp>(cleanupYield, exitBlock);
+ // Rewrite the cleanup region's yield to branch to exit block.
+ rewriter.setInsertionPoint(cleanupYield);
+ rewriter.replaceOpWithNewOp<cir::BrOp>(cleanupYield, exitBlock);
- mlir::LogicalResult result = mlir::success();
- if (isMultiExit) {
- // Build the dispatch switch in the exit block.
- rewriter.setInsertionPointToEnd(exitBlock);
-
- // Load the destination slot value.
- auto slotValue = cir::LoadOp::create(
- rewriter, loc, destSlot, /*isDeref=*/false,
- /*isVolatile=*/false, /*alignment=*/mlir::IntegerAttr(),
- cir::SyncScopeKindAttr(), cir::MemOrderAttr());
-
- // Create destination blocks for each exit and collect switch case info.
- llvm::SmallVector<mlir::APInt, 8> caseValues;
- llvm::SmallVector<mlir::Block *, 8> caseDestinations;
- llvm::SmallVector<mlir::ValueRange, 8> caseOperands;
- cir::IntType s32Type =
- cir::IntType::get(rewriter.getContext(), 32, /*isSigned=*/true);
-
- for (const CleanupExit &exit : exits) {
- // Create a block for this destination.
- mlir::Block *destBlock = rewriter.createBlock(continueBlock);
- rewriter.setInsertionPointToEnd(destBlock);
- result =
- createExitTerminator(exit.exitOp, loc, continueBlock, rewriter);
-
- // Add to switch cases.
- caseValues.push_back(
- llvm::APInt(32, static_cast<uint64_t>(exit.destinationId), true));
- caseDestinations.push_back(destBlock);
- caseOperands.push_back(mlir::ValueRange());
-
- // Replace the original exit op with: store dest ID, branch to cleanup.
- rewriter.setInsertionPoint(exit.exitOp);
- auto destIdConst = cir::ConstantOp::create(
- rewriter, loc, cir::IntAttr::get(s32Type, exit.destinationId));
- cir::StoreOp::create(rewriter, loc, destIdConst, destSlot,
- /*isVolatile=*/false,
- /*alignment=*/mlir::IntegerAttr(),
- cir::SyncScopeKindAttr(), cir::MemOrderAttr());
- rewriter.replaceOpWithNewOp<cir::BrOp>(exit.exitOp, cleanupEntry);
-
- // If the exit terminator creation failed, we're going to end up with
- // partially flattened code, but we'll also have reported an error so
- // that's OK. We need to finish out this function to keep the IR in a
- // valid state to help diagnose the error. This is a temporary
- // possibility during development. It shouldn't ever happen after the
- // implementation is complete.
- if (result.failed())
- break;
- }
+ if (isMultiExit) {
+ // Build the dispatch switch in the exit block.
+ rewriter.setInsertionPointToEnd(exitBlock);
+
+ // Load the destination slot value.
+ auto slotValue = cir::LoadOp::create(
+ rewriter, loc, destSlot, /*isDeref=*/false,
+ /*isVolatile=*/false, /*alignment=*/mlir::IntegerAttr(),
+ cir::SyncScopeKindAttr(), cir::MemOrderAttr());
+
+ // Create destination blocks for each exit and collect switch case info.
+ llvm::SmallVector<mlir::APInt, 8> caseValues;
+ llvm::SmallVector<mlir::Block *, 8> caseDestinations;
+ llvm::SmallVector<mlir::ValueRange, 8> caseOperands;
+ cir::IntType s32Type =
+ cir::IntType::get(rewriter.getContext(), 32, /*isSigned=*/true);
+
+ for (const CleanupExit &exit : exits) {
+ // Create a block for this destination.
+ mlir::Block *destBlock = rewriter.createBlock(normalInsertPt);
+ rewriter.setInsertionPointToEnd(destBlock);
+ result =
+ createExitTerminator(exit.exitOp, loc, continueBlock, rewriter);
+
+ // Add to switch cases.
+ caseValues.push_back(
+ llvm::APInt(32, static_cast<uint64_t>(exit.destinationId), true));
+ caseDestinations.push_back(destBlock);
+ caseOperands.push_back(mlir::ValueRange());
+
+ // Replace the original exit op with: store dest ID, branch to
+ // cleanup.
+ rewriter.setInsertionPoint(exit.exitOp);
+ auto destIdConst = cir::ConstantOp::create(
+ rewriter, loc, cir::IntAttr::get(s32Type, exit.destinationId));
+ cir::StoreOp::create(rewriter, loc, destIdConst, destSlot,
+ /*isVolatile=*/false,
+ /*alignment=*/mlir::IntegerAttr(),
+ cir::SyncScopeKindAttr(), cir::MemOrderAttr());
+ rewriter.replaceOpWithNewOp<cir::BrOp>(exit.exitOp, cleanupEntry);
+
+ // If the exit terminator creation failed, we're going to end up with
+ // partially flattened code, but we'll also have reported an error so
+ // that's OK. We need to finish out this function to keep the IR in a
+ // valid state to help diagnose the error. This is a temporary
+ // possibility during development. It shouldn't ever happen after the
+ // implementation is complete.
+ if (result.failed())
+ break;
+ }
- // Create the default destination (unreachable).
- mlir::Block *defaultBlock = rewriter.createBlock(continueBlock);
- rewriter.setInsertionPointToEnd(defaultBlock);
- cir::UnreachableOp::create(rewriter, loc);
+ // Create the default destination (unreachable).
+ mlir::Block *defaultBlock = rewriter.createBlock(normalInsertPt);
+ rewriter.setInsertionPointToEnd(defaultBlock);
+ cir::UnreachableOp::create(rewriter, loc);
- // Build the switch.flat operation in the exit block.
- rewriter.setInsertionPointToEnd(exitBlock);
- cir::SwitchFlatOp::create(rewriter, loc, slotValue, defaultBlock,
- mlir::ValueRange(), caseValues,
- caseDestinations, caseOperands);
+ // Build the switch.flat operation in the exit block.
+ rewriter.setInsertionPointToEnd(exitBlock);
+ cir::SwitchFlatOp::create(rewriter, loc, slotValue, defaultBlock,
+ mlir::ValueRange(), caseValues,
+ caseDestinations, caseOperands);
+ } else {
+ // Single exit: put the appropriate terminator directly in the exit
+ // block.
+ rewriter.setInsertionPointToEnd(exitBlock);
+ mlir::Operation *exitOp = exits[0].exitOp;
+ result = createExitTerminator(exitOp, loc, continueBlock, rewriter);
+
+ // Replace body exit with branch to cleanup entry.
+ rewriter.setInsertionPoint(exitOp);
+ rewriter.replaceOpWithNewOp<cir::BrOp>(exitOp, cleanupEntry);
+ }
} else {
- // Single exit: put the appropriate terminator directly in the exit block.
- rewriter.setInsertionPointToEnd(exitBlock);
- mlir::Operation *exitOp = exits[0].exitOp;
- result = createExitTerminator(exitOp, loc, continueBlock, rewriter);
-
- // Replace body exit with branch to cleanup entry.
- rewriter.setInsertionPoint(exitOp);
- rewriter.replaceOpWithNewOp<cir::BrOp>(exitOp, cleanupEntry);
+ // EH-only cleanup: normal exits skip the cleanup entirely.
+ // Replace yield exits with branches to the continue block.
+ for (CleanupExit &exit : exits) {
+ if (isa<cir::YieldOp>(exit.exitOp)) {
+ rewriter.setInsertionPoint(exit.exitOp);
+ rewriter.replaceOpWithNewOp<cir::BrOp>(exit.exitOp, continueBlock);
+ }
+ // Non-yield exits (break, continue, return) stay as-is since no normal
+ // cleanup is needed.
+ }
+ }
+
+ // Replace non-nothrow calls with try_call operations. All calls within
+ // this cleanup scope share the same unwind destination.
+ if (hasEHCleanup) {
+ for (cir::CallOp callOp : callsToRewrite)
+ replaceCallWithTryCall(callOp, unwindBlock, loc, rewriter);
}
// Erase the original cleanup scope op.
@@ -1040,11 +1295,27 @@ class CIRCleanupScopeOpFlattening
if (hasNestedCleanup)
return mlir::failure();
- // Only handle normal cleanups for now - EH and "all" cleanups are NYI.
cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind();
- if (cleanupKind != cir::CleanupKind::Normal)
- return cleanupOp->emitError(
- "EH cleanup flattening is not yet implemented");
+
+ // EH cleanups nested inside another cleanup scope are not yet supported
+ // because the inner EH unwind path must chain through the outer cleanup
+ // before unwinding to the caller.
+ if (cleanupKind != cir::CleanupKind::Normal) {
+ if (cleanupOp->getParentOfType<cir::CleanupScopeOp>())
+ return cleanupOp->emitError(
+ "nested EH cleanup scope flattening is not yet implemented");
+ }
+
+ // Throwing calls in the cleanup region of an EH-enabled cleanup scope
+ // are not yet supported. Such calls would need their own EH handling
+ // (e.g., terminate or nested cleanup) during the unwind path.
+ if (cleanupKind != cir::CleanupKind::Normal) {
+ llvm::SmallVector<cir::CallOp> cleanupThrowingCalls;
+ collectThrowingCalls(cleanupOp.getCleanupRegion(), cleanupThrowingCalls);
+ if (!cleanupThrowingCalls.empty())
+ return cleanupOp->emitError(
+ "throwing calls in cleanup region are not yet implemented");
+ }
// Collect all exits from the body region.
llvm::SmallVector<CleanupExit> exits;
@@ -1053,7 +1324,14 @@ class CIRCleanupScopeOpFlattening
assert(!exits.empty() && "cleanup scope body has no exit");
- return flattenCleanup(cleanupOp, exits, rewriter);
+ // Collect non-nothrow calls that need to be converted to try_call.
+ // This is only needed for EH and All cleanup kinds, but the vector
+ // will simply be empty for Normal cleanup.
+ llvm::SmallVector<cir::CallOp> callsToRewrite;
+ if (cleanupKind != cir::CleanupKind::Normal)
+ collectThrowingCalls(cleanupOp.getBodyRegion(), callsToRewrite);
+
+ return flattenCleanup(cleanupOp, exits, callsToRewrite, rewriter);
}
};
@@ -1104,6 +1382,22 @@ class CIRTryOpFlattening : public mlir::OpRewritePattern<cir::TryOp> {
mlir::LogicalResult
matchAndRewrite(cir::TryOp tryOp,
mlir::PatternRewriter &rewriter) const override {
+ // Cleanup scopes must be lowered before the enclosing try so that
+ // EH cleanup inside them is properly handled.
+ // Fail the match so the pattern rewriter will process cleanup scopes first.
+ bool hasNestedCleanup = tryOp
+ ->walk([&](cir::CleanupScopeOp) {
+ return mlir::WalkResult::interrupt();
+ })
+ .wasInterrupted();
+ if (hasNestedCleanup)
+ return mlir::failure();
+
+ mlir::ArrayAttr handlers = tryOp.getHandlerTypesAttr();
+ if (handlers && !handlers.empty())
+ return tryOp->emitError(
+ "TryOp flattening with handlers is not yet implemented");
+
mlir::OpBuilder::InsertionGuard guard(rewriter);
mlir::Block *afterBody = &tryOp.getTryRegion().back();
diff --git a/clang/test/CIR/Transforms/flatten-cleanup-scope-eh.cir b/clang/test/CIR/Transforms/flatten-cleanup-scope-eh.cir
new file mode 100644
index 0000000000000..2d211dc56065c
--- /dev/null
+++ b/clang/test/CIR/Transforms/flatten-cleanup-scope-eh.cir
@@ -0,0 +1,383 @@
+// RUN: cir-opt %s -cir-flatten-cfg -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s
+
+!s32i = !cir.int<s, 32>
+!u8i = !cir.int<u, 8>
+!rec_SomeClass = !cir.record<struct "SomeClass" {!s32i}>
+!rec_NonTrivial = !cir.record<struct "NonTrivial" padded {!u8i}>
+#false = #cir.bool<false> : !cir.bool
+#true = #cir.bool<true> : !cir.bool
+
+// Test EH-only cleanup flattening. Normal exits skip the cleanup,
+// exception-throwing calls are replaced with try_call and unwind to an
+// EH cleanup block that performs the cleanup and resumes unwinding.
+cir.func @test_eh_only_cleanup() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+
+// CHECK-LABEL: cir.func @test_eh_only_cleanup()
+// CHECK: %[[ALLOCA:.*]] = cir.alloca !rec_SomeClass
+// CHECK: cir.call @ctor(%[[ALLOCA]])
+// CHECK: cir.br ^[[BODY:bb[0-9]+]]
+//
+// Body: the call is replaced with try_call.
+// CHECK: ^[[BODY]]:
+// CHECK: cir.try_call @doSomething(%[[ALLOCA]]) ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+// CHECK: ^[[NORMAL]]:
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// Unwind block.
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH_TOKEN:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH_TOKEN]] : !cir.eh_token)
+//
+// EH cleanup block.
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: cir.call @dtor(%[[ALLOCA]]) nothrow
+// CHECK: cir.end_cleanup %[[CT]] : !cir.cleanup_token
+// CHECK: cir.resume
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: cir.return
+
+// Test cleanup kind "all" flattening. Normal exits go through the normal
+// cleanup path, and exception-throwing calls unwind to an EH cleanup block.
+cir.func @test_all_cleanup() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+
+// CHECK-LABEL: cir.func @test_all_cleanup()
+// CHECK: %[[ALLOCA:.*]] = cir.alloca !rec_SomeClass
+// CHECK: cir.call @ctor(%[[ALLOCA]])
+// CHECK: cir.br ^[[BODY:bb[0-9]+]]
+//
+// Body with try_call for the throwing call.
+// CHECK: ^[[BODY]]:
+// CHECK: cir.try_call @doSomething(%[[ALLOCA]]) ^[[NORMAL_BODY:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+//
+// Normal path: yield branches to normal cleanup.
+// CHECK: ^[[NORMAL_BODY]]:
+// CHECK: cir.br ^[[NORMAL_CLEANUP:bb[0-9]+]]
+// CHECK: ^[[NORMAL_CLEANUP]]:
+// CHECK: cir.call @dtor(%[[ALLOCA]]) nothrow
+// CHECK: cir.br ^[[EXIT:bb[0-9]+]]
+// CHECK: ^[[EXIT]]:
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// Unwind block.
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH_TOKEN:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH_TOKEN]] : !cir.eh_token)
+//
+// EH cleanup block (cloned from cleanup region).
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: cir.call @dtor(%[[ALLOCA]]) nothrow
+// CHECK: cir.end_cleanup %[[CT]] : !cir.cleanup_token
+// CHECK: cir.resume
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: cir.return
+
+// Test EH cleanup with multiple calls that may throw. Each call becomes a
+// try_call, and all unwind to the same shared unwind block.
+cir.func @test_eh_cleanup_multiple_calls() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.call @doSomethingElse(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+
+// CHECK-LABEL: cir.func @test_eh_cleanup_multiple_calls()
+// CHECK: %[[ALLOCA:.*]] = cir.alloca !rec_SomeClass
+// CHECK: cir.call @ctor(%[[ALLOCA]])
+// CHECK: cir.br ^[[BODY:bb[0-9]+]]
+//
+// First call replaced with try_call, unwinding to the shared block.
+// CHECK: ^[[BODY]]:
+// CHECK: cir.try_call @doSomething(%[[ALLOCA]]) ^[[AFTER_FIRST:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+//
+// Second call also replaced with try_call, unwinding to the same block.
+// CHECK: ^[[AFTER_FIRST]]:
+// CHECK: cir.try_call @doSomethingElse(%[[ALLOCA]]) ^[[AFTER_SECOND:bb[0-9]+]], ^[[UNWIND]]
+//
+// CHECK: ^[[AFTER_SECOND]]:
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// Shared unwind block -- both try_calls unwind here.
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH]] : !cir.eh_token)
+//
+// EH cleanup block.
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: cir.call @dtor(%[[ALLOCA]]) nothrow
+// CHECK: cir.end_cleanup %[[CT]] : !cir.cleanup_token
+// CHECK: cir.resume
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: cir.return
+
+// Test that nothrow calls are NOT replaced with try_call.
+cir.func @test_eh_cleanup_nothrow_call() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.call @nothrowFunc(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+
+// CHECK-LABEL: cir.func @test_eh_cleanup_nothrow_call()
+// CHECK: %[[ALLOCA:.*]] = cir.alloca !rec_SomeClass
+// CHECK: cir.call @ctor(%[[ALLOCA]])
+// CHECK: cir.br ^[[BODY:bb[0-9]+]]
+//
+// Only the throwing call becomes try_call.
+// CHECK: ^[[BODY]]:
+// CHECK: cir.try_call @doSomething(%[[ALLOCA]]) ^[[AFTER_FIRST:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+//
+// The nothrow call remains as a regular cir.call, not try_call.
+// CHECK: ^[[AFTER_FIRST]]:
+// CHECK: cir.call @nothrowFunc(%[[ALLOCA]]) nothrow
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH]] : !cir.eh_token)
+//
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: cir.call @dtor(%[[ALLOCA]]) nothrow
+// CHECK: cir.end_cleanup %[[CT]] : !cir.cleanup_token
+// CHECK: cir.resume
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: cir.return
+
+// Test EH cleanup with a call that returns a value.
+cir.func @test_eh_cleanup_call_with_result() -> !s32i {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ %1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["__retval"] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.cleanup.scope {
+ %2 = cir.call @get() : () -> !s32i
+ cir.store %2, %1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ %3 = cir.load %1 : !cir.ptr<!s32i>, !s32i
+ cir.return %3 : !s32i
+}
+
+// CHECK-LABEL: cir.func @test_eh_cleanup_call_with_result()
+// CHECK: %[[ALLOCA_CLASS:.*]] = cir.alloca !rec_SomeClass
+// CHECK: %[[ALLOCA_RET:.*]] = cir.alloca !s32i
+// CHECK: cir.call @ctor(%[[ALLOCA_CLASS]])
+// CHECK: cir.br ^[[BODY:bb[0-9]+]]
+//
+// The try_call returns a result that is used by subsequent operations.
+// CHECK: ^[[BODY]]:
+// CHECK: %[[RESULT:.*]] = cir.try_call @get() ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+// CHECK: ^[[NORMAL]]:
+// CHECK: cir.store %[[RESULT]], %[[ALLOCA_RET]]
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH]] : !cir.eh_token)
+//
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %{{.*}} = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: cir.call @dtor(%[[ALLOCA_CLASS]]) nothrow
+// CHECK: cir.end_cleanup
+// CHECK: cir.resume
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: %[[RETVAL:.*]] = cir.load %[[ALLOCA_RET]]
+// CHECK: cir.return %[[RETVAL]]
+
+// Test NRVO pattern: cleanup all with cir.if in the cleanup region.
+// The cir.if gets flattened first, creating a multi-block cleanup region.
+// Then the cleanup scope flattening must handle cloning the multi-block
+// cleanup region for the EH path.
+cir.func @test_nrvo() -> !rec_NonTrivial {
+ %0 = cir.alloca !rec_NonTrivial, !cir.ptr<!rec_NonTrivial>, ["__retval"] {alignment = 1 : i64}
+ %1 = cir.alloca !cir.bool, !cir.ptr<!cir.bool>, ["nrvo"] {alignment = 1 : i64}
+ cir.cleanup.scope {
+ %2 = cir.const #false
+ cir.store align(1) %2, %1 : !cir.bool, !cir.ptr<!cir.bool>
+ cir.call @_Z10maybeThrowv() : () -> ()
+ %3 = cir.const #true
+ cir.store %3, %1 : !cir.bool, !cir.ptr<!cir.bool>
+ cir.yield
+ } cleanup all {
+ %4 = cir.load align(1) %1 : !cir.ptr<!cir.bool>, !cir.bool
+ %5 = cir.unary(not, %4) : !cir.bool, !cir.bool
+ cir.if %5 {
+ cir.call @_ZN10NonTrivialD1Ev(%0) nothrow : (!cir.ptr<!rec_NonTrivial>) -> ()
+ }
+ cir.yield
+ }
+ %6 = cir.load %0 : !cir.ptr<!rec_NonTrivial>, !rec_NonTrivial
+ cir.return %6 : !rec_NonTrivial
+}
+
+// CHECK-LABEL: cir.func @test_nrvo()
+// CHECK: %[[RETVAL:.*]] = cir.alloca !rec_NonTrivial
+// CHECK: %[[NRVO:.*]] = cir.alloca !cir.bool
+// CHECK: cir.br ^[[BODY:bb[0-9]+]]
+//
+// Body: store false to nrvo flag, try_call maybeThrow.
+// CHECK: ^[[BODY]]:
+// CHECK: %[[FALSE:.*]] = cir.const #false
+// CHECK: cir.store align(1) %[[FALSE]], %[[NRVO]]
+// CHECK: cir.try_call @_Z10maybeThrowv() ^[[AFTER_CALL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+//
+// After call: store true to nrvo flag, branch to normal cleanup.
+// CHECK: ^[[AFTER_CALL]]:
+// CHECK: %[[TRUE:.*]] = cir.const #true
+// CHECK: cir.store %[[TRUE]], %[[NRVO]]
+// CHECK: cir.br ^[[NORMAL_CLEANUP:bb[0-9]+]]
+//
+// Normal cleanup: inlined cleanup region with flattened cir.if.
+// CHECK: ^[[NORMAL_CLEANUP]]:
+// CHECK: %[[N_FLAG:.*]] = cir.load align(1) %[[NRVO]] : !cir.ptr<!cir.bool>, !cir.bool
+// CHECK: %[[N_NOT:.*]] = cir.unary(not, %[[N_FLAG]]) : !cir.bool, !cir.bool
+// CHECK: cir.brcond %[[N_NOT]] ^[[N_IF_TRUE:bb[0-9]+]], ^[[N_MERGE:bb[0-9]+]]
+//
+// CHECK: ^[[N_IF_TRUE]]:
+// CHECK: cir.call @_ZN10NonTrivialD1Ev(%[[RETVAL]]) nothrow
+// CHECK: cir.br ^[[N_MERGE]]
+//
+// CHECK: ^[[N_MERGE]]:
+// CHECK: cir.br ^[[EXIT:bb[0-9]+]]
+// CHECK: ^[[EXIT]]:
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// Unwind block.
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH_TOK:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH_TOK]] : !cir.eh_token)
+//
+// EH cleanup entry: multi-block region cloned from the cleanup.
+// The cir.if was flattened into brcond + branches.
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: %[[EH_FLAG:.*]] = cir.load align(1) %[[NRVO]] : !cir.ptr<!cir.bool>, !cir.bool
+// CHECK: %[[EH_NOT:.*]] = cir.unary(not, %[[EH_FLAG]]) : !cir.bool, !cir.bool
+// CHECK: cir.brcond %[[EH_NOT]] ^[[EH_IF_TRUE:bb[0-9]+]], ^[[EH_MERGE:bb[0-9]+]]
+//
+// EH cleanup if-true: call destructor.
+// CHECK: ^[[EH_IF_TRUE]]:
+// CHECK: cir.call @_ZN10NonTrivialD1Ev(%[[RETVAL]]) nothrow
+// CHECK: cir.br ^[[EH_MERGE]]
+//
+// EH cleanup merge: end cleanup and resume unwinding.
+// CHECK: ^[[EH_MERGE]]:
+// CHECK: cir.end_cleanup %[[CT]] : !cir.cleanup_token
+// CHECK: cir.resume
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: %[[RET:.*]] = cir.load %[[RETVAL]]
+// CHECK: cir.return %[[RET]]
+
+
+// Test that we can handle an eh cleanup inside a try op with no handlers.
+// In this case, the unwind from the EH cleanup can be left to unwind to
+// callers.
+cir.func @test_eh_cleanup_in_try() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.try {
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+}
+
+// CHECK: cir.func @test_eh_cleanup_in_try()
+// CHECK: %[[C:.*]] = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init]
+// CHECK: cir.call @ctor(%[[C]])
+// CHECK: cir.br ^[[TRY_BODY:bb[0-9]+]]
+//
+// Try body: the cleanup scope body was inlined.
+// CHECK: ^[[TRY_BODY]]:
+// CHECK: cir.br ^[[CLEANUP_BODY:bb[0-9]+]]
+//
+// Cleanup scope body: the call is rewritten to try_call.
+// CHECK: ^[[CLEANUP_BODY]]:
+// CHECK: cir.try_call @doSomething(%[[C]]) ^[[NORMAL_CONT:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]]
+//
+// Normal continuation from the call.
+// CHECK: ^[[NORMAL_CONT]]:
+// CHECK: cir.br ^[[TRY_EXIT:bb[0-9]+]]
+//
+// Unwind block.
+// CHECK: ^[[UNWIND]]:
+// CHECK: %[[EH_TOK:.*]] = cir.eh.initiate cleanup : !cir.eh_token
+// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[EH_TOK]] : !cir.eh_token)
+//
+// EH cleanup block.
+// CHECK: ^[[EH_CLEANUP]](%[[ET:.*]]: !cir.eh_token):
+// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET]] : !cir.eh_token -> !cir.cleanup_token
+// CHECK: cir.call @dtor(%[[C]]) nothrow
+// CHECK: cir.end_cleanup %[[CT]] : !cir.cleanup_token
+// CHECK: cir.resume
+//
+// Try exit: branches to continue.
+// CHECK: ^[[TRY_EXIT]]:
+// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]]
+//
+// CHECK: ^[[CONTINUE]]:
+// CHECK: cir.return
+
+cir.func private @get() -> !s32i
+cir.func private @ctor(!cir.ptr<!rec_SomeClass>)
+cir.func private @dtor(!cir.ptr<!rec_SomeClass>) attributes {nothrow}
+cir.func private @doSomething(!cir.ptr<!rec_SomeClass>)
+cir.func private @doSomethingElse(!cir.ptr<!rec_SomeClass>)
+cir.func private @nothrowFunc(!cir.ptr<!rec_SomeClass>)
+cir.func private @shouldContinue() -> !cir.bool
+cir.func private @_Z10maybeThrowv()
+cir.func private @_ZN10NonTrivialD1Ev(!cir.ptr<!rec_NonTrivial>)
diff --git a/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir b/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir
index 29c0227ad9172..9cb3ec057d582 100644
--- a/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir
+++ b/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir
@@ -2,31 +2,140 @@
!s32i = !cir.int<s, 32>
!rec_SomeClass = !cir.record<struct "SomeClass" {!s32i}>
+!u8i = !cir.int<u, 8>
+!void = !cir.void
-// Test that we issue a diagnostic for EH-only cleanup scopes.
-cir.func @test_eh_only_cleanup() {
+cir.global "private" constant external @_ZTIi : !cir.ptr<!u8i>
+
+// Test that we issue a diagnostic for throwing calls in an EH cleanup region.
+cir.func @test_eh_cleanup_in_try() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.try {
+ // expected-error @below {{throwing calls in cleanup region are not yet implemented}}
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+}
+
+// Test that we issue a diagnostic for throwing calls in a Normal+EH cleanup region.
+cir.func @test_all_cleanup_in_try() {
%0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
- // expected-error @below {{EH cleanup flattening is not yet implemented}}
+ cir.try {
+ // expected-error @below {{throwing calls in cleanup region are not yet implemented}}
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+}
+
+// Test that we issue a diagnostic for an EH cleanup nested in a try with a
+// catch all handlers.
+cir.func @test_eh_cleanup_in_try_catchall() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ // expected-error @below {{TryOp flattening with handlers is not yet implemented}}
+ cir.try {
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.yield
+ } catch all {
+ %1 = cir.catch_param : !cir.ptr<!void>
+ cir.yield
+ }
+ cir.return
+}
+
+// Test that we issue a diagnostic for an EH cleanup nested in a try with a
+// catch and unwind handlers.
+cir.func @test_eh_cleanup_in_try_catch_unwind() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ %1 = cir.alloca !s32i, !cir.ptr<!s32i>, ["e"] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ // expected-error @below {{TryOp flattening with handlers is not yet implemented}}
+ cir.try {
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%0) nothrow : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.yield
+ } catch [type #cir.global_view<@_ZTIi> : !cir.ptr<!u8i>] {
+ %2 = cir.catch_param : !cir.ptr<!s32i>
+ %3 = cir.load align(4) %2 : !cir.ptr<!s32i>, !s32i
+ cir.store align(4) %3, %1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ } unwind {
+ cir.resume
+ }
+ cir.return
+}
+
+// Test that we issue a diagnostic for nested EH cleanup scopes.
+// The inner EH unwind path must chain through the outer cleanup, which is
+// not yet implemented.
+cir.func @test_nested_eh_cleanup() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c1", init] {alignment = 4 : i64}
+ %1 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c2", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.cleanup.scope {
- cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.call @ctor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
+ // expected-error @below {{nested EH cleanup scope flattening is not yet implemented}}
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
cir.yield
- } cleanup eh {
+ } cleanup normal {
cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.yield
}
cir.return
}
-// Test that we issue a diagnostic for Normal+EH cleanup scopes.
-cir.func @test_all_cleanup() {
- %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+// Test that we issue a diagnostic for nested Normal+EH cleanup scopes.
+// The inner EH unwind path must chain through the outer cleanup, which is
+// not yet implemented.
+cir.func @test_nested_all_cleanup() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c1", init] {alignment = 4 : i64}
+ %1 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c2", init] {alignment = 4 : i64}
cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
- // expected-error @below {{EH cleanup flattening is not yet implemented}}
cir.cleanup.scope {
- cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.call @ctor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
+ // expected-error @below {{nested EH cleanup scope flattening is not yet implemented}}
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
cir.yield
- } cleanup all {
+ } cleanup normal {
cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.yield
}
@@ -86,6 +195,40 @@ cir.func @test_goto_in_nested_cleanup() {
cir.return
}
+// Test that we issue a diagnostic for throwing calls in the cleanup region
+// of an EH cleanup scope.
+cir.func @test_throwing_call_in_eh_cleanup() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ // expected-error @below {{throwing calls in cleanup region are not yet implemented}}
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup eh {
+ // Throwing destructor
+ cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+
+// Test that we issue a diagnostic for throwing calls in the cleanup region
+// of an "all" cleanup scope.
+cir.func @test_throwing_call_in_all_cleanup() {
+ %0 = cir.alloca !rec_SomeClass, !cir.ptr<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
+ cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ // expected-error @below {{throwing calls in cleanup region are not yet implemented}}
+ cir.cleanup.scope {
+ cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ } cleanup all {
+ // Throwing destructor
+ cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+
cir.func private @ctor(!cir.ptr<!rec_SomeClass>)
cir.func private @dtor(!cir.ptr<!rec_SomeClass>)
cir.func private @doSomething(!cir.ptr<!rec_SomeClass>)
More information about the cfe-commits
mailing list