https://github.com/andykaylor created 
https://github.com/llvm/llvm-project/pull/224375

This implements CIR generation for C++ functions with dynamic exception 
specifications. The exception filter is implemented as a cir.try operation that 
wraps the entire function body. CFG flattening and EH ABI lowerting were 
implemented in previous changes, so they are already in place.

This also adds an NYI diagnostic for functions marked `noexcept`. That will be 
implemented in a follow-up change, but the new diagnostic forced updates to a 
few tests that were using that form.

Assisted-by: Cursor / various models

>From 11f55b12294270f04b7fcd0b02b6fbdb18ca19fc Mon Sep 17 00:00:00 2001
From: Andy Kaylor <[email protected]>
Date: Tue, 15 Sep 2026 17:31:38 -0700
Subject: [PATCH] [CIR][EH] Implement codegen for dynamic exception
 specification

This implements CIR generation for C++ functions with dynamic exception
specifications. The exception filter is implemented as a cir.try operation
that wraps the entire function body. CFG flattening and EH ABI lowerting
were implemented in previous changes, so they are already in place.

This also adds an NYI diagnostic for functions marked `noexcept`. That
will be implemented in a follow-up change, but the new diagnostic forced
updates to a few tests that were using that form.

Assisted-by: Cursor / various models
---
 clang/lib/CIR/CodeGen/CIRGenException.cpp     | 133 +++++++++++++++
 clang/lib/CIR/CodeGen/CIRGenFunction.cpp      |  16 +-
 clang/lib/CIR/CodeGen/CIRGenFunction.h        |  11 ++
 clang/test/CIR/CodeGen/ehspec.cpp             | 158 ++++++++++++++++++
 clang/test/CIR/CodeGen/rtti-qualfn.cpp        |   8 +-
 .../CIR/CodeGen/try-no-throwing-calls.cpp     |   2 +-
 6 files changed, 320 insertions(+), 8 deletions(-)
 create mode 100644 clang/test/CIR/CodeGen/ehspec.cpp

diff --git a/clang/lib/CIR/CodeGen/CIRGenException.cpp 
b/clang/lib/CIR/CodeGen/CIRGenException.cpp
index f5823b2be30ab..6026a7f391f63 100644
--- a/clang/lib/CIR/CodeGen/CIRGenException.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenException.cpp
@@ -15,6 +15,7 @@
 #include "mlir/IR/Block.h"
 #include "mlir/IR/Location.h"
 
+#include "clang/Basic/DiagnosticSema.h"
 #include "clang/CIR/MissingFeatures.h"
 #include "llvm/Support/SaveAndRestore.h"
 
@@ -198,6 +199,138 @@ static llvm::StringRef getPersonalityFn(CIRGenModule &cgm,
   return personalityFn.getSymName();
 }
 
+void CIRGenFunction::emitStartEHSpec(const Decl *d) {
+  if (!cgm.getLangOpts().CXXExceptions)
+    return;
+
+  const FunctionDecl *fd = dyn_cast_or_null<FunctionDecl>(d);
+  if (!fd) {
+    // This can't currently happen in CIR, but if we do get here, we need to
+    // handle the cd->isNoThrow() case.
+    if (const CapturedDecl *cd = dyn_cast_or_null<CapturedDecl>(d)) {
+      if (cd->isNothrow())
+        cgm.errorNYI(cd->getSourceRange(),
+                     "emitStartEHSpec CapturedDecl nothrow");
+    }
+    return;
+  }
+
+  const FunctionProtoType *proto = fd->getType()->getAs<FunctionProtoType>();
+  if (!proto)
+    return;
+
+  ExceptionSpecificationType est = proto->getExceptionSpecType();
+  // In C++17 and later, 'throw()' aka EST_DynamicNone is treated the same way
+  // as noexcept. In earlier standards, it is handled with 'throw(X...)'.
+  if (est != EST_Dynamic &&
+      !(est == EST_DynamicNone && !getLangOpts().CPlusPlus17)) {
+    // noexcept functions are terminate scopes in classic codegen.
+    if (proto->canThrow() == CT_Cannot && !getLangOpts().EHAsynch)
+      cgm.errorNYI(fd->getSourceRange(),
+                   "emitStartEHSpec noexcept terminate scope");
+    return;
+  }
+
+  // TODO: Revisit exception specifications for the MS ABI. There is a way
+  // to encode these in an object file but MSVC doesn't do anything with it.
+  if (getTarget().getCXXABI().isMicrosoft())
+    return;
+
+  // In Wasm EH we currently treat 'throw()' in the same way as 'noexcept'.
+  // In case of throw with types, we ignore it and print a warning for now.
+  // TODO Correctly handle exception specification in Wasm EH
+  if (cgm.getCodeGenOpts().hasWasmExceptions()) {
+    if (est == EST_Dynamic)
+      cgm.getDiags().Report(d->getLocation(),
+                            diag::warn_wasm_dynamic_exception_spec_ignored)
+          << fd->getExceptionSpecSourceRange();
+    return;
+  }
+
+  // Currently Emscripten EH only handles 'throw()' but not 'throw' with
+  // types. 'throw()' handling will be done in JS glue code so we don't need
+  // to do anything in that case. Just print a warning message in case of
+  // throw with types.
+  // TODO Correctly handle exception specification in Emscripten EH
+  if (getTarget().getCXXABI() == TargetCXXABI::WebAssembly &&
+      (cgm.getCodeGenOpts().getExceptionHandling() ==
+           clang::CodeGenOptions::ExceptionHandlingKind::None ||
+       cgm.getCodeGenOpts().getExceptionHandling() ==
+           clang::CodeGenOptions::ExceptionHandlingKind::Default) &&
+      est == EST_Dynamic)
+    cgm.getDiags().Report(d->getLocation(),
+                          diag::warn_wasm_dynamic_exception_spec_ignored)
+        << fd->getExceptionSpecSourceRange();
+
+  mlir::Location loc = getLoc(fd->getSourceRange());
+
+  SmallVector<mlir::Attribute, 4> permittedTypes;
+  for (QualType ty : proto->exceptions()) {
+    QualType exceptType = ty.getNonReferenceType().getUnqualifiedType();
+    permittedTypes.push_back(
+        cgm.getAddrOfRTTIDescriptor(loc, exceptType, /*forEh=*/true));
+  }
+
+  cir::FuncOp funcOp = mlir::cast<cir::FuncOp>(curFn);
+  if (!funcOp.getPersonality())
+    funcOp.setPersonality(getPersonalityFn(cgm, EHPersonality::get(*this)));
+
+  bool emptyFilter = permittedTypes.empty();
+  ehSpecTryOp = cir::TryOp::create(
+      builder, loc,
+      // The try body holds the function body, which the caller emits after
+      // this function returns and emitEndEHSpec terminates.
+      /*tryBuilder=*/[](mlir::OpBuilder &, mlir::Location) {},
+      /*handlersBuilder=*/
+      [&](mlir::OpBuilder &b, mlir::Location loc,
+          mlir::OperationState &result) {
+        mlir::OpBuilder::InsertionGuard guard(b);
+        mlir::Type ehTokenTy = cir::EhTokenType::get(&getMLIRContext());
+
+        mlir::Block *filterBlock = b.createBlock(
+            result.addRegion(), /*insertPt=*/{}, {ehTokenTy}, {loc});
+        if (emptyFilter)
+          cir::UnreachableOp::create(b, loc);
+        else
+          cir::ResumeOp::create(b, loc, filterBlock->getArgument(0));
+
+        mlir::Block *unexpectedBlock = b.createBlock(
+            result.addRegion(), /*insertPt=*/{}, {ehTokenTy}, {loc});
+        cir::EhUnexpectedOp::create(b, loc, unexpectedBlock->getArgument(0));
+      });
+
+  SmallVector<mlir::Attribute, 2> handlerAttrs;
+  handlerAttrs.push_back(cir::EhFilterAttr::get(
+      &getMLIRContext(), builder.getArrayAttr(permittedTypes)));
+  handlerAttrs.push_back(cir::EhUnexpectedAttr::get(&getMLIRContext()));
+  ehSpecTryOp.setHandlerTypesAttr(
+      mlir::ArrayAttr::get(&getMLIRContext(), handlerAttrs));
+
+  // Continue emitting into the try body.
+  builder.setInsertionPointToEnd(&ehSpecTryOp.getTryRegion().front());
+}
+
+void CIRGenFunction::emitEndEHSpec(const Decl *) {
+  if (!ehSpecTryOp)
+    return;
+
+  cir::TryOp tryOp = ehSpecTryOp;
+  ehSpecTryOp = cir::TryOp();
+
+  // Terminate the try body. Emitting the function body may have left the last
+  // block without a terminator, for instance when control falls off the end.
+  mlir::Block *bodyExit = &tryOp.getTryRegion().back();
+  if (bodyExit->empty() ||
+      !bodyExit->back().hasTrait<mlir::OpTrait::IsTerminator>()) {
+    mlir::OpBuilder::InsertionGuard guard(builder);
+    builder.setInsertionPointToEnd(bodyExit);
+    builder.createYield(tryOp.getLoc());
+  }
+
+  // Continue emitting the function epilogue outside the try.
+  builder.setInsertionPointAfter(tryOp);
+}
+
 void CIRGenFunction::emitCXXThrowExpr(const CXXThrowExpr *e) {
   const llvm::Triple &triple = getTarget().getTriple();
   if (cgm.getLangOpts().OpenMPIsTargetDevice &&
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp 
b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
index 80525339bd7a6..a1bd15d88b118 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.cpp
@@ -552,11 +552,15 @@ void CIRGenFunction::startFunction(GlobalDecl gd, 
QualType returnType,
     fn->setAttr(cir::CIRDialect::getStrictFPAttrName(),
                 mlir::UnitAttr::get(fn.getContext()));
   }
-  prologueCleanupDepth = ehStack.stable_begin();
-
   mlir::Block *entryBB = &fn.getBlocks().front();
   builder.setInsertionPointToStart(entryBB);
 
+  // Wrap the rest of the function in a filter try when this declaration has
+  // a dynamic exception specification. Parameter cleanups are pushed after
+  // this so they nest inside the specification, matching classic codegen.
+  emitStartEHSpec(d);
+  prologueCleanupDepth = ehStack.stable_begin();
+
   // Determine the function body begin location for the prolog.
   // If fd is null or has no body, use startLoc as fallback.
   SourceLocation bodyBeginLoc = startLoc;
@@ -688,6 +692,8 @@ void CIRGenFunction::finishFunction(SourceLocation endLoc) {
   assert(deferredConditionalCleanupStack.empty() &&
          "deferred conditional cleanups were not consumed by a "
          "FullExprCleanupScope");
+
+  emitEndEHSpec(curCodeDecl);
 }
 
 mlir::LogicalResult CIRGenFunction::emitFunctionBody(const clang::Stmt *body) {
@@ -825,10 +831,12 @@ cir::FuncOp 
CIRGenFunction::generateCode(clang::GlobalDecl gd, cir::FuncOp fn,
       llvm_unreachable("no definition for normal function");
     }
 
+    // Finish the function (including closing a dynamic exception
+    // specification try) before verifying so the try body is terminated.
+    finishFunction(bodyRange.getEnd());
+
     if (mlir::failed(fn.verifyBody()))
       return nullptr;
-
-    finishFunction(bodyRange.getEnd());
   }
 
   if (getLangOpts().OpenCL && funcDecl->hasAttr<DeviceKernelAttr>())
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h 
b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index 750ed377f812f..77cc453e1b703 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -1106,6 +1106,13 @@ class CIRGenFunction : public CIRGenTypeCache {
                      FunctionArgList args, clang::SourceLocation loc,
                      clang::SourceLocation startLoc);
 
+  /// Wrap the function body in a filter `cir.try` when \p d has a dynamic
+  /// exception specification (`throw(T...)` or pre-C++17 `throw()`).
+  void emitStartEHSpec(const clang::Decl *d);
+
+  /// Close the filter `cir.try` opened by emitStartEHSpec.
+  void emitEndEHSpec(const clang::Decl *d);
+
   /// returns true if aggregate type has a volatile member.
   bool hasVolatileMember(QualType t) {
     if (const auto *rd = t->getAsRecordDecl())
@@ -1120,6 +1127,10 @@ class CIRGenFunction : public CIRGenTypeCache {
   /// parameters.
   EHScopeStack::stable_iterator prologueCleanupDepth;
 
+  /// The `cir.try` wrapping a function with a dynamic exception specification.
+  /// Null when the current function has no such specification.
+  cir::TryOp ehSpecTryOp;
+
   bool isCatchOrCleanupRequired();
 
   /// Takes the old cleanup stack size and emits the cleanup blocks
diff --git a/clang/test/CIR/CodeGen/ehspec.cpp 
b/clang/test/CIR/CodeGen/ehspec.cpp
new file mode 100644
index 0000000000000..d33951b6add47
--- /dev/null
+++ b/clang/test/CIR/CodeGen/ehspec.cpp
@@ -0,0 +1,158 @@
+// RUN: %clang_cc1 -std=c++14 -triple x86_64-unknown-linux-gnu \
+// RUN:   -fcxx-exceptions -fexceptions -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --check-prefix=CIR --input-file=%t.cir %s
+// RUN: %clang_cc1 -std=c++14 -triple x86_64-unknown-linux-gnu \
+// RUN:   -fcxx-exceptions -fexceptions -fclangir -emit-llvm %s -o %t-cir.ll
+// RUN: FileCheck --check-prefix=LLVM,LLVMCIR --input-file=%t-cir.ll %s
+// RUN: %clang_cc1 -std=c++14 -triple x86_64-unknown-linux-gnu \
+// RUN:   -fcxx-exceptions -fexceptions -emit-llvm %s -o %t.ll
+// RUN: FileCheck --check-prefix=LLVM,OGCG --input-file=%t.ll %s
+
+void external();
+void inner();
+
+void target() throw(int) {
+  external();
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z6targetv()
+// CIR-SAME: personality(@__gxx_personality_v0)
+// CIR:         cir.try {
+// CIR:           cir.call @_Z8externalv() : () -> ()
+// CIR:           cir.yield
+// CIR:         } filter [#cir.global_view<@_ZTIi> : !cir.ptr<!u8i>]
+// CIR-SAME:        (%[[TOK:.*]]: !cir.eh_token
+// CIR:           cir.resume %[[TOK]] : !cir.eh_token
+// CIR:         } unexpected (%[[UTOK:.*]]: !cir.eh_token
+// CIR:           cir.eh.unexpected %[[UTOK]] : !cir.eh_token
+// CIR:         }
+// CIR:         cir.return
+
+// LLVM-LABEL: define{{.*}} void @_Z6targetv()
+// LLVM-SAME: personality ptr @__gxx_personality_v0
+// LLVM:         invoke void @_Z8externalv()
+// LLVM-NEXT:            to label %[[CONT:[^ ,]+]] unwind label %[[LPAD:[^ 
,]+]]
+// LLVM:       {{^}}[[CONT]]:
+// LLVM:       {{^}}[[LPAD]]:
+// LLVM-NEXT:    %{{.*}} = landingpad { ptr, i32 }
+// LLVM-NEXT:            filter [1 x ptr] [ptr @_ZTIi]
+//      A negative selector means the personality routine rejected the
+//      exception against the filter clause, which violates the specification.
+// LLVM:         %[[FAILS:.*]] = icmp slt i32 %{{.*}}, 0
+// LLVM-NEXT:    br i1 %[[FAILS]], label %[[UNEXPECTED:[^ ,]+]], label 
%[[RESUME:[^ ,]+]]
+
+// The two pipelines emit the handler blocks in opposite orders.
+// LLVMCIR:    {{^}}[[RESUME]]:
+// LLVMCIR:      resume { ptr, i32 } %{{.*}}
+// LLVMCIR:    {{^}}[[UNEXPECTED]]:
+// LLVMCIR:      call void @__cxa_call_unexpected(ptr %{{.*}})
+// LLVMCIR-NEXT: unreachable
+
+// OGCG:       {{^}}[[UNEXPECTED]]:
+// OGCG:         call void @__cxa_call_unexpected(ptr %{{.*}})
+// OGCG-NEXT:    unreachable
+// OGCG:       {{^}}[[RESUME]]:
+// OGCG:         resume { ptr, i32 } %{{.*}}
+
+void target2() throw() {
+  external();
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z7target2v()
+// CIR-SAME: personality(@__gxx_personality_v0)
+// CIR:         cir.try {
+// CIR:           cir.call @_Z8externalv() : () -> ()
+// CIR:           cir.yield
+// CIR:         } filter []
+// CIR:           cir.unreachable
+// CIR:         } unexpected (%[[UTOK:.*]]: !cir.eh_token
+// CIR:           cir.eh.unexpected %[[UTOK]] : !cir.eh_token
+// CIR:         }
+// CIR:         cir.return
+
+// LLVM-LABEL: define{{.*}} void @_Z7target2v()
+// LLVM-SAME: personality ptr @__gxx_personality_v0
+// LLVM:         invoke void @_Z8externalv()
+// LLVM-NEXT:            to label %[[CONT:[^ ,]+]] unwind label %[[LPAD:[^ 
,]+]]
+// LLVM:       {{^}}[[CONT]]:
+// LLVM:       {{^}}[[LPAD]]:
+// LLVM-NEXT:    %{{.*}} = landingpad { ptr, i32 }
+// LLVM-NEXT:            filter [0 x ptr] zeroinitializer
+//      An empty filter list permits nothing, so every exception violates the
+//      specification. There is no selector comparison and no resume path.
+// LLVM-NOT:     icmp
+// LLVM-NOT:     resume
+// LLVM:         call void @__cxa_call_unexpected(ptr %{{.*}})
+// LLVM-NEXT:    unreachable
+// LLVM-NOT:     resume
+
+void outer() throw() {
+  try {
+    inner();
+  } catch (int) {
+  }
+}
+
+// CIR-LABEL: cir.func{{.*}} @_Z5outerv()
+// CIR-SAME: personality(@__gxx_personality_v0)
+// CIR:         cir.try {
+// CIR:           cir.scope {
+// CIR:             cir.try {
+// CIR:               cir.call @_Z5innerv() : () -> ()
+// CIR:               cir.yield
+// CIR:             } catch [type #cir.global_view<@_ZTIi> : !cir.ptr<!u8i>]
+// CIR:             } unwind
+// CIR:           }
+// CIR:           cir.yield
+// CIR:         } filter []
+// CIR:           cir.unreachable
+// CIR:         } unexpected
+// CIR:           cir.eh.unexpected
+
+// LLVM-LABEL: define{{.*}} void @_Z5outerv()
+// LLVM-SAME: personality ptr @__gxx_personality_v0
+// LLVM:         invoke void @_Z5innerv()
+// LLVM-NEXT:            to label %[[CONT:[^ ,]+]] unwind label %[[LPAD:[^ 
,]+]]
+// LLVM:       {{^}}[[CONT]]:
+// LLVM-NEXT:    br label %[[TRY_CONT:[^ ,]+]]
+
+// The inner catch and the enclosing exception specification share a single
+// landing pad, whose clauses are the catch clause followed by the filter.
+// LLVM:       {{^}}[[LPAD]]:
+// LLVM-NEXT:    %{{.*}} = landingpad { ptr, i32 }
+// LLVM-NEXT:            catch ptr @_ZTIi
+// LLVM-NEXT:            filter [0 x ptr] zeroinitializer
+
+// The catch clause is tested first; a non-matching exception falls through to
+// the exception specification.
+// LLVM:         %[[TID:.*]] = call i32 @llvm.eh.typeid.for.p0(ptr @_ZTIi)
+// LLVM-NEXT:    %[[MATCHES:.*]] = icmp eq i32 %{{.*}}, %[[TID]]
+// LLVM-NEXT:    br i1 %[[MATCHES]], label %[[CATCH:[^ ,]+]], label 
%[[NOMATCH:[^ ,]+]]
+
+// Classic codegen folds the empty filter into the non-matching edge.
+// OGCG:       {{^}}[[NOMATCH]]:
+// OGCG:         call void @__cxa_call_unexpected(ptr %{{.*}})
+// OGCG-NEXT:    unreachable
+
+// LLVM:       {{^}}[[CATCH]]:
+// LLVM:         call ptr @__cxa_begin_catch(ptr %{{.*}})
+// LLVM:         call void @__cxa_end_catch()
+//      The handled exception falls through to the join rather than being
+//      rethrown; `outer` is `throw()`, so it has no resume path.
+// LLVM-NOT:     resume
+// LLVM:         br label %[[TRY_CONT]]
+
+// The CIR pipeline routes the non-matching exception through the
+// specification's own dispatch block before reaching the unexpected handler.
+// LLVMCIR:    {{^}}[[NOMATCH]]:
+// LLVMCIR:      br label %[[FILTER_DISPATCH:[0-9]+]]
+// LLVMCIR:    {{^}}[[TRY_CONT]]:
+// LLVMCIR:    {{^}}[[FILTER_DISPATCH]]:
+// LLVMCIR:      br label %[[UNEXPECTED:[0-9]+]]
+// LLVMCIR:    {{^}}[[UNEXPECTED]]:
+// LLVMCIR:      call void @__cxa_call_unexpected(ptr %{{.*}})
+// LLVMCIR-NEXT: unreachable
+// LLVMCIR:      ret void
+
+// OGCG:       {{^}}[[TRY_CONT]]:
+// OGCG-NEXT:    ret void
diff --git a/clang/test/CIR/CodeGen/rtti-qualfn.cpp 
b/clang/test/CIR/CodeGen/rtti-qualfn.cpp
index 639bc1bac92b3..9571a2b35daf1 100644
--- a/clang/test/CIR/CodeGen/rtti-qualfn.cpp
+++ b/clang/test/CIR/CodeGen/rtti-qualfn.cpp
@@ -8,8 +8,10 @@
 // Test that throwing a pointer to a noexcept function produces correct RTTI
 // with the PTI_Noexcept flag (0x40 = 64) set in the __pointer_type_info.
 
-void f() noexcept {
-  throw f;
+void g() noexcept;
+
+void f() {
+  throw g;
 }
 
 // The pointee type _ZTIFvvE (function type info for void()) must be emitted
@@ -33,4 +35,4 @@ void f() noexcept {
 // OGCG-DAG: @_ZTIFvvE = linkonce_odr constant { ptr, ptr } { ptr 
getelementptr inbounds (ptr, ptr @_ZTVN10__cxxabiv120__function_type_infoE, i64 
2), ptr @_ZTSFvvE }, comdat
 // OGCG-DAG: @_ZTSPDoFvvE = linkonce_odr constant [8 x i8] c"PDoFvvE\00", 
comdat
 // OGCG-DAG: @_ZTIPDoFvvE = linkonce_odr constant { ptr, ptr, i32, ptr } { ptr 
getelementptr inbounds (ptr, ptr @_ZTVN10__cxxabiv119__pointer_type_infoE, i64 
2), ptr @_ZTSPDoFvvE, i32 64, ptr @_ZTIFvvE }, comdat
-// OGCG: invoke void @__cxa_throw(ptr %{{.*}}, ptr @_ZTIPDoFvvE, ptr null)
+// OGCG: call void @__cxa_throw(ptr %{{.*}}, ptr @_ZTIPDoFvvE, ptr null)
diff --git a/clang/test/CIR/CodeGen/try-no-throwing-calls.cpp 
b/clang/test/CIR/CodeGen/try-no-throwing-calls.cpp
index db44c2a4e483c..73a27292ba046 100644
--- a/clang/test/CIR/CodeGen/try-no-throwing-calls.cpp
+++ b/clang/test/CIR/CodeGen/try-no-throwing-calls.cpp
@@ -9,7 +9,7 @@
 // must not crash during TryOp flattening when handler regions reference
 // values inlined from the try body.
 
-int nonThrowing() noexcept { return 42; }
+int nonThrowing() throw() { return 42; }
 
 int test() {
   int result = 0;

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to