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 <[email protected]> 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 <[email protected]> --- 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 <[email protected]> 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 { _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
