Author: Andy Kaylor Date: 2026-09-11T16:49:53-07:00 New Revision: f471750e042c5da3daf947169c0125e58300df0d
URL: https://github.com/llvm/llvm-project/commit/f471750e042c5da3daf947169c0125e58300df0d DIFF: https://github.com/llvm/llvm-project/commit/f471750e042c5da3daf947169c0125e58300df0d.diff LOG: [CIR] Document dynamic exception specification design (#222451) This change adds documentation for the CIR handling of dynamic exception specification handling in pre-C++17 code. This is not yet implemented. The implementation will follow in a series of changes based on this design. This documentation was generated by Claude Opus 5, based on an interactive planning session in which I worked out the details of how I want to implement this feature. I have carefully read the entire document to verify that it correctly matches my intended implementation. Added: Modified: clang/docs/CIR/CleanupAndEHDesign.md Removed: ################################################################################ diff --git a/clang/docs/CIR/CleanupAndEHDesign.md b/clang/docs/CIR/CleanupAndEHDesign.md index 52e7cd13d1d9b..de86de0697292 100644 --- a/clang/docs/CIR/CleanupAndEHDesign.md +++ b/clang/docs/CIR/CleanupAndEHDesign.md @@ -1596,3 +1596,454 @@ case will be handled by the personality function, using tables that are generated from the `cir.catchpad` operations. Each catch handler simply continues to the normal continuation block (`^bb6`) using the `cir.catchret` operation. + +## Dynamic Exception Specifications + +A dynamic exception specification (`throw(T...)`, and `throw()` before +C++17) constrains the set of exception types that a function is allowed +to propagate to its caller. If an exception of any other type would +escape the function, `std::unexpected()` must be called instead +([except.spec]). Dynamic exception specifications were removed in C++17, +so this representation is only produced for earlier language modes. +Functions declared `noexcept`, and `throw()` in C++17 and later, are handled + diff erently. + +Because the constraint applies to every exception that could escape the +function, it is represented as an exception handler that encloses the +entire function body. This section describes that representation used by the +high-level CIR produced by CIR generation, the flattened form produced by `cir::FlattenCFG`, and the ABI-specific form produced by EH ABI lowering. + +### High-level CIR representation + +A function with a dynamic exception specification has its entire body +wrapped in a `cir.try` operation with two handlers, a `filter` handler that +holds the path taken when the in-flight exception is permitted, and an +`unexpected` handler, identified that holds the path taken when the +specification is violated. + +```mlir +cir.try { + // function body + cir.yield +} filter [@_ZTIi] (%eh_token : !cir.eh_token) { + cir.resume %eh_token : !cir.eh_token +} unexpected (%eh_token.1 : !cir.eh_token) { + cir.eh.unexpected %eh_token.1 : !cir.eh_token +} +``` + +Both attributes occupy a slot in the try operation's handler type list, +in the same way that a `#cir.global_view` catch type, `catch all`, or +`unwind` does. Like `unwind`, and unlike a catch handler, neither region +begins with `cir.begin_catch`. Neither handler catches the exception. +Together they only decide whether the exception is permitted to continue +unwinding. + +The test that decides whether the in-flight exception matches the filter +is implicit in the handler type, in the same way that the type test for +a catch handler is implicit in its `#cir.global_view` handler type. +Neither test is expressed in a handler region. Both are materialized +during ABI lowering. The two regions therefore describe only the +outcomes of that test. The filter region contains a single `cir.resume` +operation to continue unwinding to the caller, and the unexpected region +contains a single `cir.eh.unexpected` operation. + +The `cir.eh.unexpected` operation is a terminator that signals that the +in-flight exception violated the exception specification of the +enclosing function and that `std::unexpected()` must be called. Like +`cir.eh.terminate`, it takes an `!cir.eh_token`, it is ABI-agnostic, and +it is replaced with target-specific code during EH ABI lowering. + +A filter handler and an unexpected handler must appear together, with +the filter first, and the two must be the only handlers on the try +operation. The filter try operation wraps the entire function body and +exists only to check the exception specification, while each try +statement written in the source becomes a separate `cir.try` operation +nested inside it. A function-try-block on a function that also has an +exception specification is nested the same way. + +An empty type list represents `throw()` before C++17. No exception is +permitted by such a specification, so there is no permitted path to +describe and the filter region is terminated with `cir.unreachable` +instead of `cir.resume`. This matches Clang's LLVM IR codegen, which +generates no resume path at all for a function whose exception +specification permits nothing. The unexpected region is the same in +both cases. + +#### Example: Simple dynamic exception specification + +**C++** + +```c++ +void external(); + +void target() throw(int) { + external(); +} + +void target2() throw() { + external(); +} +``` + +**CIR** + +```mlir +cir.func @_Z6targetv() personality(@__gxx_personality_v0) { + cir.try { + cir.call @_Z8externalv() : () -> () + cir.yield + } filter [@_ZTIi] (%eh_token : !cir.eh_token) { + cir.resume %eh_token : !cir.eh_token + } unexpected (%eh_token.1 : !cir.eh_token) { + cir.eh.unexpected %eh_token.1 : !cir.eh_token + } + cir.return +} + +cir.func @_Z7target2v() personality(@__gxx_personality_v0) + attributes {nothrow} { + cir.try { + cir.call @_Z8externalv() : () -> () + cir.yield + } filter [] (%eh_token : !cir.eh_token) { + cir.unreachable + } unexpected (%eh_token.1 : !cir.eh_token) { + cir.eh.unexpected %eh_token.1 : !cir.eh_token + } + cir.return +} +``` + +In `target()`, if `external()` throws an `int`, the exception is +permitted by the specification and unwinding continues to the caller +through the filter handler's `cir.resume` operation. If it throws any +other type, the specification is violated and the unexpected handler +calls `std::unexpected()`. + +In `target2()`, the specification permits nothing, so any exception +thrown by `external()` violates it and the unexpected handler is always +the one reached. There is no permitted path, which is why the filter +region holds a `cir.unreachable` rather than a `cir.resume`. The +function itself is marked `nothrow`, because no exception can escape it. + +#### Example: Try-catch within an exception specification + +**C++** + +```c++ +void external(); + +void inner() throw(int) { + external(); +} + +void outer() throw() { + try { + inner(); + } catch (int) { + } +} +``` + +**CIR** + +```mlir +cir.func @_Z5innerv() personality(@__gxx_personality_v0) { + cir.try { + cir.call @_Z8externalv() : () -> () + cir.yield + } filter [@_ZTIi] (%eh_token : !cir.eh_token) { + cir.resume %eh_token : !cir.eh_token + } unexpected (%eh_token.1 : !cir.eh_token) { + cir.eh.unexpected %eh_token.1 : !cir.eh_token + } + cir.return +} + +cir.func @_Z5outerv() personality(@__gxx_personality_v0) + attributes {nothrow} { + cir.try { + cir.scope { + %0 = cir.alloca "" align(4) : !cir.ptr<!s32i> + cir.try { + cir.call @_Z5innerv() : () -> () + cir.yield + } catch [type #cir.global_view<@_ZTIi> : !cir.ptr<!u8i>] + (%eh_token : !cir.eh_token) { + %catch_token, %exn_ptr = cir.begin_catch %eh_token + : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>) + cir.cleanup.scope { + cir.init_catch_param scalar %exn_ptr to %0 + : !cir.ptr<!void>, !cir.ptr<!s32i> + cir.yield + } cleanup all { + cir.end_catch %catch_token : !cir.catch_token + cir.yield + } + cir.yield + } unwind (%eh_token.1 : !cir.eh_token) { + cir.resume %eh_token.1 : !cir.eh_token + } + } + cir.yield + } filter [] (%eh_token.2 : !cir.eh_token) { + cir.unreachable + } unexpected (%eh_token.3 : !cir.eh_token) { + cir.eh.unexpected %eh_token.3 : !cir.eh_token + } + cir.return +} +``` + +In this example the exception specification try operation encloses the +try-catch statement written in the source. + +If `inner()` throws an `int`, the inner try operation's catch handler +runs and execution continues after the try statement. The exception +specification of `outer()` is never consulted, because the exception +does not escape the function. + +If `inner()` throws any other type, the inner try operation's `unwind` +handler is reached. Its `cir.resume` operation exits the region of the +enclosing filter try operation, so, following the rules described above +for `cir.resume` within an enclosing scope, unwinding continues into +that operation's specification check rather than leaving the function. +The exception is checked against the specification of `outer()`, which +permits nothing, so the unexpected handler is reached and +`std::unexpected()` is called. + +### CFG Flattening + +Flattening a filter try operation introduces a `filter` clause on the +`cir.eh.dispatch` operation. Both handler regions become ordinary +blocks, so flattening only has to inline them and wire up the dispatch +operation's successors. It does not synthesize any new operation. + +```mlir +cir.eh.dispatch %eh_token : !cir.eh_token [ + // Taken when the exception is *not* one of the permitted types. + filter(@_ZTIi) : ^bb4, + // Taken when it is. + unwind : ^bb5 +] +``` + +A `filter` clause names the permitted types, but its destination is +taken on the types it does *not* name. This is the opposite polarity +from a `catch` clause, whose destination is taken when the exception +does match the named type, so the two clause kinds cannot be read the +same way. The polarity comes from the Itanium personality routine, +which reports a filter *failure* by selecting the filter clause of the +landing pad, and it is preserved in the flattened form so that the +dispatch operation maps directly onto the landing pad it lowers to. + +Unlike `catch_all` and `unwind`, a `filter` clause also does not take +the place of the dispatch operation's default destination. A filter has +two outgoing edges rather than one. Either the exception violates the +specification, in which case control transfers to the filter clause's +destination, or it does not, in which case control continues along the +dispatch operation's normal `unwind` edge. A `cir.eh.dispatch` operation +carrying a `filter` clause therefore always carries an `unwind` clause +as well. + +The two clauses correspond directly to the two handler regions, with the +polarity inversion visible in the pairing. The unexpected region becomes +the destination of the `filter` clause, and the filter region, which +describes the permitted path, becomes the `unwind` destination. + +```mlir +^bb4(%eh_token : !cir.eh_token): // Flattened unexpected region + cir.eh.unexpected %eh_token : !cir.eh_token +^bb5(%eh_token.1 : !cir.eh_token): // Flattened filter region + cir.resume %eh_token.1 : !cir.eh_token +``` + +Because the filter and unexpected handlers are the only handlers on the +try operation, such a dispatch never carries catch clauses of its own. +The catch clauses of a try statement nested inside the specification +belong to that statement's own dispatch operation, which is chained +ahead of this one. + +The shape is the same when the filter type list is empty. The filter +region's `cir.unreachable` becomes the `unwind` destination +and the dispatch operation still carries both clauses. ABI lowering then +makes the branch to the filter destination unconditional, which leaves +that `unwind` destination unreachable and dead. + +#### Example: Simple dynamic exception specification + +**High-level CIR** + +```mlir +cir.func @_Z6targetv() personality(@__gxx_personality_v0) { + cir.try { + cir.call @_Z8externalv() : () -> () + cir.yield + } filter [@_ZTIi] (%eh_token : !cir.eh_token) { + cir.resume %eh_token : !cir.eh_token + } unexpected (%eh_token.1 : !cir.eh_token) { + cir.eh.unexpected %eh_token.1 : !cir.eh_token + } + cir.return +} +``` + +**Flattened CIR** + +```mlir +cir.func @_Z6targetv() personality(@__gxx_personality_v0) { + cir.try_call @_Z8externalv() ^bb1, ^bb2 : () -> () +^bb1: // Normal continue (from entry block) + cir.br ^bb6 +^bb2: // EH (from entry block) + %0 = cir.eh.initiate : !cir.eh_token + cir.br ^bb3(%0 : !cir.eh_token) +^bb3(%eh_token : !cir.eh_token): // Exception specification dispatch + cir.eh.dispatch %eh_token : !cir.eh_token [ + filter(@_ZTIi) : ^bb4, // Not an int: specification violated + unwind : ^bb5 // An int: permitted, keep unwinding + ] +^bb4(%eh_token.1 : !cir.eh_token): // Specification violated + cir.eh.unexpected %eh_token.1 : !cir.eh_token +^bb5(%eh_token.2 : !cir.eh_token): // Exception is permitted + cir.resume %eh_token.2 : !cir.eh_token +^bb6: // Normal continue (from ^bb1) + cir.return +} +``` + +If `external()` throws, control transfers to `^bb2`, which initiates +exception handling and branches to the dispatch block (`^bb3`). If the +exception is not one of the permitted types, control transfers to `^bb4` +and `cir.eh.unexpected` terminates the block. Otherwise control +transfers to `^bb5` and unwinding continues to the caller. + +### Itanium ABI Lowering + +The Itanium representation of an exception specification is a `filter` +clause on the landing pad. Accordingly, the `cir.eh.inflight_exception` +operation gains a `filter` clause carrying the permitted type info +symbols. + +```mlir +%exception_ptr, %type_id = cir.eh.inflight_exception filter [@_ZTIi] +``` + +This corresponds directly to the `filter` clause of the LLVM IR +landingpad instruction. An empty list lowers to a zero-length filter +clause, which the personality routine treats as permitting nothing. + +The clause list of a landing pad is built from the handlers that the +exception reaching that landing pad can arrive at, listed innermost +first. Since the try operation for an exception specification encloses +the entire function body, its filter clause is always last, after any +catch clauses contributed by try operations written in the source. A +filter clause terminates the clause list in the same way that a +catch-all clause does, because no handler outside the function can be +reached. A filter clause and the `cleanup` attribute may both appear on +the same landing pad, since cleanups within the function still have to +run before the specification is checked. + +The `filter` clause of a `cir.eh.dispatch` operation is lowered to a +signed comparison of the type id against zero. The personality routine +reports that an exception failed a filter by returning a *negative* +selector value, which is why the type id produced by +`cir.eh.inflight_exception` is a signed integer. Catch matching only +compares the type id for equality and is therefore in diff erent to its +signedness, but filter checking is not. When the filter type list is +empty, no comparison is generated and the filter destination is branched +to unconditionally. + +The `cir.eh.unexpected` operation is lowered to a call to +`__cxa_call_unexpected`, marked `noreturn`, followed by a +`cir.unreachable` operation. + +#### Example: Simple dynamic exception specification + +**Flattened CIR** + +```mlir +cir.func @_Z6targetv() personality(@__gxx_personality_v0) { + cir.try_call @_Z8externalv() ^bb1, ^bb2 : () -> () +^bb1: // Normal continue (from entry block) + cir.br ^bb6 +^bb2: // EH (from entry block) + %0 = cir.eh.initiate : !cir.eh_token + cir.br ^bb3(%0 : !cir.eh_token) +^bb3(%eh_token : !cir.eh_token): // Exception specification dispatch + cir.eh.dispatch %eh_token : !cir.eh_token [ + filter(@_ZTIi) : ^bb4, // Not an int: specification violated + unwind : ^bb5 // An int: permitted, keep unwinding + ] +^bb4(%eh_token.1 : !cir.eh_token): // Specification violated + cir.eh.unexpected %eh_token.1 : !cir.eh_token +^bb5(%eh_token.2 : !cir.eh_token): // Exception is permitted + cir.resume %eh_token.2 : !cir.eh_token +^bb6: // Normal continue (from ^bb1) + cir.return +} +``` + +**ABI-lowered CIR** + +```mlir +cir.func @_Z6targetv() personality(@__gxx_personality_v0) { + cir.try_call @_Z8externalv() ^bb1, ^bb2 : () -> () +^bb1: // Normal continue (from entry block) + cir.br ^bb6 +^bb2: // Landing pad (from entry block) + %exception_ptr, %type_id = cir.eh.inflight_exception filter [@_ZTIi] + cir.br ^bb3(%exception_ptr, %type_id : !cir.ptr<!void>, !s32i) +^bb3(%0: !cir.ptr<!void>, %1: !s32i): // Exception specification dispatch + %2 = cir.const #cir.int<0> : !s32i + %3 = cir.cmp lt %1, %2 : !s32i + cir.brcond %3 ^bb4(%0 : !cir.ptr<!void>), + ^bb5(%0, %1 : !cir.ptr<!void>, !s32i) +^bb4(%4: !cir.ptr<!void>): // Specification violated + cir.call @__cxa_call_unexpected(%4) {noreturn} : (!cir.ptr<!void>) -> () + cir.unreachable +^bb5(%5: !cir.ptr<!void>, %6: !s32i): // Exception is permitted + cir.resume.flat %5, %6 +^bb6: // Normal continue (from ^bb1) + cir.return +} +``` + +In this example the landing pad (`^bb2`) carries a filter clause listing +the single permitted type. The personality routine selects that clause +only when the in-flight exception is *not* an `int`, and signals this by +returning a negative selector value. The dispatch block (`^bb3`) +therefore tests the selector for a negative value and transfers control +to `^bb4` to call `__cxa_call_unexpected` when the test succeeds, or to +`^bb5` to continue unwinding when it fails. + +For `target2()`, whose specification permits nothing, the landing pad +carries an empty filter clause and no comparison is needed. + +```mlir +^bb2: // Landing pad (from entry block) + %exception_ptr, %type_id = cir.eh.inflight_exception filter [] + cir.br ^bb3(%exception_ptr : !cir.ptr<!void>) +^bb3(%0: !cir.ptr<!void>): // Specification violated + cir.call @__cxa_call_unexpected(%0) {noreturn} : (!cir.ptr<!void>) -> () + cir.unreachable +``` + +In the try-catch example above, the exception thrown by `inner()` can +reach both the catch handler of the try statement in `outer()` and the +filter of the exception specification of `outer()`, so the landing pad +for the call to `inner()` carries both clauses, with the filter clause +last. + +```mlir +%exception_ptr, %type_id = + cir.eh.inflight_exception [@_ZTIi] filter [] +``` + +### Microsoft C++ ABI Lowering + +The Microsoft C++ ABI has no runtime support for dynamic exception +specifications. As in Clang's LLVM IR codegen, no exception +specification try operation is generated when targeting that ABI, and +the specification has no effect on the generated code. _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
