[clang] [llvm] [Clang][CodeGen][Coroutines] Make coroutine startup exception-safe (CWG2935) Part1 (PR #179189)
Yexuan Xiao via cfe-commits
cfe-commits at lists.llvm.org
Mon Feb 2 02:09:25 PST 2026
https://github.com/YexuanXiao updated https://github.com/llvm/llvm-project/pull/179189
>From 8bf87ecd3e72b97d51d0e83fd550815e021a9e5d Mon Sep 17 00:00:00 2001
From: Yexuan Xiao <bizwen at nykz.org>
Date: Mon, 2 Feb 2026 17:24:58 +0800
Subject: [PATCH 1/2] Resolve CWG2935, but if some functions are not marked as
noexcept, the patch will block HALO
---
clang/lib/CodeGen/CGCoroutine.cpp | 52 +-
clang/test/CodeGenCoroutines/cwg2935.cpp | 65 +++
coroutine-cwg2935.cpp | 612 +++++++++++++++++++++++
3 files changed, 724 insertions(+), 5 deletions(-)
create mode 100644 clang/test/CodeGenCoroutines/cwg2935.cpp
create mode 100644 coroutine-cwg2935.cpp
diff --git a/clang/lib/CodeGen/CGCoroutine.cpp b/clang/lib/CodeGen/CGCoroutine.cpp
index 7282c42420657..cf5b9546995d6 100644
--- a/clang/lib/CodeGen/CGCoroutine.cpp
+++ b/clang/lib/CodeGen/CGCoroutine.cpp
@@ -134,24 +134,27 @@ static SmallString<32> buildSuspendPrefixStr(CGCoroData &Coro, AwaitKind Kind) {
// Check if function can throw based on prototype noexcept, also works for
// destructors which are implicitly noexcept but can be marked noexcept(false).
-static bool FunctionCanThrow(const FunctionDecl *D) {
+static bool FunctionCanThrow(const FunctionDecl *D, bool InitialSuspend) {
const auto *Proto = D->getType()->getAs<FunctionProtoType>();
if (!Proto) {
// Function proto is not found, we conservatively assume throwing.
return true;
}
+ if (InitialSuspend && D->getIdentifier() &&
+ (D->getIdentifier()->getName() == "await_resume"))
+ return false;
return !isNoexceptExceptionSpec(Proto->getExceptionSpecType()) ||
Proto->canThrow() != CT_Cannot;
}
-static bool StmtCanThrow(const Stmt *S) {
+static bool StmtCanThrow(const Stmt *S, bool InitialSuspend = false) {
if (const auto *CE = dyn_cast<CallExpr>(S)) {
const auto *Callee = CE->getDirectCallee();
if (!Callee)
// We don't have direct callee. Conservatively assume throwing.
return true;
- if (FunctionCanThrow(Callee))
+ if (FunctionCanThrow(Callee, InitialSuspend))
return true;
// Fall through to visit the children.
@@ -163,14 +166,14 @@ static bool StmtCanThrow(const Stmt *S) {
// We need to mark entire statement as throwing if the destructor of the
// temporary throws.
const auto *Dtor = TE->getTemporary()->getDestructor();
- if (FunctionCanThrow(Dtor))
+ if (FunctionCanThrow(Dtor, false))
return true;
// Fall through to visit the children.
}
for (const auto *child : S->children())
- if (StmtCanThrow(child))
+ if (StmtCanThrow(child, InitialSuspend))
return true;
return false;
@@ -657,10 +660,19 @@ struct GetReturnObjectManager {
bool DirectEmit = false;
Address GroActiveFlag;
+ // Active flag used for DirectEmit return value cleanup. When the coroutine
+ // return value is directly emitted into the return slot, we need to run its
+ // destructor if an exception is thrown before initial-await-resume.
+ // DirectGroActiveFlag is unused when InitialSuspendCanThrow is false.
+ bool InitialSuspendCanThrow = false;
+ Address DirectGroActiveFlag;
+
CodeGenFunction::AutoVarEmission GroEmission;
GetReturnObjectManager(CodeGenFunction &CGF, const CoroutineBodyStmt &S)
: CGF(CGF), Builder(CGF.Builder), S(S), GroActiveFlag(Address::invalid()),
+ InitialSuspendCanThrow(StmtCanThrow(S.getInitSuspendStmt(), true)),
+ DirectGroActiveFlag(Address::invalid()),
GroEmission(CodeGenFunction::AutoVarEmission::invalid()) {
// The call to get_return_object is sequenced before the call to
// initial_suspend and is invoked at most once, but there are caveats
@@ -768,6 +780,26 @@ struct GetReturnObjectManager {
CGF.EmitAnyExprToMem(S.getReturnValue(), CGF.ReturnValue,
S.getReturnValue()->getType().getQualifiers(),
/*IsInit*/ true);
+ // If the return value has a non-trivial destructor, register a
+ // conditional cleanup that will destroy it if an exception is thrown
+ // before initial-await-resume. The cleanup is activated now and will
+ // be deactivated once initial_suspend completes normally.
+ if (InitialSuspendCanThrow) {
+ if (QualType::DestructionKind DtorKind =
+ S.getReturnValue()->getType().isDestructedType();
+ DtorKind) {
+ // Create an active flag (initialize to true) for conditional
+ // cleanup. We are not necessarily in a conditional branch here so
+ // use a simple temp alloca instead of createCleanupActiveFlag().
+ auto ActiveFlag = CGF.CreateTempAlloca(
+ Builder.getInt1Ty(), CharUnits::One(), "direct.gro.active");
+ Builder.CreateStore(Builder.getTrue(), ActiveFlag);
+ CGF.pushDestroyAndDeferDeactivation(DtorKind, CGF.ReturnValue,
+ S.getReturnValue()->getType());
+ CGF.initFullExprCleanupWithFlag(ActiveFlag);
+ DirectGroActiveFlag = ActiveFlag;
+ }
+ }
}
return;
}
@@ -782,6 +814,7 @@ struct GetReturnObjectManager {
CGF.EmitAutoVarInit(GroEmission);
Builder.CreateStore(Builder.getTrue(), GroActiveFlag);
}
+
// The GRO returns either when it is first suspended or when it completes
// without ever being suspended. The EmitGroConv function evaluates these
// conditions and perform the conversion if needed.
@@ -987,6 +1020,15 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
CurCoro.Data->CurrentAwaitKind = AwaitKind::Init;
CurCoro.Data->ExceptionHandler = S.getExceptionHandler();
EmitStmt(S.getInitSuspendStmt());
+
+ // If we emitted the return value directly into the return slot, the
+ // destructor cleanup we registered above should only be active while
+ // initial_suspend is in progress. After initial_suspend completes
+ // normally, deactivate the cleanup so the return value is not
+ // destroyed here.
+ if (GroManager.DirectEmit && GroManager.DirectGroActiveFlag.isValid())
+ Builder.CreateStore(Builder.getFalse(), GroManager.DirectGroActiveFlag);
+
CurCoro.Data->FinalJD = getJumpDestInCurrentScope(FinalBB);
CurCoro.Data->CurrentAwaitKind = AwaitKind::Normal;
diff --git a/clang/test/CodeGenCoroutines/cwg2935.cpp b/clang/test/CodeGenCoroutines/cwg2935.cpp
new file mode 100644
index 0000000000000..89057cd0f9f10
--- /dev/null
+++ b/clang/test/CodeGenCoroutines/cwg2935.cpp
@@ -0,0 +1,65 @@
+// Verifies lifetime of __gro local variable
+// Verify that coroutine promise and allocated memory are freed up on exception.
+// RUN: %clang_cc1 -std=c++20 -triple=x86_64-unknown-linux-gnu -emit-llvm -o - %s -disable-llvm-passes | FileCheck %s
+
+#include "Inputs/coroutine.h"
+
+using namespace std;
+
+struct task {
+ struct promise_type {
+ task get_return_object();
+ suspend_always initial_suspend();
+ suspend_always final_suspend() noexcept;
+ void return_void();
+ void unhandled_exception();
+ };
+ ~task();
+};
+
+task f1() {
+ co_return;
+}
+// CHECK: define dso_local void @_Z2f1v
+// CHECK: entry:
+// CHECK-NEXT: %result.ptr = alloca ptr, align 8
+// CHECK-NEXT: %__promise = alloca %"struct.task::promise_type", align 1
+// CHECK-NEXT: %direct.gro.active = alloca i1, align 1
+// CHECK: coro.init: ; preds = %coro.alloc, %entry
+// CHECK-NEXT: %3 = phi ptr [ null, %entry ], [ %call, %coro.alloc ]
+// CHECK-NEXT: %4 = call ptr @llvm.coro.begin(token %0, ptr %3)
+// CHECK-NEXT: call void @llvm.lifetime.start.p{{.*}}(ptr %__promise) #2
+// CHECK-NEXT: call void @_ZN4task12promise_type17get_return_objectEv(ptr dead_on_unwind writable sret(%struct.task) align 1 %agg.result, ptr noundef nonnull align 1 dereferenceable(1) %__promise)
+// CHECK-NEXT: store i1 true, ptr %direct.gro.active, align 1
+// CHECK: cleanup.cont: ; preds = %cleanup
+// CHECK-NEXT: store i1 false, ptr %direct.gro.active, align 1
+// CHECK-NEXT: call void @_ZN4task12promise_type11return_voidEv(ptr noundef nonnull align 1 dereferenceable(1) %__promise)
+// CHECK-NEXT: br label %coro.final
+// CHECK: cleanup.cont{{.*}}:
+// CHECK-NEXT: store i1 false, ptr %direct.gro.active, align 1
+// CHECK-NEXT: br label %cleanup{{.*}}
+// CHECK: cleanup.action: ; preds = %cleanup{{.*}}
+// CHECK-NEXT: call void @_ZN4taskD1Ev(ptr noundef nonnull align 1 dereferenceable(1) %agg.result) #2
+// CHECK-NEXT: br label %cleanup.done
+
+struct intial_awaiter_resume_throw : std::suspend_always {
+ void await_resume();
+};
+
+struct task_resume_throw {
+ struct promise_type {
+ task_resume_throw get_return_object();
+ suspend_always initial_suspend() noexcept;
+ suspend_always final_suspend() noexcept;
+ void return_void();
+ void unhandled_exception();
+ };
+ ~task_resume_throw();
+};
+
+task_resume_throw f2() {
+ co_return;
+}
+
+// CHECK: define dso_local void @_Z2f2v
+// CHECK-NOT: %direct.gro.active = alloca i1, align 1
\ No newline at end of file
diff --git a/coroutine-cwg2935.cpp b/coroutine-cwg2935.cpp
new file mode 100644
index 0000000000000..6f18c7dae1a28
--- /dev/null
+++ b/coroutine-cwg2935.cpp
@@ -0,0 +1,612 @@
+#include <array>
+#include <cassert>
+#include <coroutine>
+#include <cstdlib>
+
+enum check_points {
+ para_copy_ctor,
+ para_dtor,
+ promise_ctor,
+ promise_dtor,
+ get_return_obj,
+ task_ctor,
+ task_dtor,
+ init_suspend,
+ init_a_ready,
+ init_a_suspend,
+ init_a_resume,
+ awaiter_ctor,
+ awaiter_dtor,
+ return_v,
+ unhandled,
+ fin_suspend
+};
+
+using per_test_counts_type = std::array<int, fin_suspend + 1>;
+
+per_test_counts_type per_test_counts{};
+
+void record(check_points cp) {
+ // Each checkpoint's executions must be precisely recorded to prevent double
+ // free
+ ++per_test_counts[cp];
+}
+
+void clear() { per_test_counts = per_test_counts_type{}; }
+
+std::array<bool, fin_suspend + 1> checked_cond{};
+
+// Each test will throw an exception at a designated location. After the
+// coroutine is invoked, the execution status of all checkpoints will be
+// checked, and then switch to the next test. Before throwing an exception,
+// record the execution status first.
+void throw_c(check_points cp) {
+ record(cp);
+ // Once that point has been tested, it will not be tested again.
+ if (checked_cond[cp] == false) {
+ checked_cond[cp] = true;
+ throw 0;
+ }
+}
+
+std::size_t allocate_count = 0;
+
+void *operator new(std::size_t size) {
+ ++allocate_count;
+ // When the coroutine is invoked, memory allocation is the first operation
+ // performed
+ if (void *ptr = std::malloc(size)) {
+ return ptr;
+ }
+ std::abort();
+}
+
+void operator delete(void *ptr) noexcept {
+ // Deallocation is performed last
+ --allocate_count;
+ std::free(ptr);
+}
+
+struct copy_observer {
+private:
+ copy_observer() = default;
+
+public:
+ copy_observer(copy_observer const &) { throw_c(para_copy_ctor); }
+ ~copy_observer() { record(para_dtor); }
+
+ static copy_observer get() { return {}; }
+};
+
+const auto global_observer = copy_observer::get();
+
+namespace direct_emit {
+
+struct task {
+ task() { throw_c(task_ctor); }
+ ~task() { record(task_dtor); }
+ // In this test, the task should be constructed directly, without copy elision
+ task(task const &) = delete;
+ struct promise_type {
+ promise_type() { throw_c(promise_ctor); }
+ ~promise_type() { record(promise_dtor); }
+ promise_type(const promise_type &) = delete;
+ task get_return_object() {
+ throw_c(get_return_obj);
+ return {};
+ }
+ auto initial_suspend() {
+ throw_c(init_suspend);
+ struct initial_awaiter {
+ initial_awaiter() { throw_c(awaiter_ctor); }
+ ~initial_awaiter() { record(awaiter_dtor); }
+ initial_awaiter(const initial_awaiter &) = delete;
+ bool await_ready() {
+ throw_c(init_a_ready);
+ return false;
+ }
+ bool await_suspend(std::coroutine_handle<void>) {
+ throw_c(init_a_suspend);
+ return false;
+ }
+ void await_resume() {
+ // From this point onward, all exceptions are handled by
+ // `unhandled_exception` Since the defect of exceptions escaping from
+ // `unhandled_exception` remains unresolved (CWG2934), this test only
+ // covers the coroutine startup phase. Once CWG2934 is resolved,
+ // further tests can be added based on this one.
+ record(init_a_resume);
+ }
+ };
+
+ return initial_awaiter{};
+ }
+ void return_void() { record(return_v); }
+ void unhandled_exception() { record(unhandled); }
+ // Note that no exceptions may leak after final_suspend is called, otherwise
+ // the behavior is undefined
+ std::suspend_never final_suspend() noexcept {
+ record(fin_suspend);
+ return {};
+ }
+ };
+};
+
+task coro(copy_observer) { co_return; }
+
+void catch_coro() try { coro(global_observer); } catch (...) {
+}
+
+// Currently, the conditions at the eight potential exception-throwing points
+// need to be checked. More checkpoints can be added after CWG2934 is resolved.
+void test() {
+ per_test_counts_type e{};
+ allocate_count = 0;
+ catch_coro();
+ e = {
+ 1, // para_copy_ctor
+ 0, // para_dtor
+ 0, // promise_ctor
+ 0, // promise_dtor
+ 0, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 0, // promise_dtor
+ 0, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 1, // task_dtor
+ 1, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e ==
+ per_test_counts); // Clang currently fails starting from this line. If
+ // the code you modified causes tests above this line
+ // to fail, it indicates that you have broken the
+ // correct code and should start over from scratch.
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 1, // task_dtor
+ 1, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 1, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 1, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 1, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 1, // init_a_suspend
+ 0, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ // Test for execution without exceptions
+ {
+ coro(global_observer);
+ }
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 1, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 1, // init_a_suspend
+ 1, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 1, // return_v,
+ 0, // unhandled,
+ 1, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ assert(allocate_count == 0);
+}
+
+} // namespace direct_emit
+
+namespace no_direct_emit {
+
+struct gro_tag_t {};
+
+struct task {
+ task(gro_tag_t) { throw_c(task_ctor); }
+ ~task() { record(task_dtor); }
+ // In this test, the task should be constructed directly, without copy elision
+ task(task const &) = delete;
+ struct promise_type {
+ promise_type() { throw_c(promise_ctor); }
+ ~promise_type() { record(promise_dtor); }
+ promise_type(const promise_type &) = delete;
+ gro_tag_t get_return_object() {
+ throw_c(get_return_obj);
+ return {};
+ }
+ auto initial_suspend() {
+ throw_c(init_suspend);
+ struct initial_awaiter {
+ initial_awaiter() { throw_c(awaiter_ctor); }
+ ~initial_awaiter() { record(awaiter_dtor); }
+ initial_awaiter(const initial_awaiter &) = delete;
+ bool await_ready() {
+ throw_c(init_a_ready);
+ return false;
+ }
+ bool await_suspend(std::coroutine_handle<void>) {
+ throw_c(init_a_suspend);
+ return false;
+ }
+ void await_resume() {
+ // From this point onward, all exceptions are handled by
+ // `unhandled_exception` Since the defect of exceptions escaping from
+ // `unhandled_exception` remains unresolved (CWG2934), this test only
+ // covers the coroutine startup phase. Once CWG2934 is resolved,
+ // further tests can be added based on this one.
+ record(init_a_resume);
+ }
+ };
+
+ return initial_awaiter{};
+ }
+ void return_void() { record(return_v); }
+ void unhandled_exception() { record(unhandled); }
+ // Note that no exceptions may leak after final_suspend is called, otherwise
+ // the behavior is undefined
+ std::suspend_never final_suspend() noexcept {
+ record(fin_suspend);
+ return {};
+ }
+ };
+};
+
+task coro(copy_observer) { co_return; }
+
+void catch_coro() try { coro(global_observer); } catch (...) {
+}
+
+// Currently, the conditions at the eight potential exception-throwing points
+// need to be checked. More checkpoints can be added after CWG2934 is resolved.
+void test() {
+ per_test_counts_type e{};
+ allocate_count = 0;
+ catch_coro();
+ e = {
+ 1, // para_copy_ctor
+ 0, // para_dtor
+ 0, // promise_ctor
+ 0, // promise_dtor
+ 0, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 0, // promise_dtor
+ 0, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 0, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 1, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 0, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 1, // init_suspend
+ 0, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 1, // awaiter_ctor
+ 0, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 0, // init_a_suspend
+ 0, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 0, // task_ctor
+ 0, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 1, // init_a_suspend
+ 0, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 0, // return_v,
+ 0, // unhandled,
+ 0, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ catch_coro();
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 0, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 1, // init_a_suspend
+ 1, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 1, // return_v,
+ 0, // unhandled,
+ 1, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ // Test for execution without exceptions
+ {
+ coro(global_observer);
+ }
+ e = {
+ 2, // para_copy_ctor
+ 2, // para_dtor
+ 1, // promise_ctor
+ 1, // promise_dtor
+ 1, // get_return_obj
+ 1, // task_ctor
+ 1, // task_dtor
+ 1, // init_suspend
+ 1, // init_a_ready
+ 1, // init_a_suspend
+ 1, // init_a_resume
+ 1, // awaiter_ctor
+ 1, // awaiter_dtor
+ 1, // return_v,
+ 0, // unhandled,
+ 1, // fin_suspend
+ };
+ assert(e == per_test_counts);
+ clear();
+ assert(allocate_count == 0);
+}
+
+} // namespace no_direct_emit
+
+int main() {
+ direct_emit::test();
+ // clear the global state that records the already thrown points
+ checked_cond = {};
+ no_direct_emit::test();
+}
>From 578203b0516c090aaff7c5652bcb2a673df1f9d5 Mon Sep 17 00:00:00 2001
From: Yexuan Xiao <bizwen at nykz.org>
Date: Mon, 2 Feb 2026 18:03:06 +0800
Subject: [PATCH 2/2] Add noexcept to initial_suspend to enable HALO and ensure
the tests pass
---
clang/test/CodeGenCoroutines/coro-halo.cpp | 2 +-
clang/test/CodeGenCoroutines/pr56919.cpp | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/clang/test/CodeGenCoroutines/coro-halo.cpp b/clang/test/CodeGenCoroutines/coro-halo.cpp
index e75bedaf81fa2..915e0efa9bbea 100644
--- a/clang/test/CodeGenCoroutines/coro-halo.cpp
+++ b/clang/test/CodeGenCoroutines/coro-halo.cpp
@@ -13,7 +13,7 @@ template <typename T> struct generator {
this->current_value = value;
return {};
}
- std::suspend_always initial_suspend() { return {}; }
+ std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
generator get_return_object() { return generator{this}; };
void unhandled_exception() {}
diff --git a/clang/test/CodeGenCoroutines/pr56919.cpp b/clang/test/CodeGenCoroutines/pr56919.cpp
index baa8c27ce6649..84a27a302e607 100644
--- a/clang/test/CodeGenCoroutines/pr56919.cpp
+++ b/clang/test/CodeGenCoroutines/pr56919.cpp
@@ -30,7 +30,7 @@ class Task final {
void unhandled_exception() {}
- std::suspend_always initial_suspend() { return {}; }
+ std::suspend_always initial_suspend() noexcept { return {}; }
auto await_transform(Task<void> co) {
return await_transform(std::move(co.handle_.promise()));
More information about the cfe-commits
mailing list