[CIR] Handle throwing calls inside EH cleanup (#188341)

This implements handling for throwing calls inside an EH cleanup
handler. When such a call occurs, the CFG flattening pass replaces it
with a cir.try_call op that unwinds to a terminate block.

A new CIR operation, cir.eh.terminate, is added to facilitate this
handling, and the design document is updated to describe the new
behavior.

Assisted-by: Cursor / claude-4.6-opus-high
This commit is contained in:
Andy Kaylor 2026-03-30 13:44:11 -07:00 committed by GitHub
parent b6e4d27c48
commit f7329189c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 579 additions and 135 deletions

View File

@ -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<!rec_ThrowingDtor>, ["c", init]
cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
cir.cleanup.scope {
cir.call @_ZN12ThrowingDtor11doSomethingEv(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
cir.yield
} cleanup all {
cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
cir.yield
}
cir.return
}
```
**Flattened CIR**
```
cir.func @someFunc(){
%0 = cir.alloca !rec_ThrowingDtor, !cir.ptr<!rec_ThrowingDtor>, ["c", init]
cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
cir.try_call @_ZN12ThrowingDtor11doSomethingEv(%0) ^bb1, ^bb2 : (!cir.ptr<!rec_ThrowingDtor>) -> ()
^bb1 // Normal cleanup
cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
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<!rec_ThrowingDtor>) -> ()
^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`.

View File

@ -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
//===----------------------------------------------------------------------===//

View File

@ -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<!void>, !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<mlir::Operation *> &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<cir::FuncOp>("__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<cir::EhTerminateOp>(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<cir::ResumeOp>(user)) {
auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken());
builder.setInsertionPoint(op);

View File

@ -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<cir::CleanupScopeOp> {
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<cir::CallOp> 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<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;
int nextId = 0;

View File

@ -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<!rec_ThrowingDtor>, ["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<!rec_ThrowingDtor>, ["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

View File

@ -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<!rec_SomeClass>, ["c", init] {alignment = 4 : i64}
cir.call @ctor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.try_call @doSomething(%0) ^bb1, ^bb2 : (!cir.ptr<!rec_SomeClass>) -> ()
^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<!rec_SomeClass>) -> ()
^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<!void>, !u32i)
//
// Cleanup block: begin_cleanup removed, try_call to throwing_dtor.
// CHECK: ^[[CLEANUP]](%[[C_EXN:.*]]: !cir.ptr<!void>, %[[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<!void>)
// 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<!void>) 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<!rec_SomeClass>)
cir.func private @dtor(!cir.ptr<!rec_SomeClass>) attributes {nothrow}
cir.func private @throwing_dtor(!cir.ptr<!rec_SomeClass>)
cir.func private @doSomething(!cir.ptr<!rec_SomeClass>)
cir.global "private" constant external @_ZTISt9exception : !cir.ptr<!u8i>
cir.global "private" constant external @_ZTIi : !cir.ptr<!u8i>

View File

@ -5,92 +5,6 @@
!u8i = !cir.int<u, 8>
!void = !cir.void
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>) -> ()
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 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<!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 @ctor(%1) : (!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 {
cir.call @dtor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.yield
}
cir.yield
} cleanup normal {
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 a nested Normal+EH cleanup scope (the dtor is not nothrow).
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>) -> ()
cir.cleanup.scope {
cir.call @ctor(%1) : (!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 {
cir.call @dtor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.yield
}
cir.yield
} cleanup normal {
cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
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<!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>)

View File

@ -0,0 +1,166 @@
// 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}>
// 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<!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) : (!cir.ptr<!rec_SomeClass>) -> ()
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<!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) : (!cir.ptr<!rec_SomeClass>) -> ()
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<!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) : (!cir.ptr<!rec_SomeClass>) -> ()
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.void>)
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<!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.call @ctor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.cleanup.scope {
cir.call @doSomething(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.yield
} cleanup eh {
cir.call @dtor(%1) : (!cir.ptr<!rec_SomeClass>) -> ()
cir.call @dtor(%0) : (!cir.ptr<!rec_SomeClass>) -> ()
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<!rec_SomeClass>)
cir.func private @dtor(!cir.ptr<!rec_SomeClass>)
cir.func private @doSomething(!cir.ptr<!rec_SomeClass>)