[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:46:55 PDT 2026
https://github.com/andykaylor updated https://github.com/llvm/llvm-project/pull/222451
>From 66487c551fc46eba8fb4403ed0535df1ead08b70 Mon Sep 17 00:00:00 2001
From: Andy Kaylor <akaylor at nvidia.com>
Date: Wed, 9 Sep 2026 14:12:56 -0700
Subject: [PATCH 1/4] [CIR] Document dynamic exception specification design
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.
---
clang/docs/CIR/CleanupAndEHDesign.md | 419 +++++++++++++++++++++++++++
1 file changed, 419 insertions(+)
diff --git a/clang/docs/CIR/CleanupAndEHDesign.md b/clang/docs/CIR/CleanupAndEHDesign.md
index 092568f04b869..495bb72e957ca 100644
--- a/clang/docs/CIR/CleanupAndEHDesign.md
+++ b/clang/docs/CIR/CleanupAndEHDesign.md
@@ -1599,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
+^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 indifferent 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,
+ unwind : ^bb5
+ ]
+^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.
>From 9c858e48b28e4b1bcac045938e8d310946d0a7d8 Mon Sep 17 00:00:00 2001
From: Andy Kaylor <akaylor at nvidia.com>
Date: Wed, 9 Sep 2026 15:10:03 -0700
Subject: [PATCH 2/4] Require that filter clauses be the only handler on a try
op (when present)
---
clang/docs/CIR/CleanupAndEHDesign.md | 24 +++++++++-------
llvm/include/llvm/ABI/IRTypeMapper.h | 1 +
llvm/include/llvm/ABI/Types.h | 43 ++++++++++++++++++++++++++++
llvm/lib/ABI/IRTypeMapper.cpp | 9 ++++++
llvm/unittests/ABI/TypesTest.cpp | 32 +++++++++++++++++++++
5 files changed, 98 insertions(+), 11 deletions(-)
diff --git a/clang/docs/CIR/CleanupAndEHDesign.md b/clang/docs/CIR/CleanupAndEHDesign.md
index 495bb72e957ca..1d2192117061e 100644
--- a/clang/docs/CIR/CleanupAndEHDesign.md
+++ b/clang/docs/CIR/CleanupAndEHDesign.md
@@ -1649,13 +1649,12 @@ 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.
+If a `cir.try` operation has a filter handler, that filter must be its
+only handler. 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
@@ -1816,10 +1815,13 @@ 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.
+carrying a `filter` clause therefore always carries an `unwind` clause
+as well, whose destination is the flattened filter handler region.
+
+Because the filter is the only handler 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 filter handler region describes the permitted path, so it becomes
the `unwind` destination of the dispatch operation. The destination of
diff --git a/llvm/include/llvm/ABI/IRTypeMapper.h b/llvm/include/llvm/ABI/IRTypeMapper.h
index 0fbb9a550348b..cd8160489c705 100644
--- a/llvm/include/llvm/ABI/IRTypeMapper.h
+++ b/llvm/include/llvm/ABI/IRTypeMapper.h
@@ -44,6 +44,7 @@ class IRTypeMapper {
llvm::Type *convertArrayType(const abi::ArrayType *AT);
llvm::Type *convertVectorType(const abi::VectorType *VT);
+ llvm::Type *convertTupleType(const abi::TupleType *TT);
llvm::Type *convertRecordType(const abi::RecordType *RT);
llvm::Type *convertComplexType(const abi::ComplexType *CT);
llvm::Type *convertMemberPointerType(const abi::MemberPointerType *MPT);
diff --git a/llvm/include/llvm/ABI/Types.h b/llvm/include/llvm/ABI/Types.h
index 88b99e9e095d3..ffb2238137387 100644
--- a/llvm/include/llvm/ABI/Types.h
+++ b/llvm/include/llvm/ABI/Types.h
@@ -21,6 +21,7 @@
#include "llvm/Support/Allocator.h"
#include "llvm/Support/Compiler.h"
#include "llvm/Support/TypeSize.h"
+#include <cassert>
namespace llvm {
namespace abi {
@@ -34,6 +35,7 @@ enum class TypeKind {
Pointer,
Array,
Vector,
+ Tuple,
Record,
};
@@ -78,6 +80,7 @@ class Type {
bool isPointer() const { return Kind == TypeKind::Pointer; }
bool isArray() const { return Kind == TypeKind::Array; }
bool isVector() const { return Kind == TypeKind::Vector; }
+ bool isTuple() const { return Kind == TypeKind::Tuple; }
bool isRecord() const { return Kind == TypeKind::Record; }
bool isMemberPointer() const { return Kind == TypeKind::MemberPointer; }
bool isComplex() const { return Kind == TypeKind::Complex; }
@@ -231,6 +234,38 @@ class VectorType : public Type {
}
};
+/// A homogeneous tuple of 2, 3, or 4 identical vectors, such as the
+/// AArch64 SVE types svint32x3_t and svboolx2_t.
+///
+/// The contained vector describes one register-shaped member. Size and
+/// alignment of the tuple cover the whole group: size is NumVectors times
+/// the vector size, and alignment matches the contained vector.
+class TupleType : public Type {
+private:
+ const VectorType *Vec;
+ unsigned NumVectors;
+
+ static TypeSize computeSizeInBits(const VectorType *Vec,
+ unsigned NumVectors) {
+ TypeSize VecSize = Vec->getSizeInBits();
+ return TypeSize(VecSize.getKnownMinValue() * NumVectors,
+ VecSize.isScalable());
+ }
+
+public:
+ TupleType(const VectorType *Vec, unsigned NumVectors)
+ : Type(TypeKind::Tuple, computeSizeInBits(Vec, NumVectors),
+ Vec->getAlignment()),
+ Vec(Vec), NumVectors(NumVectors) {}
+
+ const VectorType *getVectorType() const { return Vec; }
+ unsigned getNumVectors() const { return NumVectors; }
+
+ static bool classof(const Type *T) {
+ return T->getKind() == TypeKind::Tuple;
+ }
+};
+
struct FieldInfo {
const Type *FieldType;
uint64_t OffsetInBits;
@@ -374,6 +409,14 @@ class TypeBuilder {
VectorType(ElementType, NumElements, Align);
}
+ /// Creates a homogeneous tuple of \p NumVectors copies of \p Vec.
+ /// \p NumVectors must be 2, 3, or 4.
+ const TupleType *getTupleType(const VectorType *Vec, unsigned NumVectors) {
+ assert(NumVectors >= 2 && NumVectors <= 4 &&
+ "tuple types hold 2, 3, or 4 vectors");
+ return new (Allocator.Allocate<TupleType>()) TupleType(Vec, NumVectors);
+ }
+
const RecordType *getRecordType(ArrayRef<FieldInfo> Fields, TypeSize Size,
Align Align,
StructPacking Pack = StructPacking::Default,
diff --git a/llvm/lib/ABI/IRTypeMapper.cpp b/llvm/lib/ABI/IRTypeMapper.cpp
index bcd133ae30c41..5d8d440299aab 100644
--- a/llvm/lib/ABI/IRTypeMapper.cpp
+++ b/llvm/lib/ABI/IRTypeMapper.cpp
@@ -52,6 +52,9 @@ llvm::Type *IRTypeMapper::convertType(const abi::Type *ABIType) {
case abi::TypeKind::Vector:
Result = convertVectorType(cast<abi::VectorType>(ABIType));
break;
+ case abi::TypeKind::Tuple:
+ Result = convertTupleType(cast<abi::TupleType>(ABIType));
+ break;
case abi::TypeKind::Record:
Result = convertRecordType(cast<abi::RecordType>(ABIType));
break;
@@ -81,6 +84,12 @@ llvm::Type *IRTypeMapper::convertVectorType(const abi::VectorType *VT) {
return llvm::VectorType::get(ElementType, VT->getNumElements());
}
+llvm::Type *IRTypeMapper::convertTupleType(const abi::TupleType *TT) {
+ llvm::Type *VecTy = convertType(TT->getVectorType());
+ SmallVector<llvm::Type *, 4> Elements(TT->getNumVectors(), VecTy);
+ return llvm::StructType::get(Context, Elements);
+}
+
llvm::Type *IRTypeMapper::convertRecordType(const abi::RecordType *RT) {
return createStructFromFields(RT->getFields(), RT->getSizeInBits(),
RT->getAlignment(), RT->isUnion());
diff --git a/llvm/unittests/ABI/TypesTest.cpp b/llvm/unittests/ABI/TypesTest.cpp
index 85a9d368b1e25..25a2bcf221292 100644
--- a/llvm/unittests/ABI/TypesTest.cpp
+++ b/llvm/unittests/ABI/TypesTest.cpp
@@ -14,12 +14,15 @@
#include "gtest/gtest.h"
using llvm::Align;
+using llvm::ElementCount;
using llvm::TypeSize;
using llvm::abi::FieldInfo;
using llvm::abi::RecordFlags;
using llvm::abi::RecordType;
using llvm::abi::StructPacking;
+using llvm::abi::TupleType;
using llvm::abi::TypeBuilder;
+using llvm::abi::VectorType;
namespace {
@@ -125,4 +128,33 @@ TEST_F(ABITypesTest, DirectVirtualBasesAndVTablePointer) {
->isEmpty());
}
+TEST_F(ABITypesTest, GenericVector) {
+ const llvm::abi::Type *I32 = TB.getIntegerType(32, Align(4), /*Signed=*/true);
+ const VectorType *V4I32 =
+ TB.getVectorType(I32, ElementCount::getFixed(4), Align(16));
+
+ EXPECT_FALSE(V4I32->isTuple());
+ EXPECT_EQ(V4I32->getNumElements(), ElementCount::getFixed(4));
+ EXPECT_EQ(V4I32->getSizeInBits(), TypeSize::getFixed(128));
+}
+
+// svint32x3_t is three <vscale x 4 x i32> vectors.
+TEST_F(ABITypesTest, VectorTuple) {
+ const llvm::abi::Type *I32 = TB.getIntegerType(32, Align(4), /*Signed=*/true);
+ const VectorType *SVInt32 =
+ TB.getVectorType(I32, ElementCount::getScalable(4), Align(16));
+ const TupleType *SVInt32x3 = TB.getTupleType(SVInt32, /*NumVectors=*/3);
+
+ EXPECT_TRUE(SVInt32x3->isTuple());
+ EXPECT_FALSE(SVInt32->isTuple());
+ EXPECT_EQ(SVInt32x3->getNumVectors(), 3u);
+ EXPECT_EQ(SVInt32x3->getVectorType(), SVInt32);
+ EXPECT_EQ(SVInt32x3->getAlignment(), Align(16));
+ // The contained vector keeps a per-vector element count; the tuple size
+ // covers all of the vectors.
+ EXPECT_EQ(SVInt32->getNumElements(), ElementCount::getScalable(4));
+ EXPECT_EQ(SVInt32->getSizeInBits(), TypeSize::getScalable(128));
+ EXPECT_EQ(SVInt32x3->getSizeInBits(), TypeSize::getScalable(384));
+}
+
} // namespace
>From 1b904b5f2817d97aba87dfbd62099783f1bc50ab Mon Sep 17 00:00:00 2001
From: Andy Kaylor <akaylor at nvidia.com>
Date: Wed, 9 Sep 2026 15:42:34 -0700
Subject: [PATCH 3/4] Add an explicit `unexpected` region for filter try ops
---
clang/docs/CIR/CleanupAndEHDesign.md | 129 +++++++++++++++------------
1 file changed, 74 insertions(+), 55 deletions(-)
diff --git a/clang/docs/CIR/CleanupAndEHDesign.md b/clang/docs/CIR/CleanupAndEHDesign.md
index 1d2192117061e..ef75870fccabf 100644
--- a/clang/docs/CIR/CleanupAndEHDesign.md
+++ b/clang/docs/CIR/CleanupAndEHDesign.md
@@ -1619,10 +1619,10 @@ high-level CIR produced by CIR generation, the flattened form produced by `cir::
### 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.
+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 {
@@ -1630,38 +1630,48 @@ cir.try {
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
}
```
-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.
+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 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.
-
-If a `cir.try` operation has a filter handler, that filter must be its
-only handler. 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.
+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 handler region is terminated with `cir.unreachable`
+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.
+specification permits nothing. The unexpected region is the same in
+both cases.
#### Example: Simple dynamic exception specification
@@ -1688,6 +1698,8 @@ cir.func @_Z6targetv() personality(@__gxx_personality_v0) {
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
}
@@ -1699,6 +1711,8 @@ cir.func @_Z7target2v() personality(@__gxx_personality_v0)
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
}
@@ -1707,12 +1721,12 @@ cir.func @_Z7target2v() personality(@__gxx_personality_v0)
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.
+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 `std::unexpected()` is always
-called. There is no permitted path, which is why the filter handler
+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.
@@ -1744,6 +1758,8 @@ cir.func @_Z5innerv() personality(@__gxx_personality_v0) {
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
}
@@ -1776,6 +1792,8 @@ cir.func @_Z5outerv() personality(@__gxx_personality_v0)
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
}
@@ -1790,17 +1808,20 @@ 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.
+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 handler introduces a `filter` clause on the
-`cir.eh.dispatch` operation, and a new `cir.eh.unexpected` operation.
+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 [
@@ -1816,32 +1837,28 @@ 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, whose destination is the flattened filter handler region.
-
-Because the filter is the only handler 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.
+as well.
-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.
+The two clauses correspond directly to the two handler regions. 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):
+^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
```
-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.
+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 two cases have the same shape when the filter type list is empty.
-The handler region's `cir.unreachable` becomes the `unwind` destination
+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.
@@ -1857,6 +1874,8 @@ cir.func @_Z6targetv() personality(@__gxx_personality_v0) {
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
}
>From fe75d8243617921d472ae08f84614883617c6f94 Mon Sep 17 00:00:00 2001
From: Andy Kaylor <akaylor at nvidia.com>
Date: Wed, 9 Sep 2026 15:45:56 -0700
Subject: [PATCH 4/4] Emphasize behavior of filter in cir.eh.dispatch
operations
---
clang/docs/CIR/CleanupAndEHDesign.md | 33 ++++++++++++++++++----------
1 file changed, 22 insertions(+), 11 deletions(-)
diff --git a/clang/docs/CIR/CleanupAndEHDesign.md b/clang/docs/CIR/CleanupAndEHDesign.md
index ef75870fccabf..b5c4da7941617 100644
--- a/clang/docs/CIR/CleanupAndEHDesign.md
+++ b/clang/docs/CIR/CleanupAndEHDesign.md
@@ -1825,24 +1825,35 @@ 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
]
```
-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
+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. The
-unexpected region becomes the destination of the `filter` clause, and
-the filter region, which describes the permitted path, becomes the
-`unwind` destination.
+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
@@ -1893,8 +1904,8 @@ cir.func @_Z6targetv() personality(@__gxx_personality_v0) {
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
+ 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
@@ -1965,8 +1976,8 @@ cir.func @_Z6targetv() personality(@__gxx_personality_v0) {
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
+ 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
More information about the cfe-commits
mailing list