diff --git a/clang/docs/ClangIRCleanupAndEHDesign.md b/clang/docs/ClangIRCleanupAndEHDesign.md index f3011a1d9c49..afe259c3524e 100644 --- a/clang/docs/ClangIRCleanupAndEHDesign.md +++ b/clang/docs/ClangIRCleanupAndEHDesign.md @@ -732,9 +732,9 @@ CIR operations. The operations that were used in the ClangIR incubator project were closely matched to the Itanium exception handling ABI. In order to achieve a representation that also works well for other ABIs, the following new operations are being proposed: `cir.eh.initiate`, -`cir.eh.dispatch`, `cir.begin_cleanup`, and `cir.end_cleanup`. The -`cir.begin_catch` and `cir.end_catch` operations, described above, -are also used in the flattened form. +`cir.eh.dispatch`, `cir.eh.terminate`, `cir.begin_cleanup`, and +`cir.end_cleanup`. The `cir.begin_catch` and `cir.end_catch` operations, +described above, are also used in the flattened form. Any time a cir.call operation that may throw and exception appears within the try region of a `cir.try` operation or within the body region @@ -953,6 +953,91 @@ the EH cleanup block (`^bb2`), which branches to `^bb3` to perform the cleanup, but because we have no catch handler, we execute `cir.resume` after the cleanup to unwind to the function that called `someFunc()`. +#### Throwing Calls in Cleanup Regions + +When a call in an EH cleanup region may throw an exception, it requires +special handling. The C++ standard requires that if an exception is +thrown during exception cleanup (i.e., while unwinding a previous +exception), the program must call `std::terminate()`. In the flattened +CIR, such calls are replaced with `cir.try_call` operations whose +unwind destination contains a `cir.eh.initiate` followed by a +`cir.eh.terminate` operation. + +The `cir.eh.terminate` operation is a terminator that signals the need +for program termination due to an exception thrown during cleanup. It +takes the `!cir.eh_token` returned by `cir.eh.initiate` and is further +processed during EH ABI lowering, where it is replaced with target-specific +termination code. + +#### Example: Cleanup with throwing destructor + +**C++** + +``` c++ +struct ThrowingDtor { + ~ThrowingDtor() noexcept(false); +}; + +void someFunc() { + ThrowingDtor c; + c.doSomething(); +} +``` + +**CIR** + +``` +cir.func @someFunc(){ + %0 = cir.alloca !rec_ThrowingDtor, !cir.ptr, ["c", init] + cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr) -> () + cir.cleanup.scope { + cir.call @_ZN12ThrowingDtor11doSomethingEv(%0) : (!cir.ptr) -> () + cir.yield + } cleanup all { + cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr) -> () + cir.yield + } + cir.return +} +``` + +**Flattened CIR** + +``` +cir.func @someFunc(){ + %0 = cir.alloca !rec_ThrowingDtor, !cir.ptr, ["c", init] + cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr) -> () + cir.try_call @_ZN12ThrowingDtor11doSomethingEv(%0) ^bb1, ^bb2 : (!cir.ptr) -> () +^bb1 // Normal cleanup + cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr) -> () + cir.br ^bb6 +^bb2 // EH cleanup (from entry block) + %1 = cir.eh.initiate cleanup : !cir.eh_token + cir.br ^bb3(%1 : !cir.eh_token) +^bb3(%eh_token : !cir.eh_token) // Perform cleanup + %2 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token + cir.try_call @_ZN12ThrowingDtorD1Ev(%0) ^bb4, ^bb5 : (!cir.ptr) -> () +^bb4 // Destructor completed: continue unwinding + cir.end_cleanup(%2 : !cir.cleanup_token) + cir.resume %eh_token : !cir.eh_token +^bb5 // Destructor threw: terminate + %3 = cir.eh.initiate : !cir.eh_token + cir.eh.terminate %3 : !cir.eh_token +^bb6 // Normal continue (from ^bb1) + cir.return +} +``` + +In this example, the destructor for `ThrowingDtor` may throw. In the +normal cleanup path (`^bb1`), the destructor is a regular `cir.call` +since the exception would propagate normally. In the EH cleanup path +(`^bb3`), the destructor call is a `cir.try_call` because if the +destructor throws during exception unwinding, the program must +terminate. If the destructor completes normally, the exception +continues unwinding via `cir.resume`. If the destructor throws, control +transfers to `^bb5`, which initiates exception handling and immediately +terminates. + #### Example: Shared cleanups **C++** @@ -1142,7 +1227,9 @@ The Itanium exception handling ABI representation replaces the for the catch handlers. The `cir.begin_cleanup` and `cir.end_cleanup` operations are simply dropped. The `cir.begin_catch` operation becomes a call to `__cxa_begin_catch`. The `cir.end_catch` operation becomes a -call to `__cxa_end_catch`. +call to `__cxa_end_catch`. The `cir.eh.terminate` operation becomes a +call to `__clang_call_terminate` (which calls `__cxa_begin_catch` +followed by `std::terminate()`) and then an unreachable operation. The only operation that is specific to Itanium exception handling is `cir.eh.landingpad`. diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td index 329939dc1b2e..e7604af394fb 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIROps.td +++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td @@ -7037,6 +7037,46 @@ def CIR_EhInitiateOp : CIR_Op<"eh.initiate"> { let hasLLVMLowering = false; } +//===----------------------------------------------------------------------===// +// Flattened EH Operations: EhTerminateOp +//===----------------------------------------------------------------------===// + +def CIR_EhTerminateOp : CIR_Op<"eh.terminate", [ + Terminator +]> { + let summary = "Terminate due to exception thrown during cleanup"; + let description = [{ + `cir.eh.terminate` terminates program execution when an exception is thrown + while executing cleanup code during exception unwinding. The C++ standard + requires that `std::terminate()` be called in this scenario. + + This operation takes an `!cir.eh_token` from a `cir.eh.initiate` operation + and acts as a terminator. It is produced during CFG flattening when throwing + calls are found in EH cleanup regions. + + During EH ABI lowering, this is replaced with target-specific termination + code. For the Itanium ABI, the `cir.eh.initiate` is lowered to + `cir.eh.inflight_exception` (producing an exception pointer), and the + `cir.eh.terminate` becomes a call to `__clang_call_terminate` with that + pointer, followed by an unreachable operation. + + Example: + + ```mlir + ^terminate_unwind: + %eh_token = cir.eh.initiate : !cir.eh_token + cir.eh.terminate %eh_token : !cir.eh_token + ``` + }]; + + let arguments = (ins CIR_EhTokenType:$eh_token); + let assemblyFormat = [{ + $eh_token `:` type($eh_token) attr-dict + }]; + + let hasLLVMLowering = false; +} + //===----------------------------------------------------------------------===// // Flattened EH Operations: EhDispatchOp //===----------------------------------------------------------------------===// diff --git a/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp b/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp index d5d10b1b251b..8a00b0e5b99c 100644 --- a/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp +++ b/clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp @@ -17,6 +17,7 @@ // - cir.end_cleanup → (removed) // - cir.begin_catch → call to __cxa_begin_catch // - cir.end_catch → call to __cxa_end_catch +// - cir.eh.terminate → call to __clang_call_terminate + unreachable // - cir.resume → cir.resume.flat // - !cir.eh_token values → (!cir.ptr, !u32i) value pairs // - personality function set on functions requiring EH @@ -116,11 +117,13 @@ private: cir::FuncOp personalityFunc; cir::FuncOp beginCatchFunc; cir::FuncOp endCatchFunc; + cir::FuncOp clangCallTerminateFunc; constexpr const static ::llvm::StringLiteral kGxxPersonality = "__gxx_personality_v0"; void ensureRuntimeDecls(mlir::Location loc); + void ensureClangCallTerminate(mlir::Location loc); mlir::LogicalResult lowerFunc(cir::FuncOp funcOp); void lowerEhInitiate(cir::EhInitiateOp initiateOp, EhTokenMap &ehTokenMap, SmallVectorImpl &deadOps); @@ -172,6 +175,62 @@ void ItaniumEHLowering::ensureRuntimeDecls(mlir::Location loc) { } } +/// Ensure the __clang_call_terminate function exists in the module. This +/// function is defined with a body that calls __cxa_begin_catch followed by +/// std::terminate, matching the behavior of Clang's LLVM IR codegen. +/// +/// void __clang_call_terminate(void *exn) nounwind noreturn { +/// __cxa_begin_catch(exn); +/// std::terminate(); +/// unreachable; +/// } +void ItaniumEHLowering::ensureClangCallTerminate(mlir::Location loc) { + if (clangCallTerminateFunc) + return; + + ensureRuntimeDecls(loc); + + if (auto existing = mod.lookupSymbol("__clang_call_terminate")) { + clangCallTerminateFunc = existing; + return; + } + + auto funcTy = cir::FuncType::get({voidPtrType}, voidType, /*isVarArg=*/false); + builder.setInsertionPointToEnd(mod.getBody()); + auto funcOp = + cir::FuncOp::create(builder, loc, "__clang_call_terminate", funcTy); + funcOp.setLinkage(cir::GlobalLinkageKind::LinkOnceODRLinkage); + funcOp.setGlobalVisibilityAttr( + cir::VisibilityAttr::get(ctx, cir::VisibilityKind::Hidden)); + + mlir::Block *entryBlock = funcOp.addEntryBlock(); + builder.setInsertionPointToStart(entryBlock); + mlir::Value exnArg = entryBlock->getArgument(0); + + auto catchCall = cir::CallOp::create( + builder, loc, mlir::FlatSymbolRefAttr::get(beginCatchFunc), u8PtrType, + mlir::ValueRange{exnArg}); + catchCall.setNothrowAttr(builder.getUnitAttr()); + + auto terminateFuncDecl = getOrCreateRuntimeFuncDecl( + mod, loc, "_ZSt9terminatev", + cir::FuncType::get({}, voidType, /*isVarArg=*/false)); + terminateFuncDecl->setAttr(cir::CIRDialect::getNoReturnAttrName(), + builder.getUnitAttr()); + auto terminateCall = cir::CallOp::create( + builder, loc, mlir::FlatSymbolRefAttr::get(terminateFuncDecl), voidType, + mlir::ValueRange{}); + terminateCall.setNothrowAttr(builder.getUnitAttr()); + terminateCall->setAttr(cir::CIRDialect::getNoReturnAttrName(), + builder.getUnitAttr()); + + cir::UnreachableOp::create(builder, loc); + + funcOp->setAttr(cir::CIRDialect::getNoReturnAttrName(), + builder.getUnitAttr()); + clangCallTerminateFunc = funcOp; +} + /// Lower all EH operations in a single function. mlir::LogicalResult ItaniumEHLowering::lowerFunc(cir::FuncOp funcOp) { if (funcOp.isDeclaration()) @@ -360,6 +419,19 @@ void ItaniumEHLowering::lowerEhInitiate( auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken()); lowerDispatch(op, exnPtr, typeId, deadOps); } + } else if (auto op = mlir::dyn_cast(user)) { + auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken()); + ensureClangCallTerminate(op.getLoc()); + builder.setInsertionPoint(op); + auto call = cir::CallOp::create( + builder, op.getLoc(), + mlir::FlatSymbolRefAttr::get(clangCallTerminateFunc), voidType, + mlir::ValueRange{exnPtr}); + call.setNothrowAttr(builder.getUnitAttr()); + call->setAttr(cir::CIRDialect::getNoReturnAttrName(), + builder.getUnitAttr()); + cir::UnreachableOp::create(builder, op.getLoc()); + op.erase(); } else if (auto op = mlir::dyn_cast(user)) { auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken()); builder.setInsertionPoint(op); diff --git a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp index 5b1f656cac3d..935d95b01a4a 100644 --- a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp +++ b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp @@ -717,6 +717,19 @@ static mlir::Block *buildUnwindBlock(mlir::Block *dest, bool hasCleanup, return unwindBlock; } +// Create a shared terminate unwind block for throwing calls in EH cleanup +// regions. When an exception is thrown during cleanup (unwinding), the C++ +// standard requires that std::terminate() be called. +static mlir::Block *buildTerminateUnwindBlock(mlir::Location loc, + mlir::Block *insertBefore, + mlir::PatternRewriter &rewriter) { + mlir::Block *terminateBlock = rewriter.createBlock(insertBefore); + rewriter.setInsertionPointToEnd(terminateBlock); + auto ehInitiate = cir::EhInitiateOp::create(rewriter, loc, /*cleanup=*/false); + cir::EhTerminateOp::create(rewriter, loc, ehInitiate.getEhToken()); + return terminateBlock; +} + class CIRCleanupScopeOpFlattening : public mlir::OpRewritePattern { public: @@ -1332,6 +1345,28 @@ public: replaceCallWithTryCall(callOp, 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 + // cir.eh.initiate + cir.eh.terminate. + if (ehCleanupEntry) { + llvm::SmallVector ehCleanupThrowingCalls; + for (mlir::Block *block = ehCleanupEntry; block != continueBlock; + block = block->getNextNode()) { + block->walk([&](cir::CallOp callOp) { + if (!callOp.getNothrow()) + ehCleanupThrowingCalls.push_back(callOp); + }); + } + if (!ehCleanupThrowingCalls.empty()) { + mlir::Block *terminateBlock = + buildTerminateUnwindBlock(loc, continueBlock, rewriter); + for (cir::CallOp callOp : ehCleanupThrowingCalls) + replaceCallWithTryCall(callOp, terminateBlock, loc, rewriter); + } + } + // Chain inner EH cleanup resume ops to this cleanup's EH handler. // Each cir.resume from an already-flattened inner cleanup is replaced // with a branch to the outer EH cleanup entry, passing the eh_token @@ -1372,17 +1407,6 @@ public: cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind(); - // 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 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 exits; int nextId = 0; diff --git a/clang/test/CIR/CodeGen/cleanup-throwing-dtor.cpp b/clang/test/CIR/CodeGen/cleanup-throwing-dtor.cpp new file mode 100644 index 000000000000..0bcd211c1cd6 --- /dev/null +++ b/clang/test/CIR/CodeGen/cleanup-throwing-dtor.cpp @@ -0,0 +1,118 @@ +// 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 + +// Test that a struct with a potentially-throwing destructor (noexcept(false)) +// produces the correct high-level CIR (cleanup region without nothrow on the +// dtor call) and correct flattened CIR (try_call in the EH cleanup path with +// an unwind-to-terminate block). + +struct ThrowingDtor { + ~ThrowingDtor() noexcept(false); + void doSomething(); +}; + +void test_throwing_dtor_cleanup() { + ThrowingDtor c; + c.doSomething(); +} + +// High-level: the cleanup region's dtor call does NOT have nothrow. +// +// CIR: cir.func{{.*}} @_Z26test_throwing_dtor_cleanupv() +// CIR: %[[C:.*]] = cir.alloca !rec_ThrowingDtor, !cir.ptr, ["c"] +// CIR: cir.cleanup.scope { +// CIR: cir.call @_ZN12ThrowingDtor11doSomethingEv(%[[C]]) +// CIR: cir.yield +// CIR: } cleanup all { +// CIR: cir.call @_ZN12ThrowingDtorD1Ev(%[[C]]) +// CIR: cir.yield +// CIR: } + +// Flattened: body call becomes try_call. In the EH cleanup path, the dtor +// becomes a try_call that unwinds to a terminate block. +// +// CIR-FLAT: cir.func{{.*}} @_Z26test_throwing_dtor_cleanupv() +// CIR-FLAT: %[[C:.*]] = cir.alloca !rec_ThrowingDtor, !cir.ptr, ["c"] +// CIR-FLAT: cir.br ^[[BODY:bb[0-9]+]] +// +// Body: doSomething becomes a try_call. +// CIR-FLAT: ^[[BODY]]: +// CIR-FLAT: cir.try_call @_ZN12ThrowingDtor11doSomethingEv(%[[C]]) ^[[NORMAL_BODY:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]] +// +// Normal path: dtor is a regular call (not during unwinding). +// CIR-FLAT: ^[[NORMAL_BODY]]: +// CIR-FLAT: cir.call @_ZN12ThrowingDtorD1Ev(%[[C]]) +// +// EH cleanup: dtor becomes try_call with unwind to terminate. +// CIR-FLAT: cir.try_call @_ZN12ThrowingDtorD1Ev(%[[C]]) ^[[DTOR_OK:bb[0-9]+]], ^[[TERMINATE:bb[0-9]+]] +// CIR-FLAT: ^[[DTOR_OK]]: +// CIR-FLAT: cir.end_cleanup +// CIR-FLAT: cir.resume +// +// Terminate block. +// CIR-FLAT: ^[[TERMINATE]]: +// CIR-FLAT: %[[TET:.*]] = cir.eh.initiate : !cir.eh_token +// CIR-FLAT: cir.eh.terminate %[[TET]] : !cir.eh_token + +// LLVM IR via the CIR pipeline. doSomething is invoked, dtor is called on +// normal path, invoked on EH path with unwind to terminate. +// +// LLVM: define dso_local void @_Z26test_throwing_dtor_cleanupv() +// LLVM-SAME: personality ptr @__gxx_personality_v0 +// LLVM: %[[C:.*]] = alloca %struct.ThrowingDtor +// LLVM: invoke void @_ZN12ThrowingDtor11doSomethingEv(ptr {{.*}} %[[C]]) +// LLVM: to label %[[NORMAL:.*]] unwind label %[[LPAD:.*]] +// LLVM: [[NORMAL]]: +// LLVM: call void @_ZN12ThrowingDtorD1Ev(ptr {{.*}} %[[C]]) +// LLVM: [[LPAD]]: +// LLVM: landingpad { ptr, i32 } +// LLVM: cleanup +// LLVM: invoke void @_ZN12ThrowingDtorD1Ev(ptr {{.*}} %[[C]]) +// LLVM: to label %[[RESUME:.*]] unwind label %[[TERMINATE:.*]] +// LLVM: [[RESUME]]: +// LLVM: resume { ptr, i32 } +// LLVM: [[TERMINATE]]: +// LLVM: landingpad { ptr, i32 } +// LLVM: catch ptr null +// LLVM: call void @__clang_call_terminate(ptr +// LLVM: unreachable +// LLVM: ret void +// +// LLVM: define linkonce_odr hidden void @__clang_call_terminate(ptr %[[EXN:.*]]) +// LLVM: call ptr @__cxa_begin_catch(ptr %[[EXN]]) +// LLVM: call void @_ZSt9terminatev() +// LLVM: unreachable + +// Same structural flow from original Clang CodeGen. +// +// OGCG: define dso_local void @_Z26test_throwing_dtor_cleanupv() +// OGCG-SAME: personality ptr @__gxx_personality_v0 +// OGCG: %[[C:.*]] = alloca %struct.ThrowingDtor +// OGCG: invoke void @_ZN12ThrowingDtor11doSomethingEv(ptr {{.*}} %[[C]]) +// OGCG: to label %[[NORMAL:.*]] unwind label %[[LPAD:.*]] +// OGCG: [[NORMAL]]: +// OGCG: call void @_ZN12ThrowingDtorD1Ev(ptr {{.*}} %[[C]]) +// OGCG: ret void +// OGCG: [[LPAD]]: +// OGCG: landingpad { ptr, i32 } +// OGCG: cleanup +// OGCG: invoke void @_ZN12ThrowingDtorD1Ev(ptr {{.*}} %[[C]]) +// OGCG: to label %[[RESUME:.*]] unwind label %[[TERMINATE:.*]] +// OGCG: [[RESUME]]: +// OGCG: resume { ptr, i32 } +// OGCG: [[TERMINATE]]: +// OGCG: landingpad { ptr, i32 } +// OGCG: catch ptr null +// OGCG: call void @__clang_call_terminate(ptr +// OGCG: unreachable +// +// OGCG: define linkonce_odr hidden void @__clang_call_terminate(ptr {{.*}} %[[EXN:.*]]) +// OGCG: call ptr @__cxa_begin_catch(ptr %[[EXN]]) +// OGCG: call void @_ZSt9terminatev() +// OGCG: unreachable diff --git a/clang/test/CIR/Transforms/eh-abi-lowering-itanium.cir b/clang/test/CIR/Transforms/eh-abi-lowering-itanium.cir index 6fc86c233948..7b650e5b9f1b 100644 --- a/clang/test/CIR/Transforms/eh-abi-lowering-itanium.cir +++ b/clang/test/CIR/Transforms/eh-abi-lowering-itanium.cir @@ -499,14 +499,71 @@ cir.func no_inline dso_local @test_catch_with_cleanup_no_ctor() personality(@__g // CHECK: cir.call @__cxa_begin_catch(%[[CA_EXN]]) // CHECK: cir.call @__cxa_end_catch() +// Test: cir.eh.terminate is lowered to call __clang_call_terminate + unreachable. +// This represents the case where an exception is thrown during cleanup code +// (e.g., a destructor that throws while another exception is being unwound). +cir.func @test_eh_terminate() { + %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c", init] {alignment = 4 : i64} + cir.call @ctor(%0) : (!cir.ptr) -> () + cir.try_call @doSomething(%0) ^bb1, ^bb2 : (!cir.ptr) -> () +^bb1: + cir.br ^bb6 +^bb2: + %1 = cir.eh.initiate cleanup : !cir.eh_token + cir.br ^bb3(%1 : !cir.eh_token) +^bb3(%eh_token : !cir.eh_token): + %2 = cir.begin_cleanup %eh_token : !cir.eh_token -> !cir.cleanup_token + cir.try_call @throwing_dtor(%0) ^bb4, ^bb5 : (!cir.ptr) -> () +^bb4: + cir.end_cleanup %2 : !cir.cleanup_token + cir.resume %eh_token : !cir.eh_token +^bb5: + %3 = cir.eh.initiate : !cir.eh_token + cir.eh.terminate %3 : !cir.eh_token +^bb6: + cir.return +} + +// CHECK-LABEL: cir.func @test_eh_terminate() +// CHECK-SAME: personality(@__gxx_personality_v0) +// CHECK: cir.try_call @doSomething(%{{.*}}) ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]] +// CHECK: ^[[NORMAL]]: +// CHECK: cir.br ^[[RETURN:bb[0-9]+]] +// +// Unwind from doSomething: initiate → inflight. +// CHECK: ^[[UNWIND]]: +// CHECK: %[[EXN:.*]], %[[TID:.*]] = cir.eh.inflight_exception cleanup +// CHECK: cir.br ^[[CLEANUP:bb[0-9]+]](%[[EXN]], %[[TID]] : !cir.ptr, !u32i) +// +// Cleanup block: begin_cleanup removed, try_call to throwing_dtor. +// CHECK: ^[[CLEANUP]](%[[C_EXN:.*]]: !cir.ptr, %[[C_TID:.*]]: !u32i): +// CHECK: cir.try_call @throwing_dtor(%{{.*}}) ^[[DTOR_OK:bb[0-9]+]], ^[[TERMINATE:bb[0-9]+]] +// +// Normal path from dtor: end_cleanup removed, resume → resume.flat. +// CHECK: ^[[DTOR_OK]]: +// CHECK: cir.resume.flat %[[C_EXN]], %[[C_TID]] +// +// Terminate block: initiate → inflight, then call __clang_call_terminate + unreachable. +// CHECK: ^[[TERMINATE]]: +// CHECK: %[[T_EXN:.*]], %{{.*}} = cir.eh.inflight_exception +// CHECK: cir.call @__clang_call_terminate(%[[T_EXN]]) nothrow {noreturn} +// CHECK: cir.unreachable + // Verify that runtime function declarations are added. // CHECK: cir.func private @__gxx_personality_v0(...) // CHECK: cir.func private @__cxa_begin_catch(!cir.ptr) // CHECK: cir.func private @__cxa_end_catch() +// Verify the __clang_call_terminate function is defined with proper body. +// CHECK: cir.func linkonce_odr hidden @__clang_call_terminate(%[[ARG:.*]]: !cir.ptr) attributes {noreturn} +// CHECK: cir.call @__cxa_begin_catch(%[[ARG]]) nothrow +// CHECK: cir.call @_ZSt9terminatev() nothrow {noreturn} +// CHECK: cir.unreachable + cir.func private @mayThrow() cir.func private @ctor(!cir.ptr) cir.func private @dtor(!cir.ptr) attributes {nothrow} +cir.func private @throwing_dtor(!cir.ptr) cir.func private @doSomething(!cir.ptr) cir.global "private" constant external @_ZTISt9exception : !cir.ptr cir.global "private" constant external @_ZTIi : !cir.ptr diff --git a/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir b/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir index 63ed793b9cfd..8a49c111c896 100644 --- a/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir +++ b/clang/test/CIR/Transforms/flatten-cleanup-scope-nyi.cir @@ -5,92 +5,6 @@ !u8i = !cir.int !void = !cir.void -cir.global "private" constant external @_ZTIi : !cir.ptr - -// 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, ["c", init] {alignment = 4 : i64} - cir.call @ctor(%0) : (!cir.ptr) -> () - cir.try { - // expected-error @below {{throwing calls in cleanup region are not yet implemented}} - cir.cleanup.scope { - cir.call @doSomething(%0) : (!cir.ptr) -> () - cir.yield - } cleanup eh { - cir.call @dtor(%0) : (!cir.ptr) -> () - 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, ["c", init] {alignment = 4 : i64} - cir.call @ctor(%0) : (!cir.ptr) -> () - cir.try { - // expected-error @below {{throwing calls in cleanup region are not yet implemented}} - cir.cleanup.scope { - cir.call @doSomething(%0) : (!cir.ptr) -> () - cir.yield - } cleanup all { - cir.call @dtor(%0) : (!cir.ptr) -> () - cir.yield - } - cir.yield - } - cir.return -} - -// Test that we issue a diagnostic for throwing calls in the cleanup region -// of a nested EH cleanup scope (the dtor is not nothrow). -cir.func @test_nested_eh_cleanup() { - %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c1", init] {alignment = 4 : i64} - %1 = cir.alloca !rec_SomeClass, !cir.ptr, ["c2", init] {alignment = 4 : i64} - cir.call @ctor(%0) : (!cir.ptr) -> () - cir.cleanup.scope { - cir.call @ctor(%1) : (!cir.ptr) -> () - // expected-error @below {{throwing calls in cleanup region are not yet implemented}} - cir.cleanup.scope { - cir.call @doSomething(%0) : (!cir.ptr) -> () - cir.yield - } cleanup eh { - cir.call @dtor(%1) : (!cir.ptr) -> () - cir.yield - } - cir.yield - } cleanup normal { - cir.call @dtor(%0) : (!cir.ptr) -> () - cir.yield - } - cir.return -} - -// Test that we issue a diagnostic for throwing calls in the cleanup region -// of a nested Normal+EH cleanup scope (the dtor is not nothrow). -cir.func @test_nested_all_cleanup() { - %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c1", init] {alignment = 4 : i64} - %1 = cir.alloca !rec_SomeClass, !cir.ptr, ["c2", init] {alignment = 4 : i64} - cir.call @ctor(%0) : (!cir.ptr) -> () - cir.cleanup.scope { - cir.call @ctor(%1) : (!cir.ptr) -> () - // expected-error @below {{throwing calls in cleanup region are not yet implemented}} - cir.cleanup.scope { - cir.call @doSomething(%0) : (!cir.ptr) -> () - cir.yield - } cleanup all { - cir.call @dtor(%1) : (!cir.ptr) -> () - cir.yield - } - cir.yield - } cleanup normal { - cir.call @dtor(%0) : (!cir.ptr) -> () - cir.yield - } - cir.return -} - // Test that we issue a diagnostic for a single goto out of a cleanup scope. // Strictly speaking, we could handle this case, but it's left unimplemented // because when we handle multiple exits we'll need to do something to determine @@ -144,40 +58,6 @@ 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, ["c", init] {alignment = 4 : i64} - cir.call @ctor(%0) : (!cir.ptr) -> () - // expected-error @below {{throwing calls in cleanup region are not yet implemented}} - cir.cleanup.scope { - cir.call @doSomething(%0) : (!cir.ptr) -> () - cir.yield - } cleanup eh { - // Throwing destructor - cir.call @dtor(%0) : (!cir.ptr) -> () - 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, ["c", init] {alignment = 4 : i64} - cir.call @ctor(%0) : (!cir.ptr) -> () - // expected-error @below {{throwing calls in cleanup region are not yet implemented}} - cir.cleanup.scope { - cir.call @doSomething(%0) : (!cir.ptr) -> () - cir.yield - } cleanup all { - // Throwing destructor - cir.call @dtor(%0) : (!cir.ptr) -> () - cir.yield - } - cir.return -} - cir.func private @ctor(!cir.ptr) cir.func private @dtor(!cir.ptr) cir.func private @doSomething(!cir.ptr) diff --git a/clang/test/CIR/Transforms/flatten-throwing-in-cleanup.cir b/clang/test/CIR/Transforms/flatten-throwing-in-cleanup.cir new file mode 100644 index 000000000000..b5b3d2441483 --- /dev/null +++ b/clang/test/CIR/Transforms/flatten-throwing-in-cleanup.cir @@ -0,0 +1,166 @@ +// RUN: cir-opt %s -cir-flatten-cfg -o %t.cir +// RUN: FileCheck --input-file=%t.cir %s + +!s32i = !cir.int +!u8i = !cir.int +!rec_SomeClass = !cir.record + +// Test that a throwing call in an EH cleanup region is replaced with +// cir.try_call that unwinds to a terminate block. +cir.func @test_throwing_dtor_in_eh_cleanup() { + %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c", init] {alignment = 4 : i64} + cir.call @ctor(%0) : (!cir.ptr) -> () + cir.cleanup.scope { + cir.call @doSomething(%0) : (!cir.ptr) -> () + cir.yield + } cleanup eh { + cir.call @dtor(%0) : (!cir.ptr) -> () + cir.yield + } + cir.return +} + +// CHECK-LABEL: cir.func @test_throwing_dtor_in_eh_cleanup() +// CHECK: cir.br ^[[BODY:bb[0-9]+]] +// +// Body: the call is replaced with try_call unwinding to the EH handler. +// CHECK: ^[[BODY]]: +// CHECK: cir.try_call @doSomething(%{{.*}}) ^[[NORMAL:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]] +// CHECK: ^[[NORMAL]]: +// CHECK: cir.br ^[[CONTINUE:bb[0-9]+]] +// +// Unwind block for body throwing call. +// CHECK: ^[[UNWIND]]: +// CHECK: %[[ET1:.*]] = cir.eh.initiate cleanup : !cir.eh_token +// CHECK: cir.br ^[[EH_CLEANUP:bb[0-9]+]](%[[ET1]] : !cir.eh_token) +// +// EH cleanup block: the dtor call is replaced with try_call unwinding to terminate. +// CHECK: ^[[EH_CLEANUP]](%[[ET2:.*]]: !cir.eh_token): +// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET2]] +// CHECK: cir.try_call @dtor(%{{.*}}) ^[[DTOR_NORMAL:bb[0-9]+]], ^[[TERMINATE:bb[0-9]+]] +// CHECK: ^[[DTOR_NORMAL]]: +// CHECK: cir.end_cleanup %[[CT]] +// CHECK: cir.resume %[[ET2]] +// +// Terminate block: cir.eh.initiate + cir.eh.terminate. +// CHECK: ^[[TERMINATE]]: +// CHECK: %[[TET:.*]] = cir.eh.initiate : !cir.eh_token +// CHECK: cir.eh.terminate %[[TET]] : !cir.eh_token +// +// CHECK: ^[[CONTINUE]]: +// CHECK: cir.return + + +// Test that a throwing call in an "all" (Normal+EH) cleanup region is handled. +// On the EH cleanup path, the call is replaced with try_call → terminate. +// On the normal cleanup path, the call remains a regular cir.call. +cir.func @test_throwing_dtor_in_all_cleanup() { + %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c", init] {alignment = 4 : i64} + cir.call @ctor(%0) : (!cir.ptr) -> () + cir.cleanup.scope { + cir.call @doSomething(%0) : (!cir.ptr) -> () + cir.yield + } cleanup all { + cir.call @dtor(%0) : (!cir.ptr) -> () + cir.yield + } + cir.return +} + +// CHECK-LABEL: cir.func @test_throwing_dtor_in_all_cleanup() +// CHECK: cir.br ^[[BODY:bb[0-9]+]] +// +// Body try_call. +// CHECK: ^[[BODY]]: +// CHECK: cir.try_call @doSomething(%{{.*}}) ^[[NORMAL_BODY:bb[0-9]+]], ^[[UNWIND:bb[0-9]+]] +// CHECK: ^[[NORMAL_BODY]]: +// +// Normal cleanup: dtor is a regular call (not try_call). +// CHECK: cir.call @dtor(%{{.*}}) +// CHECK: cir.br ^{{bb[0-9]+}} +// +// EH cleanup path: dtor is try_call unwinding to terminate. +// CHECK: ^{{bb[0-9]+}}(%[[ET2:.*]]: !cir.eh_token): +// CHECK: %[[CT:.*]] = cir.begin_cleanup %[[ET2]] +// CHECK: cir.try_call @dtor(%{{.*}}) ^[[DTOR_NORMAL:bb[0-9]+]], ^[[TERMINATE:bb[0-9]+]] +// CHECK: ^[[DTOR_NORMAL]]: +// CHECK: cir.end_cleanup %[[CT]] +// CHECK: cir.resume %[[ET2]] +// +// Terminate block. +// CHECK: ^[[TERMINATE]]: +// CHECK: %[[TET:.*]] = cir.eh.initiate : !cir.eh_token +// CHECK: cir.eh.terminate %[[TET]] : !cir.eh_token + + +// Test throwing call in EH cleanup within a try operation. +cir.func @test_throwing_dtor_in_eh_cleanup_in_try() { + %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c", init] {alignment = 4 : i64} + cir.call @ctor(%0) : (!cir.ptr) -> () + cir.try { + cir.cleanup.scope { + cir.call @doSomething(%0) : (!cir.ptr) -> () + cir.yield + } cleanup eh { + cir.call @dtor(%0) : (!cir.ptr) -> () + cir.yield + } + cir.yield + } catch all (%eh_token : !cir.eh_token) { + %catch_token, %1 = cir.begin_catch %eh_token : !cir.eh_token -> (!cir.catch_token, !cir.ptr) + cir.end_catch %catch_token : !cir.catch_token + cir.yield + } + cir.return +} + +// CHECK-LABEL: cir.func @test_throwing_dtor_in_eh_cleanup_in_try() +// +// Body try_call unwinding to EH cleanup. +// CHECK: cir.try_call @doSomething(%{{.*}}) ^{{bb[0-9]+}}, ^{{bb[0-9]+}} +// +// EH cleanup: dtor is try_call unwinding to terminate. +// CHECK: cir.try_call @dtor(%{{.*}}) ^{{bb[0-9]+}}, ^[[TERMINATE:bb[0-9]+]] +// +// Terminate block. +// CHECK: ^[[TERMINATE]]: +// CHECK: %[[TET:.*]] = cir.eh.initiate : !cir.eh_token +// CHECK: cir.eh.terminate %[[TET]] : !cir.eh_token + + +// Test multiple throwing calls in the cleanup region. +cir.func @test_multiple_throwing_calls_in_cleanup() { + %0 = cir.alloca !rec_SomeClass, !cir.ptr, ["c1", init] {alignment = 4 : i64} + %1 = cir.alloca !rec_SomeClass, !cir.ptr, ["c2", init] {alignment = 4 : i64} + cir.call @ctor(%0) : (!cir.ptr) -> () + cir.call @ctor(%1) : (!cir.ptr) -> () + cir.cleanup.scope { + cir.call @doSomething(%0) : (!cir.ptr) -> () + cir.yield + } cleanup eh { + cir.call @dtor(%1) : (!cir.ptr) -> () + cir.call @dtor(%0) : (!cir.ptr) -> () + cir.yield + } + cir.return +} + +// CHECK-LABEL: cir.func @test_multiple_throwing_calls_in_cleanup() +// +// EH cleanup: both dtors are try_calls unwinding to the same terminate block. +// CHECK: cir.try_call @dtor(%{{.*}}) ^[[DTOR1_NORMAL:bb[0-9]+]], ^[[TERMINATE:bb[0-9]+]] +// CHECK: ^[[DTOR1_NORMAL]]: +// CHECK: cir.try_call @dtor(%{{.*}}) ^[[DTOR2_NORMAL:bb[0-9]+]], ^[[TERMINATE]] +// CHECK: ^[[DTOR2_NORMAL]]: +// CHECK: cir.end_cleanup +// CHECK: cir.resume +// +// Shared terminate block for both calls. +// CHECK: ^[[TERMINATE]]: +// CHECK: %[[TET:.*]] = cir.eh.initiate : !cir.eh_token +// CHECK: cir.eh.terminate %[[TET]] : !cir.eh_token + + +cir.func private @ctor(!cir.ptr) +cir.func private @dtor(!cir.ptr) +cir.func private @doSomething(!cir.ptr)