[clang] [CIR] Fix cleanup ordering of coroutines (PR #230574)
Morris Hafner via cfe-commits
cfe-commits at lists.llvm.org
Sat Oct 10 05:02:02 PDT 2026
https://github.com/mmha updated https://github.com/llvm/llvm-project/pull/230574
>From ce764c60615a26781677c5e9b05ed9604f23a1cd Mon Sep 17 00:00:00 2001
From: Morris Hafner <mhafner at nvidia.com>
Date: Fri, 9 Oct 2026 16:16:37 +0000
Subject: [PATCH 1/2] [CIR] Fix cleanup ordering of coroutines
* Capture promise and parameter-copy cleanups for the destroy region
* Clean up the GRO expression with any temporaries before `initial_suspend`
* Add a cleanup scope for the coroutine body
* Destroy the promise and parameter copies in the destroy region
Co-Authored-By: Claude Opus 5.5 <noreply at anthropic.com>
---
clang/include/clang/CIR/Dialect/IR/CIROps.td | 12 +-
clang/include/clang/CIR/MissingFeatures.h | 1 +
clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp | 29 +-
clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 38 +-
clang/lib/CIR/CodeGen/CIRGenFunction.h | 7 +
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 12 +-
.../CodeGenCoroutines/coro-fallthrough.cpp | 28 ++
.../CodeGenCoroutines/coro-frame-dtors.cpp | 430 ++++++++++++++++++
8 files changed, 519 insertions(+), 38 deletions(-)
create mode 100644 clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 1f264ce184c4a..cd6e71ce5eaec 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -4908,12 +4908,12 @@ def CIR_CoroutineOp : CIR_Op<"coroutine", [
- `unwind`: TODO.
- - `destroy`: destroys the coroutine frame (`coro.free` + `delete`).
- Reached by an explicit `destroy()` call on a suspended handle, by
- normal completion through `final_suspend`, or by an exception
- escaping the coroutine. Falls through to `exit` except when reached
- due to an escaping exception, where it continues propagating that
- exception instead.
+ - `destroy`: destroys the coroutine state: the promise, then the
+ parameter copies, then the coroutine frame (`coro.free` + `delete`).
+ Reached by an explicit `destroy()` call on a suspended handle, or by
+ normal completion through `final_suspend`, and falls through to
+ `exit`. It runs the destructors unconditionally, so an exception must
+ not reach it. Exceptional exits are not modeled in `unwind`.
- `exit`: the shared return-to-caller path. Reached directly from any
suspend point that isn't being destroyed (the coroutine is just
diff --git a/clang/include/clang/CIR/MissingFeatures.h b/clang/include/clang/CIR/MissingFeatures.h
index c908633c4ed6c..597c12d308616 100644
--- a/clang/include/clang/CIR/MissingFeatures.h
+++ b/clang/include/clang/CIR/MissingFeatures.h
@@ -126,6 +126,7 @@ struct MissingFeatures {
// Coroutines
static bool coroAwaitFullExprCleanups() { return false; }
static bool coroOutsideFrameMD() { return false; }
+ static bool coroUnwindRegion() { return false; }
// Various handling of deferred processing in CIRGenModule.
static bool cgmRelease() { return false; }
diff --git a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
index ad34df762fe19..d2fb569d98fe0 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
@@ -409,20 +409,27 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) {
// For zipping the arg map into debug info.
assert(!cir::MissingFeatures::generateDebugInfo());
+ // Capture frame cleanups for destroy: promise first, then parameter copies,
+ // before freeing the frame ([dcl.fct.def.coroutine]p11, p13). Local cleanup
+ // scopes would end their lifetimes too early.
+
+ assert(!cir::MissingFeatures::coroUnwindRegion());
+ CapturedCleanups frameCleanups(*this, /*active=*/true);
+
// Create parameter copies. We do it before creating a promise, since an
// evolution of coroutine TS may allow promise constructor to observe
// parameter copies.
assert(!cir::MissingFeatures::coroOutsideFrameMD());
for (auto *pm : paramMoves) {
- if (emitStmt(pm, /*useCurrentScope=*/true).failed())
- return mlir::failure();
- paramReplacer.addCopy(cast<DeclStmt>(pm));
+ const auto *paramMove = cast<DeclStmt>(pm);
+ emitAutoVarDeclWithCapturedCleanups(
+ cast<VarDecl>(*paramMove->getSingleDecl()), frameCleanups);
+ paramReplacer.addCopy(paramMove);
}
// Builds `initial_suspend`.
auto initialSuspendBuilder = [&]() -> mlir::LogicalResult {
- if (emitStmt(s.getPromiseDeclStmt(), /*useCurrentScope=*/true).failed())
- return mlir::failure();
+ emitAutoVarDeclWithCapturedCleanups(*s.getPromiseDecl(), frameCleanups);
// returnValue should be valid as long as the coroutine's return type
// is not void. The assertion could help us to reduce the check later.
assert(returnValue.isValid() == (bool)s.getReturnStmt());
@@ -435,10 +442,14 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) {
// So we couldn't emit return value when we emit return statment,
// otherwise the call to get_return_object wouldn't be in front
// of initial_suspend.
- if (returnValue.isValid())
+ if (returnValue.isValid()) {
+ // Sema may omit ExprWithCleanups when the return types match. Clean
+ // up the GRO full-expression with its temporaries here explicitly.
+ FullExprCleanupScope groScope(*this, s.getReturnValue());
emitAnyExprToMem(s.getReturnValue(), returnValue,
s.getReturnValue()->getType().getQualifiers(),
/*isInit*/ true);
+ }
curCoro.data->currentAwaitKind = cir::AwaitKind::Init;
curCoro.data->exceptionHandler = s.getExceptionHandler();
@@ -453,6 +464,7 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) {
// Builds `body`: the user-written coroutine code, including its
// implicit `try { ... } catch (...) { unhandled_exception(); }`.
auto bodyBuilder = [&]() -> mlir::LogicalResult {
+ RunCleanupsScope bodyRegionScope(*this);
curCoro.data->currentAwaitKind = cir::AwaitKind::User;
if (curCoro.data->exceptionHandler) {
@@ -519,8 +531,11 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) {
return mlir::success();
};
- // Emit "if (coro.free(CoroId, CoroBegin)) Deallocate;"
+ // Destroy the promise and the parameter copies, then emit
+ // "if (coro.free(CoroId, CoroBegin)) Deallocate;"
auto destroyBuilder = [&]() -> mlir::LogicalResult {
+ frameCleanups.emitIntoCleanupRegion(openCurlyLoc);
+
Stmt *deallocate = s.getDeallocate();
if (emitStmt(deallocate, /*useCurrentScope=*/true).failed()) {
cgm.error(deallocate->getBeginLoc(),
diff --git a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
index 01b9f810fbed4..a7fc8c2018ee0 100644
--- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp
@@ -389,12 +389,6 @@ void CIRGenFunction::emitAutoVarDecl(const VarDecl &d) {
void CIRGenFunction::emitLoopConditionVariable(const VarDecl &d,
CapturedCleanups &condCleanup) {
- // A condition variable always has automatic storage duration, so this
- // mirrors the auto-var path of emitVarDecl/emitAutoVarDecl. Capture the
- // lifetime-end cleanup pushed while emitting the alloca, but emit the
- // initializer with capturing disabled so its own cleanups get their normal
- // cir.cleanup.scope handling. The variable's destructor cleanup is captured
- // separately after initialization.
assert(d.hasLocalStorage() && "loop condition variable is not local");
// Mirror the diagnostic emitted by emitVarDecl on the automatic-storage path.
@@ -404,22 +398,23 @@ void CIRGenFunction::emitLoopConditionVariable(const VarDecl &d,
cgm.errorNYI(d.getSourceRange(),
"emitLoopConditionVariable: OpenCL local address space");
+ emitAutoVarDeclWithCapturedCleanups(d, condCleanup,
+ /*guardDestruction=*/true);
+}
+
+void CIRGenFunction::emitAutoVarDeclWithCapturedCleanups(
+ const VarDecl &d, CapturedCleanups &cleanups, bool guardDestruction) {
+ assert(d.hasLocalStorage() && "captured cleanups for a non-local variable");
CIRGenFunction::VarDeclContext varDeclCtx{*this, &d};
CIRGenFunction::AutoVarEmission emission = [&] {
- CapturedCleanups::CaptureScope capture(condCleanup);
+ CapturedCleanups::CaptureScope capture(cleanups);
return emitAutoVarAlloca(d);
}();
- // The condition variable's destructor is captured into the loop op's
- // per-iteration cleanup region, which structurally spans the initializer.
- // If the initializer throws, the variable was never constructed and its
- // destructor must not run. Classic codegen avoids this by pushing the
- // cleanup only after the initializer, but our deferred cleanup necessarily
- // covers the whole condition region, so guard it with an active flag that is
- // false while the initializer runs and set to true once construction
- // completes. The flag is stored to on every iteration, so it also resets
- // correctly across iterations.
- bool needsCleanup = d.needsDestruction(getContext()) != QualType::DK_none;
+ // If the cleanup region covers initialization, guard destruction so it
+ // cannot run when the initializer throws. Reset the flag each iteration.
+ bool needsCleanup =
+ guardDestruction && d.needsDestruction(getContext()) != QualType::DK_none;
Address activeFlag = Address::invalid();
if (needsCleanup) {
mlir::Location loc = getLoc(d.getSourceRange());
@@ -430,7 +425,14 @@ void CIRGenFunction::emitLoopConditionVariable(const VarDecl &d,
builder.createFlagStore(loc, false, activeFlag.getPointer());
}
+ // Initializer temporaries keep their normal cleanup handling. For unguarded
+ // regions, initialization must not leave uncaptured cleanups, e.g. those of
+ // lifetime-extended temporaries.
+ [[maybe_unused]] EHScopeStack::stable_iterator initDepth =
+ ehStack.stable_begin();
emitAutoVarInit(emission);
+ assert((guardDestruction || ehStack.stable_begin() == initDepth) &&
+ "initializer left cleanups that cannot be captured");
if (needsCleanup) {
// Construction has completed, so activate the destructor cleanup.
@@ -439,7 +441,7 @@ void CIRGenFunction::emitLoopConditionVariable(const VarDecl &d,
}
{
- CapturedCleanups::CaptureScope capture(condCleanup);
+ CapturedCleanups::CaptureScope capture(cleanups);
emitAutoVarCleanups(emission);
}
diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h
index 6ca9f27748b28..2efbad117824a 100644
--- a/clang/lib/CIR/CodeGen/CIRGenFunction.h
+++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h
@@ -1910,6 +1910,13 @@ class CIRGenFunction : public CIRGenTypeCache {
void emitLoopConditionVariable(const clang::VarDecl &d,
CapturedCleanups &condCleanup);
+ /// Emit \p d with captured cleanups. Set \p guardDestruction if the cleanup
+ /// region covers initialization. Otherwise it must be reached only after
+ /// initialization.
+ void emitAutoVarDeclWithCapturedCleanups(const clang::VarDecl &d,
+ CapturedCleanups &cleanups,
+ bool guardDestruction = false);
+
/// Emit the initializer for an allocated variable. If this call is not
/// associated with the call to emitAutoVarAlloca (as the address of the
/// emission is not directly an alloca), the allocatedSeparately parameter can
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 8a62ae541b4b6..42b303ebf4e3b 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -3813,9 +3813,8 @@ void cir::CoroutineOp::getSuccessorRegions(
}
// body can fall through into final_suspend (co_return), exit directly
- // from any suspend_point inside it that just suspends, or reach destroy,
- // either from a suspend_point being destroyed, or from an exception
- // escaping body's own catch-all.
+ // from any suspend_point inside it that just suspends, or reach destroy
+ // from a suspend_point being destroyed.
if (parent == &getBody()) {
regions.emplace_back(&getFinalSuspend());
regions.emplace_back(&getExit()); // any suspend_point inside body
@@ -3834,12 +3833,11 @@ void cir::CoroutineOp::getSuccessorRegions(
return;
}
- // destroy has two possible outcomes depending on why it was entered:
- // ordinary destroy dispatch falls through to exit (normal return); an
- // exception that reached destroy needs to keep propagating instead, i.e.
- // leave the whole op rather than go through exit's cir.return.
+ // destroy falls through to exit (normal return).
if (parent == &getDestroy()) {
regions.emplace_back(&getExit());
+ // Replace the parent edge with the unwind region once it is implemetend.
+ assert(!cir::MissingFeatures::coroUnwindRegion());
regions.push_back(RegionSuccessor(getOperation())); // unwind out of the op
return;
}
diff --git a/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp b/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp
index ff3e966ac3324..3faa94f9247d6 100644
--- a/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp
+++ b/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp
@@ -2,6 +2,8 @@
// RUN: FileCheck --input-file=%t.cir %s -check-prefixes=CIR,CIR-NOEH
// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fcxx-exceptions -fexceptions -fclangir -emit-cir %s -o %t-eh.cir
// RUN: FileCheck --input-file=%t-eh.cir %s -check-prefixes=CIR,CIR-EH
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -O1 -fclangir -emit-cir %s -o %t-O1.cir
+// RUN: FileCheck --input-file=%t-O1.cir %s -check-prefix=CIR-O1
// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -disable-llvm-passes %s -o %t.ll
// RUN: FileCheck --input-file=%t.ll %s -check-prefixes=OGCG,OGCG-NOEH
// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fcxx-exceptions -fexceptions -emit-llvm -disable-llvm-passes %s -o %t-eh.ll
@@ -33,6 +35,7 @@ struct Task {
};
S make();
+void use(int &);
Task falls_off_end() {
co_await std::suspend_always{};
@@ -106,6 +109,31 @@ Task falls_off_end_with_extended_temporary() {
// OGCG-NOEH-NEXT: call void @_ZN4Task12promise_type11return_voidEv(
// OGCG-EH-NEXT: invoke void @_ZN4Task12promise_type11return_voidEv(
+// With optimizations, even a local variable without a destructor has a
+// cleanup, its lifetime end.
+
+Task falls_off_end_with_trivial_local() {
+ co_await std::suspend_always{};
+ int i = 0;
+ use(i);
+}
+
+// CIR-O1-LABEL: cir.func coroutine {{.*}} @_Z32falls_off_end_with_trivial_localv(
+// CIR-O1: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-O1: %[[I:.*]] = cir.alloca "i"
+// CIR-O1: }, body : {
+// CIR-O1: cir.lifetime.start %[[I]]
+// CIR-O1-NEXT: cir.cleanup.scope {
+// CIR-O1: cir.call @_Z3useRi(%[[I]])
+// CIR-O1-NEXT: cir.yield
+// CIR-O1-NEXT: } cleanup normal {
+// CIR-O1-NEXT: cir.lifetime.end %[[I]]
+// CIR-O1-NEXT: cir.yield
+// CIR-O1-NEXT: }
+// CIR-O1-NEXT: cir.call @_ZN4Task12promise_type11return_voidEv(%[[PROMISE]])
+// CIR-O1-NEXT: cir.co_return
+// CIR-O1-NEXT: }, finalSuspend : {
+
// A co_return inside an if does not make the end of the body unreachable, so
// the fall-through handler is still emitted, after the local variables are
// destroyed.
diff --git a/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp b/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp
new file mode 100644
index 0000000000000..623c114e00e02
--- /dev/null
+++ b/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp
@@ -0,0 +1,430 @@
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fclangir -emit-cir %s -o %t.cir
+// RUN: FileCheck --input-file=%t.cir %s -check-prefixes=CIR,CIR-NOEH
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -fcxx-exceptions -fexceptions -fclangir -emit-cir %s -o %t-eh.cir
+// RUN: FileCheck --input-file=%t-eh.cir %s -check-prefixes=CIR,CIR-EH
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -O1 -fclangir -emit-cir %s -o %t-O1.cir
+// RUN: FileCheck --input-file=%t-O1.cir %s -check-prefix=CIR-O1
+// RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -disable-llvm-passes %s -o %t.ll
+// RUN: FileCheck --input-file=%t.ll %s -check-prefix=OGCG
+
+// Ensure coroutine parameter copies, promises and the body are destroyed in the correct order.
+
+#include "Inputs/coroutine.h"
+
+struct Dtor {
+ ~Dtor();
+ int x;
+};
+
+struct Task {
+ struct promise_type {
+ ~promise_type();
+ Task get_return_object() noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+struct TrivialTask {
+ struct promise_type {
+ TrivialTask get_return_object() noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+Task promise_dtor() { co_return; }
+
+// The promise's destructor is not scoped to initial_suspend, where the promise
+// is constructed, and runs only in destroy.
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z12promise_dtorv(
+// CIR: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-NOT: cir.cleanup.scope
+// CIR: cir.coroutine initialSuspend : {
+// CIR-NOT: {{cir.cleanup.scope|cir.call @_ZN4Task12promise_typeD1Ev}}
+// CIR: cir.await(init,
+// CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev
+// CIR: }, body : {
+// CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]])
+// CIR-NEXT: %[[MEM:.*]] = cir.coro.intrinsic.free
+// CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev
+// CIR: cir.call @_ZdlPvm(%[[MEM]],
+// CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev
+// CIR: }, exit : {
+// CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev
+
+// OGCG-LABEL: define {{.*}} @_Z12promise_dtorv(
+// OGCG: coro.cleanup:
+// OGCG-NEXT: call void @_ZN4Task12promise_typeD1Ev(ptr {{.*}} %__promise)
+// OGCG: call ptr @llvm.coro.free(
+
+TrivialTask param_dtor(Dtor p) {
+ co_await std::suspend_always{};
+}
+
+// The parameter copy's destructor is not scoped around the whole
+// cir.coroutine, where it would run on every return to the caller.
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z10param_dtor4Dtor(
+// CIR: %[[P:.*]] = cir.alloca "p"
+// CIR-NOT: cir.cleanup.scope
+// CIR: cir.coroutine initialSuspend : {
+// CIR-NOT: cir.call @_ZN4DtorD1Ev
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]])
+// CIR-NEXT: cir.coro.intrinsic.free
+// CIR-NOT: cir.call @_ZN4DtorD1Ev
+// CIR: }, exit : {
+// CIR-NOT: cir.call @_ZN4DtorD1Ev
+// CIR: cir.return
+// CIR-NEXT: }
+// CIR-NOT: cir.cleanup.scope
+// CIR-NOT: cir.call @_ZN4DtorD1Ev
+
+// OGCG-LABEL: define {{.*}} @_Z10param_dtor4Dtor(
+// OGCG: %[[P_COPY:.+]] = alloca %struct.Dtor
+// OGCG: coro.cleanup:
+// OGCG: call void @_ZN4DtorD1Ev(ptr {{[^,]*}} %[[P_COPY]])
+// OGCG: call ptr @llvm.coro.free(
+
+Task promise_and_params(Dtor a, int i, Dtor b) {
+ Dtor local;
+ co_await std::suspend_always{};
+}
+
+// The promise is destroyed first, then the parameter copies in reverse order
+// of construction. A local variable of the body is still destroyed when
+// control leaves the body, before the implicit co_return calls return_void().
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z18promise_and_params4DtoriS_(
+// CIR-DAG: %[[A:.*]] = cir.alloca "a"
+// CIR-DAG: %[[B:.*]] = cir.alloca "b"
+// CIR-DAG: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-NOEH-DAG: %[[LOCAL:.*]] = cir.alloca "local"
+// CIR-NOT: cir.call {{@_ZN4DtorD1Ev|@_ZN4Task12promise_typeD1Ev}}
+// CIR: cir.coroutine initialSuspend : {
+// CIR-NOT: cir.call {{@_ZN4DtorD1Ev|@_ZN4Task12promise_typeD1Ev}}
+// CIR: }, body : {
+// CIR-EH: %[[LOCAL:.*]] = cir.alloca "local"
+// CIR: cir.await(user,
+// CIR: },)
+// CIR-NEXT: cir.yield
+// CIR-NOEH-NEXT: } cleanup normal {
+// CIR-EH-NEXT: } cleanup all {
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[LOCAL]])
+// CIR-NEXT: cir.yield
+// CIR-NEXT: }
+// CIR-NEXT: cir.call @_ZN4Task12promise_type11return_voidEv(
+// CIR-NEXT: cir.co_return
+// CIR-NOT: cir.call {{@_ZN4DtorD1Ev|@_ZN4Task12promise_typeD1Ev}}
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]])
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[B]])
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[A]])
+// CIR-NEXT: cir.coro.intrinsic.free
+// CIR-NOT: cir.call {{@_ZN4DtorD1Ev|@_ZN4Task12promise_typeD1Ev}}
+// CIR: }, exit : {
+// CIR-NOT: cir.call {{@_ZN4DtorD1Ev|@_ZN4Task12promise_typeD1Ev}}
+
+// At -O1, the lifetime of the promise and of the parameter copies also ends in
+// destroy, after each object is destroyed.
+
+// CIR-O1-LABEL: cir.func coroutine {{.*}} @_Z18promise_and_params4DtoriS_(
+// CIR-O1-DAG: %[[A:.*]] = cir.alloca "a"
+// CIR-O1-DAG: %[[B:.*]] = cir.alloca "b"
+// CIR-O1-DAG: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-O1: cir.lifetime.start %[[A]]
+// CIR-O1: cir.lifetime.start %[[I:.*]] : !cir.ptr<!s32i>
+// CIR-O1: cir.lifetime.start %[[B]]
+// CIR-O1: cir.coroutine initialSuspend : {
+// CIR-O1-NEXT: cir.lifetime.start %[[PROMISE]]
+// CIR-O1-NOT: cir.lifetime.end %[[PROMISE]]
+// CIR-O1-NOT: cir.lifetime.end %[[B]]
+// CIR-O1-NOT: cir.lifetime.end %[[A]]
+// CIR-O1: }, destroy : {
+// CIR-O1-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]])
+// CIR-O1-NEXT: cir.lifetime.end %[[PROMISE]]
+// CIR-O1-NEXT: cir.call @_ZN4DtorD1Ev(%[[B]])
+// CIR-O1-NEXT: cir.lifetime.end %[[B]]
+// CIR-O1-NEXT: cir.lifetime.end %[[I]]
+// CIR-O1-NEXT: cir.call @_ZN4DtorD1Ev(%[[A]])
+// CIR-O1-NEXT: cir.lifetime.end %[[A]]
+// CIR-O1-NEXT: cir.coro.intrinsic.free
+
+// OGCG-LABEL: define {{.*}} @_Z18promise_and_params4DtoriS_(
+// OGCG: %[[A_COPY:.+]] = alloca %struct.Dtor
+// OGCG: %[[B_COPY:.+]] = alloca %struct.Dtor
+// OGCG: coro.cleanup:
+// OGCG-NEXT: call void @_ZN4Task12promise_typeD1Ev(ptr {{[^,]*}} %__promise)
+// OGCG: call void @_ZN4DtorD1Ev(ptr {{[^,]*}} %[[B_COPY]])
+// OGCG: call void @_ZN4DtorD1Ev(ptr {{[^,]*}} %[[A_COPY]])
+// OGCG: call ptr @llvm.coro.free(
+
+// The promise constructor sees the parameter copies.
+struct PreviewTask {
+ struct promise_type {
+ promise_type(Dtor &);
+ ~promise_type();
+ PreviewTask get_return_object() noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+PreviewTask promise_preview(Dtor d) { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z15promise_preview4Dtor(
+// CIR: %[[D:.*]] = cir.alloca "d"
+// CIR: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR: cir.coroutine initialSuspend : {
+// CIR-NEXT: cir.call @_ZN11PreviewTask12promise_typeC1ER4Dtor(%[[PROMISE]], %[[D]])
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN11PreviewTask12promise_typeD1Ev(%[[PROMISE]])
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[D]])
+// CIR-NEXT: cir.coro.intrinsic.free
+
+// A temporary in a parameter copy's or the promise's initializer is still
+// destroyed at the end of its full-expression; it does not leak into destroy
+// and does not leave initial_suspend unterminated.
+struct Tmp {
+ Tmp();
+ ~Tmp();
+};
+
+struct TmpMove {
+ TmpMove(TmpMove &&, Tmp = Tmp());
+ ~TmpMove();
+};
+
+struct TmpTask {
+ struct promise_type {
+ promise_type(Tmp = Tmp());
+ ~promise_type();
+ TmpTask get_return_object() noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+TmpTask init_temporaries(TmpMove m) { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z16init_temporaries7TmpMove(
+// CIR: %[[M:.*]] = cir.alloca "m"
+// CIR: %[[MTMP:.*]] = cir.alloca "agg.tmp0"
+// CIR: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR: %[[PTMP:.*]] = cir.alloca "agg.tmp1"
+// CIR: cir.call @_ZN3TmpC1Ev(%[[MTMP]])
+// CIR-NEXT: cir.cleanup.scope {
+// CIR-NEXT: cir.call @_ZN7TmpMoveC1EOS_3Tmp(%[[M]], %arg0, %[[MTMP]])
+// CIR-NEXT: cir.yield
+// CIR-NOEH-NEXT: } cleanup normal {
+// CIR-EH-NEXT: } cleanup all {
+// CIR-NEXT: cir.call @_ZN3TmpD1Ev(%[[MTMP]])
+// CIR-NEXT: cir.yield
+// CIR-NEXT: }
+// CIR-NEXT: cir.coroutine initialSuspend : {
+// CIR-NEXT: cir.call @_ZN3TmpC1Ev(%[[PTMP]])
+// CIR-NEXT: cir.cleanup.scope {
+// CIR-NEXT: cir.call @_ZN7TmpTask12promise_typeC1E3Tmp(%[[PROMISE]], %[[PTMP]])
+// CIR-NEXT: cir.yield
+// CIR-NOEH-NEXT: } cleanup normal {
+// CIR-EH-NEXT: } cleanup all {
+// CIR-NEXT: cir.call @_ZN3TmpD1Ev(%[[PTMP]])
+// CIR-NEXT: cir.yield
+// CIR-NEXT: }
+// CIR-NOT: cir.cleanup.scope
+// CIR: cir.await(init,
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN7TmpTask12promise_typeD1Ev(%[[PROMISE]])
+// CIR-NEXT: cir.call @_ZN7TmpMoveD1Ev(%[[M]])
+// CIR-NEXT: cir.coro.intrinsic.free
+// CIR-NOT: cir.call @_ZN3TmpD1Ev
+// CIR: }, exit : {
+
+// A temporary in the get_return_object() call is destroyed at the end of that
+// full-expression, inside initial_suspend ([class.temporary]p4). Classic
+// CodeGen does not end this full-expression: it leaves the temporary's cleanup
+// on the EH stack and destroys it in coro.cleanup, with the coroutine state.
+struct GROTask {
+ struct promise_type {
+ GROTask get_return_object(Tmp = Tmp()) noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+GROTask gro_temporary() { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z13gro_temporaryv(
+// CIR: cir.coroutine initialSuspend : {
+// CIR: cir.cleanup.scope {
+// CIR-NEXT: cir.call @_ZN7GROTask12promise_type17get_return_objectE3Tmp(
+// CIR-NOT: cir.await
+// CIR-NOEH: } cleanup normal {
+// CIR-EH: } cleanup all {
+// CIR-NEXT: cir.call @_ZN3TmpD1Ev
+// CIR-NEXT: cir.yield
+// CIR-NEXT: }
+// CIR-NEXT: cir.call @_ZN7GROTask12promise_type15initial_suspendEv(
+// CIR: cir.await(init,
+// CIR: }, destroy : {
+// CIR-NOT: cir.call @_ZN3TmpD1Ev
+// CIR: }, exit : {
+
+// OGCG-LABEL: define {{.*}} @_Z13gro_temporaryv(
+// OGCG: call void @_ZN7GROTask12promise_type17get_return_objectE3Tmp(
+// OGCG-NOT: call void @_ZN3TmpD1Ev
+// OGCG: coro.cleanup:
+// OGCG-NEXT: call void @_ZN3TmpD1Ev(
+
+// A temporary that such a default argument creates conditionally is destroyed
+// at the end of the full-expression too, guarded by its active flag. This also
+// holds for a parameter copy's constructor.
+bool cond();
+
+struct CondTmp {
+ CondTmp();
+ ~CondTmp();
+ bool ok();
+};
+
+struct CondGROTask {
+ struct promise_type {
+ CondGROTask get_return_object(bool = cond() && CondTmp().ok()) noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+CondGROTask cond_gro_temporary() { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z18cond_gro_temporaryv(
+// CIR: %[[TMP:.*]] = cir.alloca "ref.tmp0" {{.*}} : !cir.ptr<!rec_CondTmp>
+// CIR: %[[ACTIVE:.*]] = cir.alloca "cleanup.cond"
+// CIR: cir.coroutine initialSuspend : {
+// CIR: cir.cleanup.scope {
+// CIR: cir.call @_ZN7CondTmpC1Ev(%[[TMP]])
+// CIR: cir.call @_ZN11CondGROTask12promise_type17get_return_objectEb(
+// CIR-NOEH: } cleanup normal {
+// CIR-EH: } cleanup all {
+// CIR-NEXT: %[[IS_ACTIVE:.*]] = cir.load {{.*}} %[[ACTIVE]]
+// CIR-NEXT: cir.if %[[IS_ACTIVE]] {
+// CIR-NEXT: cir.call @_ZN7CondTmpD1Ev(%[[TMP]])
+// CIR: cir.await(init,
+// CIR: }, destroy : {
+// CIR-NOT: cir.call @_ZN7CondTmpD1Ev
+// CIR: }, exit : {
+
+struct CondMove {
+ CondMove(CondMove &&, bool = cond() && CondTmp().ok());
+ ~CondMove();
+};
+
+TrivialTask cond_param_temporary(CondMove m) { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z20cond_param_temporary8CondMove(
+// CIR: %[[M:.*]] = cir.alloca "m"
+// CIR: %[[TMP:.*]] = cir.alloca "ref.tmp0" {{.*}} : !cir.ptr<!rec_CondTmp>
+// CIR: %[[ACTIVE:.*]] = cir.alloca "cleanup.cond"
+// CIR: cir.cleanup.scope {
+// CIR: cir.call @_ZN7CondTmpC1Ev(%[[TMP]])
+// CIR: cir.call @_ZN8CondMoveC1EOS_b(%[[M]],
+// CIR-NOEH: } cleanup normal {
+// CIR-EH: } cleanup all {
+// CIR-NEXT: %[[IS_ACTIVE:.*]] = cir.load {{.*}} %[[ACTIVE]]
+// CIR-NEXT: cir.if %[[IS_ACTIVE]] {
+// CIR-NEXT: cir.call @_ZN7CondTmpD1Ev(%[[TMP]])
+// CIR: cir.coroutine initialSuspend : {
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN8CondMoveD1Ev(%[[M]])
+// CIR-NOT: cir.call @_ZN7CondTmpD1Ev
+// CIR: }, exit : {
+
+// A coroutine that never reaches its end has no co_return and no fallthrough
+// co_return, so final_suspend has no cir.await, but destroy still destroys
+// the promise and the parameter copies.
+struct ValueTask {
+ struct promise_type {
+ ~promise_type();
+ ValueTask get_return_object() noexcept;
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_value(int) noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+ValueTask no_final_await(Dtor p) {
+ for (;;)
+ co_await std::suspend_always{};
+}
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z14no_final_await4Dtor(
+// CIR: %[[P:.*]] = cir.alloca "p"
+// CIR: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR: }, finalSuspend : {
+// CIR-NEXT: cir.yield
+// CIR-NEXT: }, destroy : {
+// CIR-NEXT: cir.call @_ZN9ValueTask12promise_typeD1Ev(%[[PROMISE]])
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]])
+// CIR-NEXT: cir.coro.intrinsic.free
+
+// A member coroutine copies its by-value parameters, but not *this.
+struct S {
+ Task member(Dtor d);
+};
+Task S::member(Dtor d) { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_ZN1S6memberE4Dtor(
+// CIR: %[[D:.*]] = cir.alloca "d"
+// CIR: %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]])
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[D]])
+// CIR-NEXT: cir.coro.intrinsic.free
+
+// An exception from get_return_object() propagates to the caller before the
+// initial suspend point; classic CodeGen destroys the parameter copy and frees
+// the frame on that path. FIXME: The unwind region is not implemented yet, so
+// with -fexceptions CIR currently destroys the copy only in destroy, which the
+// exception does not reach.
+struct ThrowingGROTask {
+ struct promise_type {
+ ThrowingGROTask get_return_object();
+ std::suspend_always initial_suspend() noexcept;
+ std::suspend_always final_suspend() noexcept;
+ void return_void() noexcept;
+ void unhandled_exception() noexcept;
+ };
+};
+
+ThrowingGROTask eh_before_body(Dtor p) { co_return; }
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z14eh_before_body4Dtor(
+// CIR: %[[P:.*]] = cir.alloca "p"
+// CIR-NOT: cir.cleanup.scope
+// CIR: cir.coroutine initialSuspend : {
+// CIR-NEXT: cir.call @_ZN15ThrowingGROTask12promise_type17get_return_objectEv(
+// CIR-NOT: cir.call @_ZN4DtorD1Ev
+// CIR: }, destroy : {
+// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]])
+
+// OGCG-LABEL: define {{.*}} @_Z14eh_before_body4Dtor(
+// OGCG: %[[P_COPY:.+]] = alloca %struct.Dtor
+// OGCG: coro.cleanup:
+// OGCG: call void @_ZN4DtorD1Ev(ptr {{[^,]*}} %[[P_COPY]])
>From f07cd41f39803ec72596175d28a54177d7f7608f Mon Sep 17 00:00:00 2001
From: Morris Hafner <mhafner at nvidia.com>
Date: Sat, 10 Oct 2026 12:01:35 +0000
Subject: [PATCH 2/2] Checkl the cleanup.cond store
---
.../CIR/CodeGenCoroutines/coro-frame-dtors.cpp | 14 ++++++++++++--
1 file changed, 12 insertions(+), 2 deletions(-)
diff --git a/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp b/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp
index 623c114e00e02..1e57ba6695b2d 100644
--- a/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp
+++ b/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp
@@ -318,7 +318,12 @@ CondGROTask cond_gro_temporary() { co_return; }
// CIR: %[[ACTIVE:.*]] = cir.alloca "cleanup.cond"
// CIR: cir.coroutine initialSuspend : {
// CIR: cir.cleanup.scope {
-// CIR: cir.call @_ZN7CondTmpC1Ev(%[[TMP]])
+// CIR-NEXT: %[[FALSE:.*]] = cir.const #false
+// CIR-NEXT: cir.store %[[FALSE]], %[[ACTIVE]]
+// CIR-NEXT: %{{.*}} = cir.ternary(%{{.*}}, true {
+// CIR-NEXT: cir.call @_ZN7CondTmpC1Ev(%[[TMP]])
+// CIR-NEXT: %[[TRUE:.*]] = cir.const #true
+// CIR-NEXT: cir.store %[[TRUE]], %[[ACTIVE]]
// CIR: cir.call @_ZN11CondGROTask12promise_type17get_return_objectEb(
// CIR-NOEH: } cleanup normal {
// CIR-EH: } cleanup all {
@@ -342,7 +347,12 @@ TrivialTask cond_param_temporary(CondMove m) { co_return; }
// CIR: %[[TMP:.*]] = cir.alloca "ref.tmp0" {{.*}} : !cir.ptr<!rec_CondTmp>
// CIR: %[[ACTIVE:.*]] = cir.alloca "cleanup.cond"
// CIR: cir.cleanup.scope {
-// CIR: cir.call @_ZN7CondTmpC1Ev(%[[TMP]])
+// CIR-NEXT: %[[FALSE:.*]] = cir.const #false
+// CIR-NEXT: cir.store %[[FALSE]], %[[ACTIVE]]
+// CIR-NEXT: %{{.*}} = cir.ternary(%{{.*}}, true {
+// CIR-NEXT: cir.call @_ZN7CondTmpC1Ev(%[[TMP]])
+// CIR-NEXT: %[[TRUE:.*]] = cir.const #true
+// CIR-NEXT: cir.store %[[TRUE]], %[[ACTIVE]]
// CIR: cir.call @_ZN8CondMoveC1EOS_b(%[[M]],
// CIR-NOEH: } cleanup normal {
// CIR-EH: } cleanup all {
More information about the cfe-commits
mailing list