Skip to content

Commit f732918

Browse files
authored
[CIR] Handle throwing calls inside EH cleanup (llvm#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
1 parent b6e4d27 commit f732918

8 files changed

Lines changed: 579 additions & 135 deletions

File tree

clang/docs/ClangIRCleanupAndEHDesign.md

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -732,9 +732,9 @@ CIR operations. The operations that were used in the ClangIR incubator
732732
project were closely matched to the Itanium exception handling ABI. In
733733
order to achieve a representation that also works well for other ABIs,
734734
the following new operations are being proposed: `cir.eh.initiate`,
735-
`cir.eh.dispatch`, `cir.begin_cleanup`, and `cir.end_cleanup`. The
736-
`cir.begin_catch` and `cir.end_catch` operations, described above,
737-
are also used in the flattened form.
735+
`cir.eh.dispatch`, `cir.eh.terminate`, `cir.begin_cleanup`, and
736+
`cir.end_cleanup`. The `cir.begin_catch` and `cir.end_catch` operations,
737+
described above, are also used in the flattened form.
738738

739739
Any time a cir.call operation that may throw and exception appears
740740
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
953953
cleanup, but because we have no catch handler, we execute `cir.resume`
954954
after the cleanup to unwind to the function that called `someFunc()`.
955955

956+
#### Throwing Calls in Cleanup Regions
957+
958+
When a call in an EH cleanup region may throw an exception, it requires
959+
special handling. The C++ standard requires that if an exception is
960+
thrown during exception cleanup (i.e., while unwinding a previous
961+
exception), the program must call `std::terminate()`. In the flattened
962+
CIR, such calls are replaced with `cir.try_call` operations whose
963+
unwind destination contains a `cir.eh.initiate` followed by a
964+
`cir.eh.terminate` operation.
965+
966+
The `cir.eh.terminate` operation is a terminator that signals the need
967+
for program termination due to an exception thrown during cleanup. It
968+
takes the `!cir.eh_token` returned by `cir.eh.initiate` and is further
969+
processed during EH ABI lowering, where it is replaced with target-specific
970+
termination code.
971+
972+
#### Example: Cleanup with throwing destructor
973+
974+
**C++**
975+
976+
``` c++
977+
struct ThrowingDtor {
978+
~ThrowingDtor() noexcept(false);
979+
};
980+
981+
void someFunc() {
982+
ThrowingDtor c;
983+
c.doSomething();
984+
}
985+
```
986+
987+
**CIR**
988+
989+
```
990+
cir.func @someFunc(){
991+
%0 = cir.alloca !rec_ThrowingDtor, !cir.ptr<!rec_ThrowingDtor>, ["c", init]
992+
cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
993+
cir.cleanup.scope {
994+
cir.call @_ZN12ThrowingDtor11doSomethingEv(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
995+
cir.yield
996+
} cleanup all {
997+
cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
998+
cir.yield
999+
}
1000+
cir.return
1001+
}
1002+
```
1003+
1004+
**Flattened CIR**
1005+
1006+
```
1007+
cir.func @someFunc(){
1008+
%0 = cir.alloca !rec_ThrowingDtor, !cir.ptr<!rec_ThrowingDtor>, ["c", init]
1009+
cir.call @_ZN12ThrowingDtorC1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
1010+
cir.try_call @_ZN12ThrowingDtor11doSomethingEv(%0) ^bb1, ^bb2 : (!cir.ptr<!rec_ThrowingDtor>) -> ()
1011+
^bb1 // Normal cleanup
1012+
cir.call @_ZN12ThrowingDtorD1Ev(%0) : (!cir.ptr<!rec_ThrowingDtor>) -> ()
1013+
cir.br ^bb6
1014+
^bb2 // EH cleanup (from entry block)
1015+
%1 = cir.eh.initiate cleanup : !cir.eh_token
1016+
cir.br ^bb3(%1 : !cir.eh_token)
1017+
^bb3(%eh_token : !cir.eh_token) // Perform cleanup
1018+
%2 = cir.begin_cleanup(%eh_token : !cir.eh_token) : !cir.cleanup_token
1019+
cir.try_call @_ZN12ThrowingDtorD1Ev(%0) ^bb4, ^bb5 : (!cir.ptr<!rec_ThrowingDtor>) -> ()
1020+
^bb4 // Destructor completed: continue unwinding
1021+
cir.end_cleanup(%2 : !cir.cleanup_token)
1022+
cir.resume %eh_token : !cir.eh_token
1023+
^bb5 // Destructor threw: terminate
1024+
%3 = cir.eh.initiate : !cir.eh_token
1025+
cir.eh.terminate %3 : !cir.eh_token
1026+
^bb6 // Normal continue (from ^bb1)
1027+
cir.return
1028+
}
1029+
```
1030+
1031+
In this example, the destructor for `ThrowingDtor` may throw. In the
1032+
normal cleanup path (`^bb1`), the destructor is a regular `cir.call`
1033+
since the exception would propagate normally. In the EH cleanup path
1034+
(`^bb3`), the destructor call is a `cir.try_call` because if the
1035+
destructor throws during exception unwinding, the program must
1036+
terminate. If the destructor completes normally, the exception
1037+
continues unwinding via `cir.resume`. If the destructor throws, control
1038+
transfers to `^bb5`, which initiates exception handling and immediately
1039+
terminates.
1040+
9561041
#### Example: Shared cleanups
9571042
9581043
**C++**
@@ -1142,7 +1227,9 @@ The Itanium exception handling ABI representation replaces the
11421227
for the catch handlers. The `cir.begin_cleanup` and `cir.end_cleanup`
11431228
operations are simply dropped. The `cir.begin_catch` operation becomes a
11441229
call to `__cxa_begin_catch`. The `cir.end_catch` operation becomes a
1145-
call to `__cxa_end_catch`.
1230+
call to `__cxa_end_catch`. The `cir.eh.terminate` operation becomes a
1231+
call to `__clang_call_terminate` (which calls `__cxa_begin_catch`
1232+
followed by `std::terminate()`) and then an unreachable operation.
11461233

11471234
The only operation that is specific to Itanium exception handling is
11481235
`cir.eh.landingpad`.

clang/include/clang/CIR/Dialect/IR/CIROps.td

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7037,6 +7037,46 @@ def CIR_EhInitiateOp : CIR_Op<"eh.initiate"> {
70377037
let hasLLVMLowering = false;
70387038
}
70397039

7040+
//===----------------------------------------------------------------------===//
7041+
// Flattened EH Operations: EhTerminateOp
7042+
//===----------------------------------------------------------------------===//
7043+
7044+
def CIR_EhTerminateOp : CIR_Op<"eh.terminate", [
7045+
Terminator
7046+
]> {
7047+
let summary = "Terminate due to exception thrown during cleanup";
7048+
let description = [{
7049+
`cir.eh.terminate` terminates program execution when an exception is thrown
7050+
while executing cleanup code during exception unwinding. The C++ standard
7051+
requires that `std::terminate()` be called in this scenario.
7052+
7053+
This operation takes an `!cir.eh_token` from a `cir.eh.initiate` operation
7054+
and acts as a terminator. It is produced during CFG flattening when throwing
7055+
calls are found in EH cleanup regions.
7056+
7057+
During EH ABI lowering, this is replaced with target-specific termination
7058+
code. For the Itanium ABI, the `cir.eh.initiate` is lowered to
7059+
`cir.eh.inflight_exception` (producing an exception pointer), and the
7060+
`cir.eh.terminate` becomes a call to `__clang_call_terminate` with that
7061+
pointer, followed by an unreachable operation.
7062+
7063+
Example:
7064+
7065+
```mlir
7066+
^terminate_unwind:
7067+
%eh_token = cir.eh.initiate : !cir.eh_token
7068+
cir.eh.terminate %eh_token : !cir.eh_token
7069+
```
7070+
}];
7071+
7072+
let arguments = (ins CIR_EhTokenType:$eh_token);
7073+
let assemblyFormat = [{
7074+
$eh_token `:` type($eh_token) attr-dict
7075+
}];
7076+
7077+
let hasLLVMLowering = false;
7078+
}
7079+
70407080
//===----------------------------------------------------------------------===//
70417081
// Flattened EH Operations: EhDispatchOp
70427082
//===----------------------------------------------------------------------===//

clang/lib/CIR/Dialect/Transforms/EHABILowering.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
// - cir.end_cleanup → (removed)
1818
// - cir.begin_catch → call to __cxa_begin_catch
1919
// - cir.end_catch → call to __cxa_end_catch
20+
// - cir.eh.terminate → call to __clang_call_terminate + unreachable
2021
// - cir.resume → cir.resume.flat
2122
// - !cir.eh_token values → (!cir.ptr<!void>, !u32i) value pairs
2223
// - personality function set on functions requiring EH
@@ -116,11 +117,13 @@ class ItaniumEHLowering : public EHABILowering {
116117
cir::FuncOp personalityFunc;
117118
cir::FuncOp beginCatchFunc;
118119
cir::FuncOp endCatchFunc;
120+
cir::FuncOp clangCallTerminateFunc;
119121

120122
constexpr const static ::llvm::StringLiteral kGxxPersonality =
121123
"__gxx_personality_v0";
122124

123125
void ensureRuntimeDecls(mlir::Location loc);
126+
void ensureClangCallTerminate(mlir::Location loc);
124127
mlir::LogicalResult lowerFunc(cir::FuncOp funcOp);
125128
void lowerEhInitiate(cir::EhInitiateOp initiateOp, EhTokenMap &ehTokenMap,
126129
SmallVectorImpl<mlir::Operation *> &deadOps);
@@ -172,6 +175,62 @@ void ItaniumEHLowering::ensureRuntimeDecls(mlir::Location loc) {
172175
}
173176
}
174177

178+
/// Ensure the __clang_call_terminate function exists in the module. This
179+
/// function is defined with a body that calls __cxa_begin_catch followed by
180+
/// std::terminate, matching the behavior of Clang's LLVM IR codegen.
181+
///
182+
/// void __clang_call_terminate(void *exn) nounwind noreturn {
183+
/// __cxa_begin_catch(exn);
184+
/// std::terminate();
185+
/// unreachable;
186+
/// }
187+
void ItaniumEHLowering::ensureClangCallTerminate(mlir::Location loc) {
188+
if (clangCallTerminateFunc)
189+
return;
190+
191+
ensureRuntimeDecls(loc);
192+
193+
if (auto existing = mod.lookupSymbol<cir::FuncOp>("__clang_call_terminate")) {
194+
clangCallTerminateFunc = existing;
195+
return;
196+
}
197+
198+
auto funcTy = cir::FuncType::get({voidPtrType}, voidType, /*isVarArg=*/false);
199+
builder.setInsertionPointToEnd(mod.getBody());
200+
auto funcOp =
201+
cir::FuncOp::create(builder, loc, "__clang_call_terminate", funcTy);
202+
funcOp.setLinkage(cir::GlobalLinkageKind::LinkOnceODRLinkage);
203+
funcOp.setGlobalVisibilityAttr(
204+
cir::VisibilityAttr::get(ctx, cir::VisibilityKind::Hidden));
205+
206+
mlir::Block *entryBlock = funcOp.addEntryBlock();
207+
builder.setInsertionPointToStart(entryBlock);
208+
mlir::Value exnArg = entryBlock->getArgument(0);
209+
210+
auto catchCall = cir::CallOp::create(
211+
builder, loc, mlir::FlatSymbolRefAttr::get(beginCatchFunc), u8PtrType,
212+
mlir::ValueRange{exnArg});
213+
catchCall.setNothrowAttr(builder.getUnitAttr());
214+
215+
auto terminateFuncDecl = getOrCreateRuntimeFuncDecl(
216+
mod, loc, "_ZSt9terminatev",
217+
cir::FuncType::get({}, voidType, /*isVarArg=*/false));
218+
terminateFuncDecl->setAttr(cir::CIRDialect::getNoReturnAttrName(),
219+
builder.getUnitAttr());
220+
auto terminateCall = cir::CallOp::create(
221+
builder, loc, mlir::FlatSymbolRefAttr::get(terminateFuncDecl), voidType,
222+
mlir::ValueRange{});
223+
terminateCall.setNothrowAttr(builder.getUnitAttr());
224+
terminateCall->setAttr(cir::CIRDialect::getNoReturnAttrName(),
225+
builder.getUnitAttr());
226+
227+
cir::UnreachableOp::create(builder, loc);
228+
229+
funcOp->setAttr(cir::CIRDialect::getNoReturnAttrName(),
230+
builder.getUnitAttr());
231+
clangCallTerminateFunc = funcOp;
232+
}
233+
175234
/// Lower all EH operations in a single function.
176235
mlir::LogicalResult ItaniumEHLowering::lowerFunc(cir::FuncOp funcOp) {
177236
if (funcOp.isDeclaration())
@@ -360,6 +419,19 @@ void ItaniumEHLowering::lowerEhInitiate(
360419
auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken());
361420
lowerDispatch(op, exnPtr, typeId, deadOps);
362421
}
422+
} else if (auto op = mlir::dyn_cast<cir::EhTerminateOp>(user)) {
423+
auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken());
424+
ensureClangCallTerminate(op.getLoc());
425+
builder.setInsertionPoint(op);
426+
auto call = cir::CallOp::create(
427+
builder, op.getLoc(),
428+
mlir::FlatSymbolRefAttr::get(clangCallTerminateFunc), voidType,
429+
mlir::ValueRange{exnPtr});
430+
call.setNothrowAttr(builder.getUnitAttr());
431+
call->setAttr(cir::CIRDialect::getNoReturnAttrName(),
432+
builder.getUnitAttr());
433+
cir::UnreachableOp::create(builder, op.getLoc());
434+
op.erase();
363435
} else if (auto op = mlir::dyn_cast<cir::ResumeOp>(user)) {
364436
auto [exnPtr, typeId] = ehTokenMap.lookup(op.getEhToken());
365437
builder.setInsertionPoint(op);

clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,19 @@ static mlir::Block *buildUnwindBlock(mlir::Block *dest, bool hasCleanup,
717717
return unwindBlock;
718718
}
719719

720+
// Create a shared terminate unwind block for throwing calls in EH cleanup
721+
// regions. When an exception is thrown during cleanup (unwinding), the C++
722+
// standard requires that std::terminate() be called.
723+
static mlir::Block *buildTerminateUnwindBlock(mlir::Location loc,
724+
mlir::Block *insertBefore,
725+
mlir::PatternRewriter &rewriter) {
726+
mlir::Block *terminateBlock = rewriter.createBlock(insertBefore);
727+
rewriter.setInsertionPointToEnd(terminateBlock);
728+
auto ehInitiate = cir::EhInitiateOp::create(rewriter, loc, /*cleanup=*/false);
729+
cir::EhTerminateOp::create(rewriter, loc, ehInitiate.getEhToken());
730+
return terminateBlock;
731+
}
732+
720733
class CIRCleanupScopeOpFlattening
721734
: public mlir::OpRewritePattern<cir::CleanupScopeOp> {
722735
public:
@@ -1332,6 +1345,28 @@ class CIRCleanupScopeOpFlattening
13321345
replaceCallWithTryCall(callOp, unwindBlock, loc, rewriter);
13331346
}
13341347

1348+
// Handle throwing calls in EH cleanup blocks. When an exception is thrown
1349+
// during cleanup code that runs on the exception unwind path, the C++
1350+
// standard requires that std::terminate() be called. Replace such calls
1351+
// with try_call operations that unwind to a terminate block containing
1352+
// cir.eh.initiate + cir.eh.terminate.
1353+
if (ehCleanupEntry) {
1354+
llvm::SmallVector<cir::CallOp> ehCleanupThrowingCalls;
1355+
for (mlir::Block *block = ehCleanupEntry; block != continueBlock;
1356+
block = block->getNextNode()) {
1357+
block->walk([&](cir::CallOp callOp) {
1358+
if (!callOp.getNothrow())
1359+
ehCleanupThrowingCalls.push_back(callOp);
1360+
});
1361+
}
1362+
if (!ehCleanupThrowingCalls.empty()) {
1363+
mlir::Block *terminateBlock =
1364+
buildTerminateUnwindBlock(loc, continueBlock, rewriter);
1365+
for (cir::CallOp callOp : ehCleanupThrowingCalls)
1366+
replaceCallWithTryCall(callOp, terminateBlock, loc, rewriter);
1367+
}
1368+
}
1369+
13351370
// Chain inner EH cleanup resume ops to this cleanup's EH handler.
13361371
// Each cir.resume from an already-flattened inner cleanup is replaced
13371372
// with a branch to the outer EH cleanup entry, passing the eh_token
@@ -1372,17 +1407,6 @@ class CIRCleanupScopeOpFlattening
13721407

13731408
cir::CleanupKind cleanupKind = cleanupOp.getCleanupKind();
13741409

1375-
// Throwing calls in the cleanup region of an EH-enabled cleanup scope
1376-
// are not yet supported. Such calls would need their own EH handling
1377-
// (e.g., terminate or nested cleanup) during the unwind path.
1378-
if (cleanupKind != cir::CleanupKind::Normal) {
1379-
llvm::SmallVector<cir::CallOp> cleanupThrowingCalls;
1380-
collectThrowingCalls(cleanupOp.getCleanupRegion(), cleanupThrowingCalls);
1381-
if (!cleanupThrowingCalls.empty())
1382-
return cleanupOp->emitError(
1383-
"throwing calls in cleanup region are not yet implemented");
1384-
}
1385-
13861410
// Collect all exits from the body region.
13871411
llvm::SmallVector<CleanupExit> exits;
13881412
int nextId = 0;

0 commit comments

Comments
 (0)