[clang] [llvm] [CIR] Document dynamic exception specification design (PR #222451)

Andy Kaylor via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 9 15:48:35 PDT 2026


================
@@ -1596,3 +1599,422 @@ 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
+differently.
+
+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 whose handler is a *filter* handler.
+A filter handler is identified by a `#cir.eh_filter` handler type
+attribute, which carries the list of type info symbols naming the
+permitted types.
+
+```mlir
+cir.try {
+  // function body
+  cir.yield
+} filter [@_ZTIi] (%eh_token : !cir.eh_token) {
+  cir.resume %eh_token : !cir.eh_token
+}
+```
+
+The `#cir.eh_filter` attribute occupies 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, a filter handler region does not begin with `cir.begin_catch`.
+A filter does not catch the exception. It only decides 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 the handler region. Both are materialized
+during CFG flattening and ABI lowering. The body of a filter handler
+region therefore contains only the code that runs when the exception
+*is* permitted by the specification, which is a single `cir.resume`
+operation to continue unwinding to the caller.
+
+A `cir.try` operation may have at most one filter handler, it must be
+the last handler in the handler list, and it may not be combined with a
+`catch all` handler, because a catch-all consumes every exception and
+nothing could reach the filter. The try operation created for an
+exception specification always has the filter as its only handler. A
+function-try-block on a function that also has an exception
+specification produces a separate `cir.try` operation nested inside it.
+
+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 handler 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.
+
+#### 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
+  }
+  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
+  }
+  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 `std::unexpected()` is
+called.
+
+In `target2()`, the specification permits nothing, so any exception
+thrown by `external()` violates it and `std::unexpected()` is always
+called. There is no permitted path, which is why the filter handler
+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
+  }
+  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
+  }
+  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 enclosing
+filter handler's try region, so, following the rules described above for
+`cir.resume` within an enclosing scope, unwinding continues into the
+filter handler rather than leaving the function. The exception is
+checked against the specification of `outer()`, which permits nothing,
+and `std::unexpected()` is called.
+
+### CFG Flattening
+
+Flattening a filter handler introduces a `filter` clause on the
+`cir.eh.dispatch` operation, and a new `cir.eh.unexpected` operation.
+
+```mlir
+cir.eh.dispatch %eh_token : !cir.eh_token [
+  filter(@_ZTIi) : ^bb4,
+  unwind : ^bb5
+]
+```
+
+Unlike `catch_all` and `unwind`, a `filter` clause 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 still requires a `catch_all` or
+`unwind` clause, and in practice always has an `unwind` clause, since
+the filter is only reached after every catch handler has failed to
+match.
+
+The filter handler region describes the permitted path, so it becomes
+the `unwind` destination of the dispatch operation. The destination of
+the `filter` clause is a new block, which contains a single
+`cir.eh.unexpected` operation.
+
+```mlir
+^bb4(%eh_token : !cir.eh_token):
+  cir.eh.unexpected %eh_token : !cir.eh_token
+```
+
+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 the `!cir.eh_token` produced by a preceding
+`cir.eh.initiate` operation, it is ABI-agnostic, and it is replaced with
+target-specific code during EH ABI lowering.
+
+The two cases have the same shape when the filter type list is empty.
+The handler 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
+  }
+  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,
+    unwind : ^bb5
+  ]
+^bb4(%eh_token.1 : !cir.eh_token): // Specification violated
+  cir.eh.unexpected %eh_token.1 : !cir.eh_token
----------------
andykaylor wrote:

I've updated the documentation to be more emphatic about this.

https://github.com/llvm/llvm-project/pull/222451


More information about the cfe-commits mailing list