https://github.com/mmha created https://github.com/llvm/llvm-project/pull/231050

The initial await_resume() result may have a throwing destructor. Destroy it 
inside the resumption try/catch before marking resumption successful, so a 
destructor exception calls unhandled_exception() and skips the coroutine body.

>From 7bfa85a19bed2ff34583957013da0b647ea9d8b3 Mon Sep 17 00:00:00 2001
From: Morris Hafner <[email protected]>
Date: Sun, 11 Oct 2026 23:50:26 +0000
Subject: [PATCH] [CIR] Handle throwing destructors of initial await_resume
 results

The initial await_resume() result may have a throwing destructor. Destroy it 
inside the resumption try/catch before marking resumption successful, so a 
destructor exception calls unhandled_exception() and skips the coroutine body.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp     |  46 ++++--
 clang/test/CIR/CodeGenCoroutines/coro-agg.cpp | 144 +++++++++++++++++-
 2 files changed, 172 insertions(+), 18 deletions(-)

diff --git a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp 
b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
index ad34df762fe19..7e0f61121595a 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
@@ -634,14 +634,29 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt 
&s) {
   return res;
 }
 
-static bool memberCallExpressionCanThrow(const Expr *e) {
-  if (const auto *ce = dyn_cast<CXXMemberCallExpr>(e))
-    if (const auto *proto =
-            ce->getMethodDecl()->getType()->getAs<FunctionProtoType>())
-      if (isNoexceptExceptionSpec(proto->getExceptionSpecType()) &&
-          proto->canThrow() == CT_Cannot)
-        return false;
-  return true;
+static bool functionCanThrow(const FunctionDecl *d) {
+  const auto *proto = d->getType()->getAs<FunctionProtoType>();
+  if (!proto)
+    return true;
+  return !isNoexceptExceptionSpec(proto->getExceptionSpecType()) ||
+         proto->canThrow() != CT_Cannot;
+}
+
+static bool stmtCanThrow(const Stmt *s) {
+  if (const auto *ce = dyn_cast<CallExpr>(s)) {
+    const FunctionDecl *callee = ce->getDirectCallee();
+    if (!callee || functionCanThrow(callee))
+      return true;
+  }
+
+  // The destructor call of a bound temporary is not among the children.
+  if (const auto *te = dyn_cast<CXXBindTemporaryExpr>(s))
+    if (functionCanThrow(te->getTemporary()->getDestructor()))
+      return true;
+
+  return llvm::any_of(s->children(), [](const Stmt *child) {
+    return child && stmtCanThrow(child);
+  });
 }
 
 // Given a suspend expression which roughly looks like:
@@ -667,6 +682,12 @@ struct LValueOrRValue {
 };
 } // namespace
 
+static bool isResumeExprInTryCatch(const CGCoroData &coro, cir::AwaitKind kind,
+                                   const CoroutineSuspendExpr &s) {
+  return coro.exceptionHandler && kind == cir::AwaitKind::Init &&
+         stmtCanThrow(s.getResumeExpr());
+}
+
 static LValueOrRValue
 emitSuspendExpression(CIRGenFunction &cgf, CGCoroData &coro,
                       CoroutineSuspendExpr const &s, cir::AwaitKind kind,
@@ -681,10 +702,9 @@ emitSuspendExpression(CIRGenFunction &cgf, CGCoroData 
&coro,
   CIRGenBuilderTy &builder = cgf.getBuilder();
 
   // Exception handling requires additional IR. We avoid generating it when
-  // the resume expression is a direct call to a 'noexcept' member function.
-  const bool resumeInTry = coro.exceptionHandler &&
-                           kind == cir::AwaitKind::Init &&
-                           memberCallExpressionCanThrow(s.getResumeExpr());
+  // neither the await_resume() call nor the destruction of its result can
+  // throw.
+  const bool resumeInTry = isResumeExprInTryCatch(coro, kind, s);
 
   // If the await_resume() result needs a destructor, take over its
   // destruction, unless the destination owns it. The try/catch path destroys
@@ -768,8 +788,10 @@ emitSuspendExpression(CIRGenFunction &cgf, CGCoroData 
&coro,
                   resumeEHVar(resumeEHVar.getPointer()) {}
 
             mlir::LogicalResult operator()(CIRGenFunction &cgf) override {
+              CIRGenFunction::RunCleanupsScope resumeCleanups(cgf);
               mlir::LogicalResult res =
                   cgf.emitStmt(tryStmt.getTryBlock(), 
/*useCurrentScope=*/true);
+              resumeCleanups.forceCleanup();
               cgf.getBuilder().createFlagStore(loc, true, resumeEHVar);
               return res;
             }
diff --git a/clang/test/CIR/CodeGenCoroutines/coro-agg.cpp 
b/clang/test/CIR/CodeGenCoroutines/coro-agg.cpp
index a966537abf918..83262823ed72a 100644
--- a/clang/test/CIR/CodeGenCoroutines/coro-agg.cpp
+++ b/clang/test/CIR/CodeGenCoroutines/coro-agg.cpp
@@ -1,9 +1,11 @@
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fclangir 
-Wno-coroutine-missing-unhandled-exception -emit-cir %s -o %t.cir
-// RUN: FileCheck --input-file=%t.cir %s -check-prefix=CIR
+// RUN: FileCheck --input-file=%t.cir %s 
-check-prefixes=CIR,CIR-COMMON,CIR-NOEH
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fclangir 
-fcxx-exceptions -fexceptions -Wno-coroutine-missing-unhandled-exception 
-emit-cir %s -o %t.eh.cir
-// RUN: FileCheck --input-file=%t.eh.cir %s -check-prefix=CIR-EH
+// RUN: FileCheck --input-file=%t.eh.cir %s -check-prefixes=CIR-EH,CIR-COMMON
 // RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm 
-disable-llvm-passes -Wno-coroutine-missing-unhandled-exception %s -o %t.ll
 // RUN: FileCheck --input-file=%t.ll %s -check-prefix=OGCG
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu 
-fcxx-exceptions -fexceptions -emit-llvm -disable-llvm-passes 
-Wno-coroutine-missing-unhandled-exception %s -o %t.eh.ll
+// RUN: FileCheck --input-file=%t.eh.ll %s -check-prefix=OGCG-EH
 
 #include "Inputs/coroutine.h"
 
@@ -425,18 +427,25 @@ struct coro_init_final_nt_t {
 // CIR-NEXT: } cleanup normal {
 // CIR-NEXT:   cir.call @_ZN2NTD1Ev(%[[FINAL]])
 
-// With exceptions, the initial await_resume() is emitted in a try/catch that
-// owns and destroys the result; nothing is pushed after the cir.await.
+// With exceptions, destroy the initial resume result in the try block before
+// setting the flag that allows the body to run.
 // CIR-EH-LABEL: cir.func coroutine {{.*}} @_Z21init_final_nontrivialv
+// CIR-EH: %[[RESUME_EH:.*]] = cir.alloca "resume.eh"
 // CIR-EH: cir.coroutine initialSuspend : {
 // CIR-EH:   cir.await(init, ready : {
 // CIR-EH:   }, resume : {
 // CIR-EH:     cir.try {
 // CIR-EH-NEXT:  cir.call @_ZN11InitAwaitNT12await_resumeEv(%[[INIT:.*]], 
%{{.*}})
 // CIR-EH-NEXT:  cir.cleanup.scope {
-// CIR-EH:       } cleanup all {
+// CIR-EH-NEXT:    cir.yield
+// CIR-EH-NEXT:  } cleanup all {
 // CIR-EH-NEXT:    cir.call @_ZN2NTD1Ev(%[[INIT]])
-// CIR-EH:     } catch all
+// CIR-EH-NEXT:    cir.yield
+// CIR-EH-NEXT:  }
+// CIR-EH-NEXT:  %[[TRUE:.*]] = cir.const #true
+// CIR-EH-NEXT:  cir.store %[[TRUE]], %[[RESUME_EH]]
+// CIR-EH-NEXT:  cir.yield
+// CIR-EH-NEXT: } catch all
 // CIR-EH:   },)
 // CIR-EH-NEXT: cir.yield
 // CIR-EH-NEXT: }, body : {
@@ -458,4 +467,127 @@ struct coro_init_final_nt_t {
 // OGCG: final.ready:
 // OGCG-NEXT: call void @_ZN12FinalAwaitNT12await_resumeEv(ptr {{.*}} 
%[[FINAL:.*]], ptr
 // OGCG-NEXT: call void @_ZN2NTD1Ev(ptr {{.*}} %[[FINAL]])
+
+// OGCG-EH-LABEL: define {{.*}} void @_Z21init_final_nontrivialv(
+// OGCG-EH:       init.ready:
+// OGCG-EH-NEXT:    store i1 true, ptr %[[RESUME_EH:.*]], align 1
+// OGCG-EH-NEXT:    invoke void @_ZN11InitAwaitNT12await_resumeEv(ptr {{.*}} 
%[[INIT:.*]], ptr
+// OGCG-EH-NEXT:      to label %[[CONT:.*]] unwind label
+// OGCG-EH:       [[CONT]]:
+// OGCG-EH-NEXT:    call void @_ZN2NTD1Ev(ptr {{.*}} %[[INIT]])
+// OGCG-EH-NEXT:    store i1 false, ptr %[[RESUME_EH]], align 1
 coro_init_final_nt_t init_final_nontrivial() { co_return; }
+
+void next();
+
+// The try/catch is only needed if the resume expression can throw: here
+// neither await_resume() nor the result's destructor can.
+
+struct NoexceptInitAw {
+  bool await_ready() noexcept;
+  void await_suspend(std::coroutine_handle<>) noexcept;
+  NT await_resume() noexcept;
+};
+
+struct NoexceptInitTask {
+  struct promise_type {
+    NoexceptInitTask get_return_object() noexcept;
+    NoexceptInitAw initial_suspend() noexcept;
+    std::suspend_never final_suspend() noexcept;
+    void return_void() noexcept;
+    void unhandled_exception() noexcept;
+  };
+};
+
+NoexceptInitTask noexcept_initial_resume() {
+  next();
+  co_return;
+}
+
+// CIR-COMMON-LABEL: cir.func coroutine {{.*}} @_Z23noexcept_initial_resumev(
+// CIR-COMMON-NOT:       resume.eh
+// CIR-COMMON:           %[[INIT:.*]] = cir.alloca "agg.tmp.ensured" align(4) 
: !cir.ptr<!rec_NT>
+// CIR-COMMON-NOT:       resume.eh
+// CIR-COMMON:           cir.await(init, ready : {
+// CIR-COMMON:           }, resume : {
+// CIR-COMMON-NEXT:        cir.call 
@_ZN14NoexceptInitAw12await_resumeEv(%[[INIT]], %{{.*}})
+// CIR-COMMON-NEXT:        cir.yield
+// CIR-COMMON-NEXT:      },)
+// CIR-COMMON-NEXT:      cir.cleanup.scope {
+// CIR-COMMON-NEXT:        cir.yield
+// CIR-NOEH-NEXT: } cleanup normal {
+// CIR-EH-NEXT:   } cleanup all {
+// CIR-COMMON-NEXT:        cir.call @_ZN2NTD1Ev(%[[INIT]])
+// CIR-COMMON-NEXT:        cir.yield
+// CIR-COMMON-NEXT:      }
+// CIR-COMMON-NEXT:      cir.yield
+// CIR-COMMON-NEXT:    }, body : {
+// CIR-COMMON-NOT:       resume.eh
+// CIR-COMMON:         }, finalSuspend : {
+
+// OGCG-EH-LABEL: define {{.*}} void @_Z23noexcept_initial_resumev(
+// OGCG-EH-NOT:     initresume.eh
+// OGCG-EH:       init.ready:
+// OGCG-EH-NEXT:    call void @_ZN14NoexceptInitAw12await_resumeEv(ptr {{.*}} 
%[[INIT:.*]], ptr
+// OGCG-EH-NEXT:    call void @_ZN2NTD1Ev(ptr {{.*}} %[[INIT]])
+// OGCG-EH-NOT:     initresume.eh
+// OGCG-EH:       define
+
+// With a destructor that may throw, a noexcept await_resume() call is still in
+// the try/catch, and the result is destroyed before the flag is set.
+
+struct TD {
+  int x;
+  ~TD() noexcept(false);
+};
+
+struct ThrowingDtorInitAw {
+  bool await_ready() noexcept;
+  void await_suspend(std::coroutine_handle<>) noexcept;
+  TD await_resume() noexcept;
+};
+
+struct ThrowingDtorInitTask {
+  struct promise_type {
+    ThrowingDtorInitTask get_return_object() noexcept;
+    ThrowingDtorInitAw initial_suspend() noexcept;
+    std::suspend_never final_suspend() noexcept;
+    void return_void() noexcept;
+    void unhandled_exception() noexcept;
+  };
+};
+
+ThrowingDtorInitTask throwing_dtor_initial_resume() {
+  next();
+  co_return;
+}
+
+// CIR-COMMON-LABEL: cir.func coroutine {{.*}} 
@_Z28throwing_dtor_initial_resumev(
+// CIR-EH:      %[[RESUME_EH:.*]] = cir.alloca "resume.eh"
+// CIR-COMMON:         cir.await(init, ready : {
+// CIR-COMMON:         }, resume : {
+// CIR-EH:          cir.try {
+// CIR-EH-NEXT:       cir.call 
@_ZN18ThrowingDtorInitAw12await_resumeEv(%[[INIT:.*]], %{{.*}})
+// CIR-EH-NEXT:       cir.cleanup.scope {
+// CIR-EH-NEXT:         cir.yield
+// CIR-EH-NEXT:       } cleanup all {
+// CIR-EH-NEXT:         cir.call @_ZN2TDD1Ev(%[[INIT]])
+// CIR-EH-NEXT:         cir.yield
+// CIR-EH-NEXT:       }
+// CIR-EH-NEXT:       %[[TRUE:.*]] = cir.const #true
+// CIR-EH-NEXT:       cir.store %[[TRUE]], %[[RESUME_EH]]
+// CIR-EH-NEXT:       cir.yield
+// CIR-EH-NEXT:     } catch all
+// CIR-COMMON:         }, body : {
+// CIR-EH-NEXT:   %[[RESUMED:.*]] = cir.load {{.*}} %[[RESUME_EH]]
+// CIR-EH-NEXT:   cir.if %[[RESUMED]] {
+// CIR-EH:          cir.call @_Z4nextv()
+
+// OGCG-EH-LABEL: define {{.*}} void @_Z28throwing_dtor_initial_resumev(
+// OGCG-EH:       init.ready:
+// OGCG-EH-NEXT:    store i1 true, ptr %[[RESUME_EH:.*]], align 1
+// OGCG-EH-NEXT:    call void @_ZN18ThrowingDtorInitAw12await_resumeEv(ptr 
{{.*}} %[[INIT:.*]], ptr
+// OGCG-EH-NEXT:    invoke void @_ZN2TDD1Ev(ptr {{.*}} %[[INIT]])
+// OGCG-EH-NEXT:      to label %[[CONT:.*]] unwind label
+// OGCG-EH:       [[CONT]]:
+// OGCG-EH-NEXT:    store i1 false, ptr %[[RESUME_EH]], align 1

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

Reply via email to