https://github.com/andykaylor updated https://github.com/llvm/llvm-project/pull/199121
>From 2b414b95209e59aa4113e0974380950772055f67 Mon Sep 17 00:00:00 2001 From: Andy Kaylor <[email protected]> Date: Thu, 14 May 2026 17:51:58 -0700 Subject: [PATCH 1/2] [CIR] Handle throwing an exception from a cleanup scope The CIR FlattenCFG pass had been ignoring any ThrowOp that occurred inside a cleanup scope or try operation, which led to the thrown exception not triggering local cleanups and bypassing local catch handlers. This change introduces a new CIR operation, TryThrowOp, which is analagous to the existing TryCallOp. The TryThrowOp (as well as the ThrowOp) will eventually be lowered to a function call, but which function gets called is a target-dependent detail, so we need an abstract operation before EHABI lowering. The Flatten CFG pass replaces any ThrowOp inside a cleanup scope or try operation with a TryThrowOp that has an unreachable normal destination and unwinds to the appropriate cleanup or catch dispatch block. Assisted-by: Cursor / claude-opus-4.7-thinking-xhigh --- clang/include/clang/CIR/Dialect/IR/CIROps.td | 86 ++++++++++++-- .../Dialect/Transforms/CIRTransformUtils.h | 14 +++ clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 26 +++-- .../Dialect/Transforms/CIRTransformUtils.cpp | 48 ++++++++ .../CIR/Dialect/Transforms/EHABILowering.cpp | 103 ++++++++++++++++ .../lib/CIR/Dialect/Transforms/FlattenCFG.cpp | 110 +++++++++++------- .../CodeGen/cleanup-throw-from-cleanup.cpp | 97 +++++++++++++++ clang/test/CIR/CodeGen/try-catch.cpp | 97 +++++++++++++++ 8 files changed, 522 insertions(+), 59 deletions(-) create mode 100644 clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td index ad6b9335ec4b6..ca39d3c7b3b71 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIROps.td +++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td @@ -7045,11 +7045,35 @@ def CIR_VAArgOp : CIR_Op<"va_arg"> { }]; } +//===----------------------------------------------------------------------===// +// ThrowOpBase +//===----------------------------------------------------------------------===// + +// Common base class shared by `cir.throw` and its EH counterpart +// `cir.try_throw`. Both operations carry the same operands and attributes +// (an optional exception pointer, an optional RTTI symbol and an optional +// destructor symbol) and use the same verifier logic; subclasses contribute +// any additional traits, successors and assembly format pieces. +class CIR_ThrowOpBase<string mnemonic, list<Trait> traits = []> + : CIR_Op<mnemonic, traits> { + let arguments = (ins + Optional<CIR_PointerType>:$exception_ptr, + OptionalAttr<FlatSymbolRefAttr>:$type_info, + OptionalAttr<FlatSymbolRefAttr>:$dtor + ); + + let extraClassDeclaration = [{ + bool rethrows() { return getNumOperands() == 0; } + }]; + + let hasVerifier = 1; +} + //===----------------------------------------------------------------------===// // ThrowOp //===----------------------------------------------------------------------===// -def CIR_ThrowOp : CIR_Op<"throw"> { +def CIR_ThrowOp : CIR_ThrowOpBase<"throw"> { let summary = "(Re)Throws an exception"; let description = [{ This operation is equivalent to either __cxa_throw or __cxa_rethrow, @@ -7084,24 +7108,66 @@ def CIR_ThrowOp : CIR_Op<"throw"> { ``` }]; - let arguments = (ins - Optional<CIR_PointerType>:$exception_ptr, - OptionalAttr<FlatSymbolRefAttr>:$type_info, - OptionalAttr<FlatSymbolRefAttr>:$dtor - ); - let assemblyFormat = [{ ($exception_ptr^ `:` type($exception_ptr))? (`,` $type_info^)? (`,` $dtor^)? attr-dict }]; +} - let extraClassDeclaration = [{ - bool rethrows() { return getNumOperands() == 0; } +//===----------------------------------------------------------------------===// +// TryThrowOp +//===----------------------------------------------------------------------===// + +def CIR_TryThrowOp : CIR_ThrowOpBase<"try_throw", [Terminator]> { + let summary = "throw an exception with an unwind destination"; + let description = [{ + Similar to `cir.throw` but acts as a terminator with two destination + blocks: a `normalDest` that should contain a `cir.unreachable` + operation (since a throw never returns) and an `unwindDest` that + receives control when the throw needs to unwind through an enclosing + cleanup or catch handler. This is the EH counterpart of `cir.throw`, + analogous to how `cir.try_call` is the EH counterpart of `cir.call`. + + Like `cir.throw`, the absence of operands means rethrow. With operands, + it carries the same exception pointer, type info, and optional + destructor as `cir.throw`. + + This operation is produced by the FlattenCFG pass for `cir.throw` + operations that appear inside a cleanup scope or try region. It is + later lowered by the EHABI lowering pass to a `cir.try_call` of + `__cxa_throw` (or `__cxa_rethrow`). + + Example: + + ``` + cir.try_throw %exception_addr : !cir.ptr<!s32i>, @_ZTIi + ^normalDest, ^unwindDest + ^normalDest: + cir.unreachable + ^unwindDest: + ... + ``` }]; - let hasVerifier = 1; + let successors = (successor + AnySuccessor:$normalDest, + AnySuccessor:$unwindDest + ); + + let assemblyFormat = [{ + ($exception_ptr^ `:` type($exception_ptr))? + (`,` $type_info^)? + (`,` $dtor^)? + $normalDest `,` $unwindDest + attr-dict + }]; + + // The EHABI lowering pass replaces every cir.try_throw with a cir.try_call + // of __cxa_throw / __cxa_rethrow before the LLVM lowering runs, so no + // direct LLVM lowering pattern is needed for this op. + let hasLLVMLowering = false; } //===----------------------------------------------------------------------===// diff --git a/clang/include/clang/CIR/Dialect/Transforms/CIRTransformUtils.h b/clang/include/clang/CIR/Dialect/Transforms/CIRTransformUtils.h index b132698dfc7d6..b708da61bf01f 100644 --- a/clang/include/clang/CIR/Dialect/Transforms/CIRTransformUtils.h +++ b/clang/include/clang/CIR/Dialect/Transforms/CIRTransformUtils.h @@ -32,6 +32,20 @@ mlir::Block *replaceCallWithTryCall(cir::CallOp callOp, mlir::Block *unwindDest, mlir::Location loc, mlir::RewriterBase &rewriter); +/// Replace a `cir::ThrowOp` with a `cir::TryThrowOp` whose unwind +/// destination is \p unwindDest. The throw's parent block is split +/// immediately after the throw; the resulting suffix block (which should +/// contain the `cir.unreachable` that follows every throw) becomes the +/// try_throw's normal destination and is returned to the caller. +/// +/// All attributes of the original throw other than the operand segment +/// sizes (which `TryThrowOp::create` sets itself) are copied onto the new +/// try_throw, and the original throw is erased. +mlir::Block *replaceThrowWithTryThrow(cir::ThrowOp throwOp, + mlir::Block *unwindDest, + mlir::Location loc, + mlir::RewriterBase &rewriter); + /// Collect ops in blocks that are unreachable from their region's entry, /// appending them to \p ops. Used by CIR passes that drive /// `applyPartialConversion` and need to feed it operations the conversion diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp index 46b3cd5a47935..85095a0a2de07 100644 --- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp @@ -4035,21 +4035,27 @@ ParseResult cir::InlineAsmOp::parse(OpAsmParser &parser, } //===----------------------------------------------------------------------===// -// ThrowOp +// ThrowOp / TryThrowOp //===----------------------------------------------------------------------===// -mlir::LogicalResult cir::ThrowOp::verify() { - // For the no-rethrow version, it must have at least the exception pointer. - if (rethrows()) - return success(); +template <typename ThrowOpTy> +static mlir::LogicalResult verifyThrowOpImpl(ThrowOpTy op) { + if (op.rethrows()) + return mlir::success(); - if (getNumOperands() != 0) { - if (getTypeInfo()) - return success(); - return emitOpError() << "'type_info' symbol attribute missing"; + if (op.getNumOperands() != 0) { + if (op.getTypeInfo()) + return mlir::success(); + return op.emitOpError() << "'type_info' symbol attribute missing"; } - return failure(); + return mlir::failure(); +} + +mlir::LogicalResult cir::ThrowOp::verify() { return verifyThrowOpImpl(*this); } + +mlir::LogicalResult cir::TryThrowOp::verify() { + return verifyThrowOpImpl(*this); } //===----------------------------------------------------------------------===// diff --git a/clang/lib/CIR/Dialect/Transforms/CIRTransformUtils.cpp b/clang/lib/CIR/Dialect/Transforms/CIRTransformUtils.cpp index 91b55c3220245..1653e673859dd 100644 --- a/clang/lib/CIR/Dialect/Transforms/CIRTransformUtils.cpp +++ b/clang/lib/CIR/Dialect/Transforms/CIRTransformUtils.cpp @@ -105,3 +105,51 @@ mlir::Block *cir::replaceCallWithTryCall(cir::CallOp callOp, rewriter.eraseOp(callOp); return normalDest; } + +mlir::Block *cir::replaceThrowWithTryThrow(cir::ThrowOp throwOp, + mlir::Block *unwindDest, + mlir::Location loc, + mlir::RewriterBase &rewriter) { + // The throw never returns, so the try_throw's normal destination is + // literally unreachable. Place it at the end of the parent function + // rather than splitting it out of the throw's block in the middle of + // the normal control flow. + auto funcOp = throwOp->getParentOfType<cir::FuncOp>(); + assert(funcOp && "throw must be inside a function"); + mlir::Region &body = funcOp.getBody(); + + mlir::Block *normalDest; + { + mlir::OpBuilder::InsertionGuard guard(rewriter); + normalDest = rewriter.createBlock(&body, body.end()); + cir::UnreachableOp::create(rewriter, loc); + } + + // Build the try_throw to replace the original throw. + rewriter.setInsertionPoint(throwOp); + auto tryThrowOp = cir::TryThrowOp::create( + rewriter, loc, throwOp.getExceptionPtr(), throwOp.getTypeInfoAttr(), + throwOp.getDtorAttr(), normalDest, unwindDest); + + // Copy any extra attributes from the original throw. The type_info and + // dtor attributes are already set by TryThrowOp::create above. + llvm::StringRef excludedAttrs[] = { + "type_info", + "dtor", + }; + for (mlir::NamedAttribute attr : throwOp->getAttrs()) { + if (llvm::is_contained(excludedAttrs, attr.getName())) + continue; + tryThrowOp->setAttr(attr.getName(), attr.getValue()); + } + + // Erase the throw along with any operations that followed it in its + // parent block (typically a cir.unreachable left over from CIR codegen). + // They must be removed because try_throw is a terminator and a block + // can have only one terminator. + mlir::Block *throwBlock = throwOp->getBlock(); + while (&throwBlock->back() != tryThrowOp) + rewriter.eraseOp(&throwBlock->back()); + + return normalDest; +} diff --git a/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp b/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp index 802740e800d7f..e6c76fed6f78a 100644 --- a/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp +++ b/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp @@ -125,6 +125,8 @@ class ItaniumEHLowering : public EHABILowering { cir::FuncOp endCatchFunc; cir::FuncOp getExceptionPtrFunc; cir::FuncOp clangCallTerminateFunc; + cir::FuncOp cxaThrowFunc; + cir::FuncOp cxaRethrowFunc; DenseMap<mlir::StringAttr, cir::FuncOp> catchCopyThunks; @@ -133,6 +135,8 @@ class ItaniumEHLowering : public EHABILowering { void ensureRuntimeDecls(mlir::Location loc); void ensureClangCallTerminate(mlir::Location loc); + void ensureCxaThrowDecl(mlir::Location loc); + void ensureCxaRethrowDecl(mlir::Location loc); mlir::Block *buildTerminateBlock(cir::FuncOp funcOp, mlir::Location loc); mlir::FailureOr<cir::FuncOp> resolveCatchCopyThunk(cir::ConstructCatchParamOp op); @@ -146,6 +150,7 @@ class ItaniumEHLowering : public EHABILowering { mlir::LogicalResult lowerConstructCatchParam(cir::ConstructCatchParamOp op, mlir::Value exnPtr); void lowerInitCatchParam(cir::InitCatchParamOp op); + mlir::LogicalResult lowerTryThrow(cir::TryThrowOp op); }; /// Lower all EH operations in the module to the Itanium-specific form. @@ -253,6 +258,29 @@ void ItaniumEHLowering::ensureClangCallTerminate(mlir::Location loc) { clangCallTerminateFunc = funcOp; } +/// Ensure the __cxa_throw runtime function is declared in the module. +/// +/// void __cxa_throw(void *exception, void *type_info, void *dtor); +void ItaniumEHLowering::ensureCxaThrowDecl(mlir::Location loc) { + if (cxaThrowFunc) + return; + auto throwFuncTy = cir::FuncType::get({voidPtrType, voidPtrType, voidPtrType}, + voidType, /*isVarArg=*/false); + cxaThrowFunc = + getOrCreateRuntimeFuncDecl(mod, loc, "__cxa_throw", throwFuncTy); +} + +/// Ensure the __cxa_rethrow runtime function is declared in the module. +/// +/// void __cxa_rethrow(); +void ItaniumEHLowering::ensureCxaRethrowDecl(mlir::Location loc) { + if (cxaRethrowFunc) + return; + auto rethrowFuncTy = cir::FuncType::get({}, voidType, /*isVarArg=*/false); + cxaRethrowFunc = + getOrCreateRuntimeFuncDecl(mod, loc, "__cxa_rethrow", rethrowFuncTy); +} + /// Create a terminate landing pad block at the end of the specified function. mlir::Block *ItaniumEHLowering::buildTerminateBlock(cir::FuncOp funcOp, mlir::Location loc) { @@ -330,6 +358,15 @@ mlir::LogicalResult ItaniumEHLowering::lowerFunc(cir::FuncOp funcOp) { for (cir::InitCatchParamOp op : initCatchOps) lowerInitCatchParam(op); + // Lower any cir.try_throw ops in this function to cir.try_call of + // __cxa_throw / __cxa_rethrow. These are produced by FlattenCFG when a + // cir.throw appears inside a cleanup scope or try region. + SmallVector<cir::TryThrowOp> tryThrowOps; + funcOp.walk([&](cir::TryThrowOp op) { tryThrowOps.push_back(op); }); + for (cir::TryThrowOp op : tryThrowOps) + if (mlir::failed(lowerTryThrow(op))) + return mlir::failure(); + return mlir::success(); } @@ -704,6 +741,72 @@ ItaniumEHLowering::lowerConstructCatchParam(cir::ConstructCatchParamOp op, return mlir::success(); } +/// Lower a cir.try_throw to a cir.try_call of __cxa_throw (or +/// __cxa_rethrow for the no-operand rethrow form). Materializes the +/// type_info and dtor pointers from their symbol attributes, bitcasting +/// each to !cir.ptr<!void> as required by the runtime function signature. +mlir::LogicalResult ItaniumEHLowering::lowerTryThrow(cir::TryThrowOp op) { + mlir::Location loc = op.getLoc(); + mlir::Block *normalDest = op.getNormalDest(); + mlir::Block *unwindDest = op.getUnwindDest(); + builder.setInsertionPoint(op); + + if (op.rethrows()) { + ensureCxaRethrowDecl(loc); + cir::TryCallOp::create( + builder, loc, mlir::FlatSymbolRefAttr::get(cxaRethrowFunc), voidType, + normalDest, unwindDest, mlir::ValueRange{}); + op.erase(); + return mlir::success(); + } + + ensureCxaThrowDecl(loc); + + // Bitcast the exception pointer to void* if necessary. + mlir::Value exnPtr = op.getExceptionPtr(); + if (exnPtr.getType() != voidPtrType) + exnPtr = cir::CastOp::create(builder, loc, voidPtrType, + cir::CastKind::bitcast, exnPtr); + + // Materialize the type_info pointer, looking up the typed symbol in the + // module so we get the correct pointer type for cir.get_global, then + // bitcasting to void* to match the runtime signature. + mlir::FlatSymbolRefAttr typeInfoAttr = op.getTypeInfoAttr(); + auto typeInfoGlobal = mod.lookupSymbol<cir::GlobalOp>(typeInfoAttr); + if (!typeInfoGlobal) + return op.emitError("type_info symbol not found in module"); + auto typeInfoPtrTy = cir::PointerType::get(typeInfoGlobal.getSymType()); + mlir::Value typeInfo = cir::GetGlobalOp::create(builder, loc, typeInfoPtrTy, + typeInfoAttr.getValue()); + if (typeInfo.getType() != voidPtrType) + typeInfo = cir::CastOp::create(builder, loc, voidPtrType, + cir::CastKind::bitcast, typeInfo); + + // Materialize the dtor pointer (or null if no dtor). + mlir::Value dtor; + if (mlir::FlatSymbolRefAttr dtorAttr = op.getDtorAttr()) { + auto dtorFunc = mod.lookupSymbol<cir::FuncOp>(dtorAttr); + if (!dtorFunc) + return op.emitError("dtor symbol not found in module"); + auto dtorPtrTy = cir::PointerType::get(dtorFunc.getFunctionType()); + dtor = + cir::GetGlobalOp::create(builder, loc, dtorPtrTy, dtorAttr.getValue()); + if (dtor.getType() != voidPtrType) + dtor = cir::CastOp::create(builder, loc, voidPtrType, + cir::CastKind::bitcast, dtor); + } else { + dtor = cir::ConstantOp::create( + builder, loc, + cir::ConstPtrAttr::get(voidPtrType, builder.getI64IntegerAttr(0))); + } + + cir::TryCallOp::create( + builder, loc, mlir::FlatSymbolRefAttr::get(cxaThrowFunc), voidType, + normalDest, unwindDest, mlir::ValueRange{exnPtr, typeInfo, dtor}); + op.erase(); + return mlir::success(); +} + /// Lower a cir.init_catch_param into the Itanium-specific sequence that /// materializes the catch parameter's local variable from the exception /// pointer returned by __cxa_begin_catch. The shape of the lowering diff --git a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp index a21394dc62332..baab456b652b5 100644 --- a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp +++ b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp @@ -672,6 +672,18 @@ collectThrowingCalls(mlir::Region ®ion, }); } +// Collect all cir.throw operations in a region that need to be replaced +// with cir.try_throw operations so they can unwind through an enclosing +// cleanup or catch handler. Nested cleanup scopes and try ops are always +// flattened before their enclosing parents, so there are no nested +// regions to skip here. +static void +collectThrows(mlir::Region ®ion, + llvm::SmallVectorImpl<cir::ThrowOp> &throwsToRewrite) { + region.walk( + [&](cir::ThrowOp throwOp) { throwsToRewrite.push_back(throwOp); }); +} + // Collect all cir.resume operations in a region that come from // already-flattened try or cleanup scope operations. These resume ops need // to be chained through this scope's EH handler instead of unwinding @@ -1178,6 +1190,7 @@ class CIRCleanupScopeOpFlattening flattenCleanup(cir::CleanupScopeOp cleanupOp, llvm::SmallVectorImpl<CleanupExit> &exits, llvm::SmallVectorImpl<cir::CallOp> &callsToRewrite, + llvm::SmallVectorImpl<cir::ThrowOp> &throwsToRewrite, llvm::SmallVectorImpl<cir::ResumeOp> &resumeOpsToChain, mlir::PatternRewriter &rewriter) const { mlir::Location loc = cleanupOp.getLoc(); @@ -1222,20 +1235,20 @@ class CIRCleanupScopeOpFlattening // 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. - // EH cleanup blocks are needed when there are throwing calls that need to - // be rewritten to try_call, or when there are resume ops from + // EH cleanup blocks are needed when there are throwing calls or throws + // that need to be rewritten, or when there are resume ops from // already-flattened inner cleanup scopes that need to chain through this // cleanup's EH handler. mlir::Block *unwindBlock = nullptr; mlir::Block *ehCleanupEntry = nullptr; - if (hasEHCleanup && - (!callsToRewrite.empty() || !resumeOpsToChain.empty())) { + if (hasEHCleanup && (!callsToRewrite.empty() || !throwsToRewrite.empty() || + !resumeOpsToChain.empty())) { ehCleanupEntry = buildEHCleanupBlocks(cleanupOp, loc, continueBlock, rewriter); - // The unwind block is only needed when there are throwing calls that - // need a shared unwind destination. Resume ops from inner cleanups - // branch directly to the EH cleanup entry. - if (!callsToRewrite.empty()) + // The unwind block is only needed when there are throwing calls or + // throws that need a shared unwind destination. Resume ops from inner + // cleanups branch directly to the EH cleanup entry. + if (!callsToRewrite.empty() || !throwsToRewrite.empty()) unwindBlock = buildUnwindBlock(ehCleanupEntry, /*isCleanupOnly=*/true, loc, ehCleanupEntry, rewriter); } @@ -1354,32 +1367,43 @@ class CIRCleanupScopeOpFlattening } } - // Replace non-nothrow calls with try_call operations. All calls within - // this cleanup scope share the same unwind destination. + // Replace non-nothrow calls and throws with try_call/try_throw + // operations. All calls and throws within this cleanup scope share the + // same unwind destination. if (hasEHCleanup) { for (cir::CallOp callOp : callsToRewrite) replaceCallWithTryCall(callOp, unwindBlock, loc, rewriter); + for (cir::ThrowOp throwOp : throwsToRewrite) + replaceThrowWithTryThrow(throwOp, unwindBlock, loc, rewriter); } - // Handle throwing calls in EH cleanup blocks. When an exception is thrown - // during cleanup code that runs on the exception unwind path, the C++ - // standard requires that std::terminate() be called. Replace such calls - // with try_call operations that unwind to a terminate block containing + // Handle throwing calls and throws in EH cleanup blocks. When an + // exception is thrown during cleanup code that runs on the exception + // unwind path, the C++ standard requires that std::terminate() be + // called. Replace such calls and throws with try_call/try_throw + // operations that unwind to a terminate block containing // cir.eh.initiate + cir.eh.terminate. if (ehCleanupEntry) { llvm::SmallVector<cir::CallOp> ehCleanupThrowingCalls; + llvm::SmallVector<cir::ThrowOp> ehCleanupThrows; for (mlir::Block *block = ehCleanupEntry; block != continueBlock; block = block->getNextNode()) { - block->walk([&](cir::CallOp callOp) { - if (!callOp.getNothrow()) - ehCleanupThrowingCalls.push_back(callOp); + block->walk([&](mlir::Operation *op) { + if (auto callOp = mlir::dyn_cast<cir::CallOp>(op)) { + if (!callOp.getNothrow()) + ehCleanupThrowingCalls.push_back(callOp); + } else if (auto throwOp = mlir::dyn_cast<cir::ThrowOp>(op)) { + ehCleanupThrows.push_back(throwOp); + } }); } - if (!ehCleanupThrowingCalls.empty()) { + if (!ehCleanupThrowingCalls.empty() || !ehCleanupThrows.empty()) { mlir::Block *terminateBlock = buildTerminateUnwindBlock(loc, continueBlock, rewriter); for (cir::CallOp callOp : ehCleanupThrowingCalls) replaceCallWithTryCall(callOp, terminateBlock, loc, rewriter); + for (cir::ThrowOp throwOp : ehCleanupThrows) + replaceThrowWithTryThrow(throwOp, terminateBlock, loc, rewriter); } } @@ -1443,12 +1467,15 @@ class CIRCleanupScopeOpFlattening assert(!exits.empty() && "cleanup scope body has no exit"); - // 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. + // Collect non-nothrow calls and throws that need to be converted to + // try_call/try_throw. This is only needed for EH and All cleanup kinds, + // but the vectors will simply be empty for Normal cleanup. llvm::SmallVector<cir::CallOp> callsToRewrite; - if (cleanupKind != cir::CleanupKind::Normal) + llvm::SmallVector<cir::ThrowOp> throwsToRewrite; + if (cleanupKind != cir::CleanupKind::Normal) { collectThrowingCalls(cleanupOp.getBodyRegion(), callsToRewrite); + collectThrows(cleanupOp.getBodyRegion(), throwsToRewrite); + } // Collect resume ops from already-flattened inner cleanup scopes that // need to chain through this cleanup's EH handler. @@ -1456,8 +1483,8 @@ class CIRCleanupScopeOpFlattening if (cleanupKind != cir::CleanupKind::Normal) collectResumeOps(cleanupOp.getBodyRegion(), resumeOpsToChain); - return flattenCleanup(cleanupOp, exits, callsToRewrite, resumeOpsToChain, - rewriter); + return flattenCleanup(cleanupOp, exits, callsToRewrite, throwsToRewrite, + resumeOpsToChain, rewriter); } }; @@ -1641,9 +1668,11 @@ class CIRTryOpFlattening : public mlir::OpRewritePattern<cir::TryOp> { mlir::MutableArrayRef<mlir::Region> handlerRegions = tryOp.getHandlerRegions(); - // Collect throwing calls in the try body. + // Collect throwing calls and throws in the try body. llvm::SmallVector<cir::CallOp> callsToRewrite; collectThrowingCalls(tryOp.getTryRegion(), callsToRewrite); + llvm::SmallVector<cir::ThrowOp> throwsToRewrite; + collectThrows(tryOp.getTryRegion(), throwsToRewrite); // Collect resume ops from already-flattened cleanup scopes in the try body. llvm::SmallVector<cir::ResumeOp> resumeOpsToChain; @@ -1677,12 +1706,13 @@ class CIRTryOpFlattening : public mlir::OpRewritePattern<cir::TryOp> { return mlir::success(); } - // If there are no throwing calls and no resume ops from inner cleanup - // scopes, exceptions cannot reach the catch handlers. Drop all uses - // from the (unreachable) handler regions before erasing the try op, - // since handler ops may reference values that were inlined from the - // try body into the parent block. - if (callsToRewrite.empty() && resumeOpsToChain.empty()) { + // If there are no throwing calls, no throws, and no resume ops from + // inner cleanup scopes, exceptions cannot reach the catch handlers. + // Drop all uses from the (unreachable) handler regions before erasing + // the try op, since handler ops may reference values that were inlined + // from the try body into the parent block. + if (callsToRewrite.empty() && throwsToRewrite.empty() && + resumeOpsToChain.empty()) { for (mlir::Region &handlerRegion : handlerRegions) for (mlir::Block &block : handlerRegion) block.dropAllDefinedValueUses(); @@ -1727,20 +1757,22 @@ class CIRTryOpFlattening : public mlir::OpRewritePattern<cir::TryOp> { return mlir::isa<cir::CatchAllAttr>(attr); }); - // Build a block to be the unwind desination for throwing calls and replace - // the calls with try_call ops. Note that the unwind block created here is - // something different than the unwind handler that we may have created - // above. The unwind handler continues unwinding after uncaught exceptions. - // This is the block that will eventually become the landing pad for invoke - // instructions. + // Build a block to be the unwind desination for throwing calls/throws + // and replace the calls/throws with try_call/try_throw ops. Note that + // the unwind block created here is something different than the unwind + // handler that we may have created above. The unwind handler continues + // unwinding after uncaught exceptions. This is the block that will + // eventually become the landing pad for invoke instructions. bool isCleanupOnly = tryOp.getCleanup() && !hasCatchAll; - if (!callsToRewrite.empty()) { - // Create a shared unwind block for all throwing calls. + if (!callsToRewrite.empty() || !throwsToRewrite.empty()) { + // Create a shared unwind block for all throwing calls/throws. mlir::Block *unwindBlock = buildUnwindBlock(dispatchBlock, isCleanupOnly, loc, dispatchBlock, rewriter); for (cir::CallOp callOp : callsToRewrite) replaceCallWithTryCall(callOp, unwindBlock, loc, rewriter); + for (cir::ThrowOp throwOp : throwsToRewrite) + replaceThrowWithTryThrow(throwOp, unwindBlock, loc, rewriter); } // Chain resume ops from inner cleanup scopes. diff --git a/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp b/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp new file mode 100644 index 0000000000000..4102a2a6f55ae --- /dev/null +++ b/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp @@ -0,0 +1,97 @@ +// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fcxx-exceptions -fexceptions -fclangir -emit-cir %s -o %t.cir +// RUN: FileCheck --input-file=%t.cir %s -check-prefix=CIR +// RUN: cir-opt --cir-flatten-cfg %t.cir -o %t-flat.cir +// RUN: FileCheck --input-file=%t-flat.cir %s --check-prefix=CIR-FLAT +// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fcxx-exceptions -fexceptions -fclangir -emit-llvm %s -o %t-cir.ll +// RUN: FileCheck --input-file=%t-cir.ll %s -check-prefix=LLVM +// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fcxx-exceptions -fexceptions -emit-llvm %s -o %t.ll +// RUN: FileCheck --input-file=%t.ll %s -check-prefix=OGCG + +struct Local { ~Local(); }; +void testSwitchWithCleanup(int n) { + Local x; + throw 42; +} + +// In CIR, the throw is emitted inside a `cir.cleanup.scope` whose cleanup +// region runs the destructor for `x` on the EH unwind path. + +// CIR: cir.func{{.*}} @_Z21testSwitchWithCleanupi(%[[ARG:.*]]: !s32i +// CIR: %[[N_ADDR:.*]] = cir.alloca !s32i, !cir.ptr<!s32i>, ["n", init] +// CIR: %[[X:.*]] = cir.alloca !rec_Local, !cir.ptr<!rec_Local>, ["x"] +// CIR: cir.store %[[ARG]], %[[N_ADDR]] : !s32i +// CIR: cir.cleanup.scope { +// CIR: %[[EXN:.*]] = cir.alloc.exception 4 -> !cir.ptr<!s32i> +// CIR: %[[VAL:.*]] = cir.const #cir.int<42> : !s32i +// CIR: cir.store align(16) %[[VAL]], %[[EXN]] : !s32i, !cir.ptr<!s32i> +// CIR: cir.throw %[[EXN]] : !cir.ptr<!s32i>, @_ZTIi +// CIR: cir.unreachable +// CIR: cir.yield +// CIR: } cleanup all { +// CIR: cir.call @_ZN5LocalD1Ev(%[[X]]) nothrow +// CIR: cir.yield +// CIR: } +// CIR: cir.return + +// After CFG flattening the cleanup scope is gone: the `cir.throw` becomes a +// `cir.try_throw` whose normal destination is a literally-unreachable block +// at the end of the function and whose unwind destination is the EH +// cleanup chain that runs the destructor and then resumes. + +// CIR-FLAT: cir.func{{.*}} @_Z21testSwitchWithCleanupi(%[[ARG:.*]]: !s32i +// CIR-FLAT: %[[N_ADDR:.*]] = cir.alloca !s32i, !cir.ptr<!s32i>, ["n", init] +// CIR-FLAT: %[[X:.*]] = cir.alloca !rec_Local, !cir.ptr<!rec_Local>, ["x"] +// CIR-FLAT: cir.store %[[ARG]], %[[N_ADDR]] +// CIR-FLAT: cir.br ^[[BODY:.+]] +// CIR-FLAT: ^[[BODY]]: +// CIR-FLAT: %[[EXN:.*]] = cir.alloc.exception 4 -> !cir.ptr<!s32i> +// CIR-FLAT: %[[VAL:.*]] = cir.const #cir.int<42> : !s32i +// CIR-FLAT: cir.store align(16) %[[VAL]], %[[EXN]] +// CIR-FLAT: cir.try_throw %[[EXN]] : !cir.ptr<!s32i>, @_ZTIi ^[[UNREACH:.+]], ^[[UNWIND:.+]] +// CIR-FLAT: ^[[UNWIND]]: +// CIR-FLAT: %[[ET:.*]] = cir.eh.initiate cleanup : !cir.eh_token +// CIR-FLAT: cir.br ^[[CLEANUP:.+]](%[[ET]] : !cir.eh_token) +// CIR-FLAT: ^[[CLEANUP]](%[[ET2:.*]]: !cir.eh_token): +// CIR-FLAT: %[[CT:.*]] = cir.begin_cleanup %[[ET2]] +// CIR-FLAT: cir.call @_ZN5LocalD1Ev(%[[X]]) nothrow +// CIR-FLAT: cir.end_cleanup %[[CT]] +// CIR-FLAT: cir.resume %[[ET2]] +// CIR-FLAT: cir.return +// CIR-FLAT: ^[[UNREACH]]: +// CIR-FLAT: cir.unreachable + +// In LLVM IR the throw becomes an `invoke @__cxa_throw` whose unwind +// destination is a landingpad with a `cleanup` clause, runs the destructor, +// and resumes. The "normal" destination of the invoke is a block containing +// just `unreachable`. + +// LLVM: define dso_local void @_Z21testSwitchWithCleanupi(i32 noundef %{{.*}}) {{.*}} personality ptr @__gxx_personality_v0 +// LLVM: %[[X:.*]] = alloca %struct.Local +// LLVM: %[[EXN:.*]] = call ptr @__cxa_allocate_exception(i64 4) +// LLVM: store i32 42, ptr %[[EXN]] +// LLVM: invoke void @__cxa_throw(ptr %[[EXN]], ptr @_ZTIi, ptr null) +// LLVM-NEXT: to label %[[NORMAL:.*]] unwind label %[[LPAD:.*]] +// LLVM: [[LPAD]]: +// LLVM: %{{.*}} = landingpad { ptr, i32 } +// LLVM-NEXT: cleanup +// LLVM: call void @_ZN5LocalD1Ev(ptr {{.*}} %[[X]]) +// LLVM: resume { ptr, i32 } %{{.*}} +// LLVM: [[NORMAL]]: +// LLVM: unreachable + +// OGCG produces equivalent IR: an `invoke __cxa_throw` whose unwind path +// is a `cleanup` landingpad that calls the destructor and resumes. + +// OGCG: define dso_local void @_Z21testSwitchWithCleanupi(i32 noundef %n) {{.*}} personality ptr @__gxx_personality_v0 +// OGCG: %[[X:.*]] = alloca %struct.Local +// OGCG: %[[EXN:.*]] = call ptr @__cxa_allocate_exception(i64 4) +// OGCG: store i32 42, ptr %[[EXN]] +// OGCG: invoke void @__cxa_throw(ptr %[[EXN]], ptr @_ZTIi, ptr null) +// OGCG-NEXT: to label %[[NORMAL:.*]] unwind label %[[LPAD:.*]] +// OGCG: [[LPAD]]: +// OGCG: %{{.*}} = landingpad { ptr, i32 } +// OGCG-NEXT: cleanup +// OGCG: call void @_ZN5LocalD1Ev(ptr {{.*}} %[[X]]) +// OGCG: resume { ptr, i32 } %{{.*}} +// OGCG: [[NORMAL]]: +// OGCG: unreachable diff --git a/clang/test/CIR/CodeGen/try-catch.cpp b/clang/test/CIR/CodeGen/try-catch.cpp index 08292f297da21..d5da5d3b2285f 100644 --- a/clang/test/CIR/CodeGen/try-catch.cpp +++ b/clang/test/CIR/CodeGen/try-catch.cpp @@ -1762,3 +1762,100 @@ int init_catch_param_with_ref_to_ptr_to_non_record() { // OGCG: %[[TMP_EXCEPTION_INFO:.*]] = insertvalue { ptr, i32 } poison, ptr %[[TMP_EXCEPTION]], 0 // OGCG: %[[EXCEPTION_INFO:.*]] = insertvalue { ptr, i32 } %[[TMP_EXCEPTION_INFO]], i32 %[[TMP_EH_TYPE_ID]], 1 // OGCG: resume { ptr, i32 } %[[EXCEPTION_INFO]] + +void direct_inside_try_catch_with_exception_type() { + try { + throw 42; + } catch (int e) { + } +} + +// CIR: cir.func {{.*}} @_Z43direct_inside_try_catch_with_exception_typev() personality(@__gxx_personality_v0) +// CIR: cir.scope { +// CIR: %[[E:.*]] = cir.alloca !s32i, !cir.ptr<!s32i>, ["e"] +// CIR: cir.try { +// CIR: %[[EXN:.*]] = cir.alloc.exception 4 -> !cir.ptr<!s32i> +// CIR: %[[FORTYTWO:.*]] = cir.const #cir.int<42> : !s32i +// CIR: cir.store{{.*}} %[[FORTYTWO]], %[[EXN]] +// CIR: cir.throw %[[EXN]] : !cir.ptr<!s32i>, @_ZTIi +// CIR: cir.unreachable +// CIR: } catch [type #cir.global_view<@_ZTIi> : !cir.ptr<!u8i>] (%[[TOKEN:.*]]: !cir.eh_token {{.*}}) { +// CIR: %[[CATCH_TOKEN:.*]], %[[EXN_PTR:.*]] = cir.begin_catch %{{.*}} : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>) +// CIR: cir.cleanup.scope { +// CIR: cir.init_catch_param scalar %[[EXN_PTR]] to %[[E]] : !cir.ptr<!void>, !cir.ptr<!s32i> +// CIR: cir.yield +// CIR: } cleanup all { +// CIR: cir.end_catch %catch_token : !cir.catch_token +// CIR: cir.yield +// CIR: } +// CIR: cir.yield +// CIR: } unwind (%{{.*}}: !cir.eh_token {{.*}}) { +// CIR: cir.resume %{{.*}} : !cir.eh_token +// CIR: } +// CIR: } + +// LLVM: define {{.*}} void @_Z43direct_inside_try_catch_with_exception_typev() {{.*}} personality ptr @__gxx_personality_v0 { +// LLVM: %[[E:.*]] = alloca i32 +// LLVM: %[[EXN:.*]] = call ptr @__cxa_allocate_exception(i64 4) +// LLVM: store i32 42, ptr %[[EXN]] +// LLVM: invoke void @__cxa_throw(ptr %[[EXN]], ptr @_ZTIi, ptr null) +// LLVM: to label %[[UNREACHABLE:.*]] unwind label %[[LANDING_PAD:.*]] +// LLVM: [[LANDING_PAD]]: +// LLVM: %[[LP:.*]] = landingpad { ptr, i32 } +// LLVM: catch ptr @_ZTIi +// LLVM: br label %[[CATCH:.*]] +// LLVM: [[CATCH]]: +// LLVM: br label %[[DISPATCH:.*]] +// LLVM: [[DISPATCH]]: +// LLVM: %[[EXN_PTR:.*]] = phi ptr +// LLVM: %[[EH_SELECTOR:.*]] = phi i32 +// LLVM: %[[INT_TYPE_ID:.*]] = call i32 @llvm.eh.typeid.for.p0(ptr @_ZTIi) +// LLVM: %[[TYPE_ID_EQ:.*]] = icmp eq i32 %[[EH_SELECTOR]], %[[INT_TYPE_ID]] +// LLVM: br i1 %[[TYPE_ID_EQ]], label %[[CATCH_INT:.*]], label %[[RESUME:.*]] +// LLVM: [[CATCH_INT]]: +// LLVM: %[[EXN_PTR:.*]] = phi ptr +// LLVM: %[[EH_SELECTOR:.*]] = phi i32 +// LLVM: %[[BEGIN_CATCH:.*]] = call ptr @__cxa_begin_catch(ptr %[[EXN_PTR]]) +// LLVM: call void @__cxa_end_catch() +// LLVM: br label %[[AFTER_CATCH:.*]] +// LLVM: [[AFTER_CATCH]]: +// LLVM: br label %[[END_DISPATCH:.*]] +// LLVM: [[END_DISPATCH]]: +// LLVM: br label %[[END_TRY:.*]] +// LLVM: [[RESUME]]: +// LLVM: resume { ptr, i32 } +// LLVM: [[END_TRY]]: +// LLVM: br label %[[TRY_CONT:.*]] +// LLVM: [[TRY_CONT]]: +// LLVM: ret void +// LLVM: [[UNREACHABLE]]: +// LLVM: unreachable + +// OGCG: define {{.*}} void @_Z43direct_inside_try_catch_with_exception_typev() {{.*}} personality ptr @__gxx_personality_v0 { +// OGCG: %[[EXN_SLOT:.*]] = alloca ptr +// OGCG: %[[EH_SELECTOR_SLOT:.*]] = alloca i32 +// OGCG: %[[E:.*]] = alloca i32 +// OGCG: %[[EXN:.*]] = call ptr @__cxa_allocate_exception(i64 4) +// OGCG: store i32 42, ptr %[[EXN]] +// OGCG: invoke void @__cxa_throw(ptr %[[EXN]], ptr @_ZTIi, ptr null) +// OGCG: to label %[[UNREACHABLE:.*]] unwind label %[[LANDING_PAD:.*]] +// OGCG: [[LANDING_PAD]]: +// OGCG: %[[LP:.*]] = landingpad { ptr, i32 } +// OGCG: catch ptr @_ZTIi +// OGCG: br label %[[DISPATCH:.*]] +// OGCG: [[DISPATCH]]: +// OGCG: %[[EH_SELECTOR:.*]] = load i32, ptr %[[EH_SELECTOR_SLOT]] +// OGCG: %[[INT_TYPE_ID:.*]] = call i32 @llvm.eh.typeid.for.p0(ptr @_ZTIi) +// OGCG: %[[TYPE_ID_EQ:.*]] = icmp eq i32 %[[EH_SELECTOR]], %[[INT_TYPE_ID]] +// OGCG: br i1 %[[TYPE_ID_EQ]], label %[[CATCH_INT:.*]], label %[[RESUME:.*]] +// OGCG: [[CATCH_INT]]: +// OGCG: %[[EXN_PTR:.*]] = load ptr, ptr %[[EXN_SLOT]] +// OGCG: %[[BEGIN_CATCH:.*]] = call ptr @__cxa_begin_catch(ptr %[[EXN_PTR]]) +// OGCG: call void @__cxa_end_catch() +// OGCG: br label %[[TRY_CONT:.*]] +// OGCG: [[TRY_CONT]]: +// OGCG: ret void +// OGCG: [[RESUME]]: +// OGCG: resume { ptr, i32 } +// OGCG: [[UNREACHABLE]]: +// OGCG: unreachable >From b119b5d3f3c2e9c7f998ac73c64651a3587c8ed4 Mon Sep 17 00:00:00 2001 From: Andy Kaylor <[email protected]> Date: Fri, 22 May 2026 11:58:17 -0700 Subject: [PATCH 2/2] Address review feedback --- clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp | 4 ++-- clang/test/CIR/CodeGen/try-catch.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp b/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp index 4102a2a6f55ae..c7cf0b03422a8 100644 --- a/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp +++ b/clang/test/CIR/CodeGen/cleanup-throw-from-cleanup.cpp @@ -23,7 +23,7 @@ void testSwitchWithCleanup(int n) { // CIR: cir.cleanup.scope { // CIR: %[[EXN:.*]] = cir.alloc.exception 4 -> !cir.ptr<!s32i> // CIR: %[[VAL:.*]] = cir.const #cir.int<42> : !s32i -// CIR: cir.store align(16) %[[VAL]], %[[EXN]] : !s32i, !cir.ptr<!s32i> +// CIR: cir.store{{.*}} %[[VAL]], %[[EXN]] : !s32i, !cir.ptr<!s32i> // CIR: cir.throw %[[EXN]] : !cir.ptr<!s32i>, @_ZTIi // CIR: cir.unreachable // CIR: cir.yield @@ -46,7 +46,7 @@ void testSwitchWithCleanup(int n) { // CIR-FLAT: ^[[BODY]]: // CIR-FLAT: %[[EXN:.*]] = cir.alloc.exception 4 -> !cir.ptr<!s32i> // CIR-FLAT: %[[VAL:.*]] = cir.const #cir.int<42> : !s32i -// CIR-FLAT: cir.store align(16) %[[VAL]], %[[EXN]] +// CIR-FLAT: cir.store{{.*}} %[[VAL]], %[[EXN]] // CIR-FLAT: cir.try_throw %[[EXN]] : !cir.ptr<!s32i>, @_ZTIi ^[[UNREACH:.+]], ^[[UNWIND:.+]] // CIR-FLAT: ^[[UNWIND]]: // CIR-FLAT: %[[ET:.*]] = cir.eh.initiate cleanup : !cir.eh_token diff --git a/clang/test/CIR/CodeGen/try-catch.cpp b/clang/test/CIR/CodeGen/try-catch.cpp index d5da5d3b2285f..cf971062437a6 100644 --- a/clang/test/CIR/CodeGen/try-catch.cpp +++ b/clang/test/CIR/CodeGen/try-catch.cpp @@ -1645,7 +1645,7 @@ int init_catch_param_with_ref_to_ptr_to_non_record() { // CIR: cir.store {{.*}} %[[P_VAL]], %[[RV_ADDR]] : !s32i, !cir.ptr<!s32i> // CIR: cir.yield // CIR: } cleanup all { -// CIR: cir.end_catch %catch_token : !cir.catch_token +// CIR: cir.end_catch %[[CATCH_TOKEN]] : !cir.catch_token // CIR: cir.yield // CIR: } // CIR: cir.yield @@ -1785,7 +1785,7 @@ void direct_inside_try_catch_with_exception_type() { // CIR: cir.init_catch_param scalar %[[EXN_PTR]] to %[[E]] : !cir.ptr<!void>, !cir.ptr<!s32i> // CIR: cir.yield // CIR: } cleanup all { -// CIR: cir.end_catch %catch_token : !cir.catch_token +// CIR: cir.end_catch %[[CATCH_TOKEN]] : !cir.catch_token // CIR: cir.yield // CIR: } // CIR: cir.yield _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
