https://github.com/mmha updated https://github.com/llvm/llvm-project/pull/230574
>From 2562f73bb2cedf2e939e2f3b44436de9c22e3316 Mon Sep 17 00:00:00 2001 From: Morris Hafner <[email protected]> Date: Fri, 9 Oct 2026 16:16:37 +0000 Subject: [PATCH 1/3] [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 | 13 +- clang/include/clang/CIR/MissingFeatures.h | 1 + clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp | 29 +- clang/lib/CIR/CodeGen/CIRGenDecl.cpp | 52 +-- clang/lib/CIR/CodeGen/CIRGenFunction.h | 15 +- clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 12 +- .../CodeGenCoroutines/coro-fallthrough.cpp | 28 ++ .../CodeGenCoroutines/coro-frame-dtors.cpp | 430 ++++++++++++++++++ 8 files changed, 532 insertions(+), 48 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 63d50761806a6..0d6e87365d3a5 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIROps.td +++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td @@ -4948,12 +4948,13 @@ 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 `initial_suspend`, `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 `initial_suspend` or `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 0ff6c6d44d191..3bf75b2349792 100644 --- a/clang/include/clang/CIR/MissingFeatures.h +++ b/clang/include/clang/CIR/MissingFeatures.h @@ -122,6 +122,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 9d06f29b3ae5e..6b0d031584dd3 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 17d832046363f..a90b82dcaedca 100644 --- a/clang/lib/CIR/CodeGen/CIRGenDecl.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenDecl.cpp @@ -452,14 +452,6 @@ CIRGenFunction::getLifetimeExtendedTemporariesWithDestructor(const VarDecl &d) { void CIRGenFunction::emitLoopConditionVariable( const VarDecl &d, CapturedCleanups &condCleanup, ArrayRef<QualType> extendedTemporaries) { - // 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 the cleanups of its temporaries get - // their normal cir.cleanup.scope handling. Only the cleanups of the - // temporaries that it lifetime-extends are captured, when its - // full-expression ends. 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. @@ -469,27 +461,30 @@ void CIRGenFunction::emitLoopConditionVariable( cgm.errorNYI(d.getSourceRange(), "emitLoopConditionVariable: OpenCL local address space"); + emitAutoVarDeclWithCapturedCleanups(d, condCleanup, + /*guardDestruction=*/true, + extendedTemporaries); +} + +void CIRGenFunction::emitAutoVarDeclWithCapturedCleanups( + const VarDecl &d, CapturedCleanups &cleanups, bool guardDestruction, + ArrayRef<QualType> extendedTemporaries) { + assert(d.hasLocalStorage() && "captured cleanups for a non-local variable"); + assert((guardDestruction || extendedTemporaries.empty()) && + "lifetime-extended temporaries need a guarded cleanup region"); 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. - // - // The temporaries that the initializer lifetime-extends are destroyed with - // the variable as well ([class.temporary]p6). Their cleanups are pushed when - // the initializer's full-expression ends, so capture them too and guard them - // with the same flag. - 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. The + // temporaries that the initializer lifetime-extends are destroyed with the + // variable ([class.temporary]p6), so capture their cleanups under the same + // flag. + bool needsCleanup = + guardDestruction && d.needsDestruction(getContext()) != QualType::DK_none; bool extendsTemporaries = !extendedTemporaries.empty(); // Destroy the variable first, then the temporaries in reverse order of @@ -513,13 +508,18 @@ void CIRGenFunction::emitLoopConditionVariable( builder.createFlagStore(loc, false, activeFlag.getPointer()); } + // Initializer temporaries keep their normal cleanup handling, except for the + // lifetime-extended ones, which are captured when the full-expression ends. + // For unguarded regions, initialization must not leave uncaptured cleanups. EHScopeStack::stable_iterator initDepth = ehStack.stable_begin(); { - LifetimeExtensionCapture capture{&d, &condCleanup, activeFlag}; + LifetimeExtensionCapture capture{&d, &cleanups, activeFlag}; llvm::SaveAndRestore<const LifetimeExtensionCapture *> captureExtended( lifetimeExtensionCapture, extendsTemporaries ? &capture : nullptr); emitAutoVarInit(emission); } + assert((guardDestruction || ehStack.stable_begin() == initDepth) && + "initializer left cleanups that cannot be captured"); // Only lifetime-extended cleanups remain after initialization. If the AST // walk found no extended temporaries, these cleanups are uncaptured and wrap @@ -544,7 +544,7 @@ void CIRGenFunction::emitLoopConditionVariable( } { - 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 90da139c0f19c..2c49e3d5aa550 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.h +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h @@ -208,8 +208,9 @@ class CIRGenFunction : public CIRGenTypeCache { /// what closes those scopes. cir::CleanupScopeOp currentFullExprCleanupScope = nullptr; - /// Set by emitLoopConditionVariable while it emits the initializer of a loop - /// condition variable that lifetime-extends temporaries with a destructor. + /// Set by emitAutoVarDeclWithCapturedCleanups while it emits the initializer + /// of a loop condition variable that lifetime-extends temporaries with a + /// destructor. /// The cleanups of the temporaries that \c extendingDecl extends are /// captured by \c cleanups for the loop's cleanup region and guarded by /// \c activeFlag, instead of opening cir.cleanup.scope operations around the @@ -1956,6 +1957,16 @@ class CIRGenFunction : public CIRGenTypeCache { static llvm::SmallVector<QualType, 2> getLifetimeExtendedTemporariesWithDestructor(const clang::VarDecl &d); + /// Emit \p d with captured cleanups. Set \p guardDestruction if the cleanup + /// region covers initialization. Otherwise it must be reached only after + /// initialization. The cleanups of \p extendedTemporaries (see + /// emitLoopConditionVariable) are captured too; they need + /// \p guardDestruction. + void emitAutoVarDeclWithCapturedCleanups( + const clang::VarDecl &d, CapturedCleanups &cleanups, + bool guardDestruction = false, + llvm::ArrayRef<QualType> extendedTemporaries = {}); + /// 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 f564eee053705..ede311d9c8f61 100644 --- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp @@ -3830,9 +3830,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 @@ -3851,12 +3850,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 d912d5ba7351b..dc395af44f5ed 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: cir-opt -cir-flatten-cfg %t.cir -o %t-flat.cir // RUN: FileCheck --input-file=%t-flat.cir %s -check-prefix=CIR-FLAT // RUN: %clang_cc1 -std=c++20 -triple x86_64-unknown-linux-gnu -emit-llvm -disable-llvm-passes %s -o %t.ll @@ -45,6 +47,7 @@ struct IntTask { }; S make(); +void use(int &); extern "C" void dead(); extern "C" void live(); @@ -124,6 +127,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 9547596a011ecd034c45b347a5645bb19b12712c Mon Sep 17 00:00:00 2001 From: Morris Hafner <[email protected]> Date: Sat, 10 Oct 2026 12:01:35 +0000 Subject: [PATCH 2/3] 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 { >From b33e6070bfc1863d65b058d34e9d62611723afae Mon Sep 17 00:00:00 2001 From: Morris Hafner <[email protected]> Date: Wed, 21 Oct 2026 00:03:55 +0000 Subject: [PATCH 3/3] * Emit nested cleanup scopes for Coroutine frame * Move unwind region missing feature out of destroy region to body region --- clang/include/clang/CIR/Dialect/IR/CIROps.td | 10 +- clang/lib/CIR/CodeGen/CIRGenCleanup.cpp | 133 +++++++++------- clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp | 52 +++++-- clang/lib/CIR/CodeGen/CIRGenFunction.h | 16 ++ clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 8 +- .../CodeGenCoroutines/coro-frame-dtors.cpp | 142 ++++++++++++++++-- clang/unittests/CIR/ControlFlowTest.cpp | 7 +- 7 files changed, 275 insertions(+), 93 deletions(-) diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td index 0d6e87365d3a5..6c94340305eca 100644 --- a/clang/include/clang/CIR/Dialect/IR/CIROps.td +++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td @@ -4953,13 +4953,15 @@ def CIR_CoroutineOp : CIR_Op<"coroutine", [ Reached by an explicit `destroy()` call on a suspended handle, or by normal completion through `initial_suspend` or `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`. + exception must not reach it. An exception from one of these destructors + still destroys the remaining parameter copies and frees the frame before + it leaves the op, through `cir.cleanup.scope` operations of kind `all` + around them. - `exit`: the shared return-to-caller path. Reached directly from any suspend point that isn't being destroyed (the coroutine is just - pausing), and from `destroy` when it was entered via ordinary - dispatch. Holds `coro.end` + the actual `cir.return`. + pausing), and from `destroy`. Holds `coro.end` + the actual + `cir.return`. }]; diff --git a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp index 98fb9502ef016..7904ee18f359a 100644 --- a/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenCleanup.cpp @@ -21,6 +21,7 @@ #include "clang/AST/RecursiveASTVisitor.h" #include "clang/CIR/MissingFeatures.h" +#include "llvm/Support/MathExtras.h" using namespace clang; using namespace clang::CIRGen; @@ -89,6 +90,52 @@ class ConditionalEvaluationFinder return TraverseStmt(e->getOperand()); } }; + +/// Owns a copy of a cleanup and its metadata so they remain valid after the +/// EH stack entry is popped. +class SavedCleanup { + struct alignas(EHScopeStack::ScopeStackAlignment) StorageUnit { + char bytes[EHScopeStack::ScopeStackAlignment]; + }; + llvm::SmallVector<StorageUnit, 8 * sizeof(void *) / sizeof(StorageUnit)> data; + size_t size; + CleanupKind kind; + Address activeFlag; + +public: + explicit SavedCleanup(EHCleanupScope &scope) + : size(scope.getCleanupSize()), activeFlag(scope.getActiveFlag()) { + unsigned cleanupKind = 0; + if (scope.isNormalCleanup()) + cleanupKind |= NormalCleanup; + if (scope.isEHCleanup()) + cleanupKind |= EHCleanup; + if (scope.isLifetimeMarker()) + cleanupKind |= LifetimeMarker; + kind = static_cast<CleanupKind>(cleanupKind); + + data.resize_for_overwrite(llvm::divideCeil(size, sizeof(StorageUnit))); + memcpy(data.data(), scope.getCleanupBuffer(), size); + } + + const void *getData() const { return data.data(); } + size_t getSize() const { return size; } + CleanupKind getKind() const { return kind; } + Address getActiveFlag() const { return activeFlag; } + + EHScopeStack::Cleanup *getCleanup() { + return reinterpret_cast<EHScopeStack::Cleanup *>(data.data()); + } + + EHScopeStack::Cleanup::Flags getFlags() const { + EHScopeStack::Cleanup::Flags flags; + if (kind & NormalCleanup) + flags.setIsNormalCleanupKind(); + if (kind & EHCleanup) + flags.setIsEHCleanupKind(); + return flags; + } +}; } // namespace //===----------------------------------------------------------------------===// @@ -776,35 +823,7 @@ void CIRGenFunction::popCleanupBlock(bool forDeactivation) { return; } - // Copy the cleanup emission data out. This uses either a stack - // array or malloc'd memory, depending on the size, which is - // behavior that SmallVector would provide, if we could use it - // here. Unfortunately, if you ask for a SmallVector<char>, the - // alignment isn't sufficient. - auto *cleanupSource = reinterpret_cast<char *>(scope.getCleanupBuffer()); - alignas(EHScopeStack::ScopeStackAlignment) char - cleanupBufferStack[8 * sizeof(void *)]; - std::unique_ptr<char[]> cleanupBufferHeap; - size_t cleanupSize = scope.getCleanupSize(); - EHScopeStack::Cleanup *cleanup; - - // This is necessary because we are going to deallocate the cleanup - // (in popCleanup) before we emit it. - if (cleanupSize <= sizeof(cleanupBufferStack)) { - memcpy(cleanupBufferStack, cleanupSource, cleanupSize); - cleanup = reinterpret_cast<EHScopeStack::Cleanup *>(cleanupBufferStack); - } else { - cleanupBufferHeap.reset(new char[cleanupSize]); - memcpy(cleanupBufferHeap.get(), cleanupSource, cleanupSize); - cleanup = - reinterpret_cast<EHScopeStack::Cleanup *>(cleanupBufferHeap.get()); - } - - EHScopeStack::Cleanup::Flags cleanupFlags; - if (scope.isNormalCleanup()) - cleanupFlags.setIsNormalCleanupKind(); - if (scope.isEHCleanup()) - cleanupFlags.setIsEHCleanupKind(); + SavedCleanup cleanup(scope); // Determine the active flag for the cleanup handler. Address cleanupActiveFlag = normalActiveFlag.isValid() ? normalActiveFlag @@ -816,7 +835,8 @@ void CIRGenFunction::popCleanupBlock(bool forDeactivation) { // pass handles lowering the structured cleanup scope. ehStack.popCleanup(); scope.markEmitted(); - emitCleanup(*this, cleanupScope, cleanup, cleanupFlags, cleanupActiveFlag); + emitCleanup(*this, cleanupScope, cleanup.getCleanup(), cleanup.getFlags(), + cleanupActiveFlag); } void CIRGenFunction::emitCapturedCleanups(EHScopeStack::stable_iterator depth, @@ -831,12 +851,6 @@ void CIRGenFunction::emitCapturedCleanups(EHScopeStack::stable_iterator depth, assert(!scope.getCleanupScopeOp() && "captured cleanup should not own a cleanup scope"); - EHScopeStack::Cleanup::Flags cleanupFlags; - if (scope.isNormalCleanup()) - cleanupFlags.setIsNormalCleanupKind(); - if (scope.isEHCleanup()) - cleanupFlags.setIsEHCleanupKind(); - // A captured cleanup may be guarded by an active flag. The destructor // cleanups of a loop condition variable and of the temporaries that its // initializer lifetime-extends have one that is false while its @@ -845,29 +859,36 @@ void CIRGenFunction::emitCapturedCleanups(EHScopeStack::stable_iterator depth, // because its lifetime starts before initialization. For a loop, each // emission serves both the normal per-iteration exit and the EH unwind // path. - Address activeFlag = scope.getActiveFlag(); - - // Copy the cleanup emission data out before popping, since popCleanup - // deallocates the entry. This mirrors popCleanupBlock. - auto *cleanupSource = reinterpret_cast<char *>(scope.getCleanupBuffer()); - alignas(EHScopeStack::ScopeStackAlignment) char - cleanupBufferStack[8 * sizeof(void *)]; - std::unique_ptr<char[]> cleanupBufferHeap; - size_t cleanupSize = scope.getCleanupSize(); - EHScopeStack::Cleanup *cleanup; - if (cleanupSize <= sizeof(cleanupBufferStack)) { - memcpy(cleanupBufferStack, cleanupSource, cleanupSize); - cleanup = reinterpret_cast<EHScopeStack::Cleanup *>(cleanupBufferStack); - } else { - cleanupBufferHeap.reset(new char[cleanupSize]); - memcpy(cleanupBufferHeap.get(), cleanupSource, cleanupSize); - cleanup = - reinterpret_cast<EHScopeStack::Cleanup *>(cleanupBufferHeap.get()); - } + SavedCleanup cleanup(scope); + ehStack.popCleanup(); + emitCleanupBody(*this, cleanup.getCleanup(), cleanup.getFlags(), + cleanup.getActiveFlag(), loc); + } +} +void CIRGenFunction::emitCapturedCleanupsAsScopes( + EHScopeStack::stable_iterator depth, mlir::Location loc) { + // Copy the captured cleanups off the EH stack, the last captured one first. + llvm::SmallVector<SavedCleanup> captured; + while (ehStack.stable_begin() != depth) { + assert(isa<EHCleanupScope>(*ehStack.begin()) && "top not a cleanup!"); + EHCleanupScope &scope = cast<EHCleanupScope>(*ehStack.begin()); + assert(!scope.getCleanupScopeOp() && + "captured cleanup should not own a cleanup scope"); + assert(!scope.hasActiveFlag() && "captured cleanup with an active flag"); + captured.emplace_back(scope); ehStack.popCleanup(); - emitCleanupBody(*this, cleanup, cleanupFlags, activeFlag, loc); } + + // Push them again in the order in which they were captured, each one + // opening a cir.cleanup.scope, and emit them. + for (const SavedCleanup &cleanup : llvm::reverse(captured)) { + ehStack.pushCopyOfCleanup(cleanup.getKind(), cleanup.getData(), + cleanup.getSize()); + if (auto scope = cast<EHCleanupScope>(*ehStack.begin()).getCleanupScopeOp()) + scope->setLoc(loc); + } + popCleanupBlocks(depth); } /// Pops cleanup blocks until the given savepoint is reached. diff --git a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp index 6b0d031584dd3..2294d9ad0f522 100644 --- a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp +++ b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp @@ -415,6 +415,13 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) { assert(!cir::MissingFeatures::coroUnwindRegion()); CapturedCleanups frameCleanups(*this, /*active=*/true); + // Whether the promise or a parameter copy has a destructor. + bool frameHasDestructors = false; + auto emitFrameObject = [&](const VarDecl &var) { + emitAutoVarDeclWithCapturedCleanups(var, frameCleanups); + if (var.needsDestruction(getContext()) != QualType::DK_none) + frameHasDestructors = true; + }; // Create parameter copies. We do it before creating a promise, since an // evolution of coroutine TS may allow promise constructor to observe @@ -422,14 +429,13 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) { assert(!cir::MissingFeatures::coroOutsideFrameMD()); for (auto *pm : paramMoves) { const auto *paramMove = cast<DeclStmt>(pm); - emitAutoVarDeclWithCapturedCleanups( - cast<VarDecl>(*paramMove->getSingleDecl()), frameCleanups); + emitFrameObject(cast<VarDecl>(*paramMove->getSingleDecl())); paramReplacer.addCopy(paramMove); } // Builds `initial_suspend`. auto initialSuspendBuilder = [&]() -> mlir::LogicalResult { - emitAutoVarDeclWithCapturedCleanups(*s.getPromiseDecl(), frameCleanups); + emitFrameObject(*s.getPromiseDecl()); // 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()); @@ -531,11 +537,8 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) { return mlir::success(); }; - // Destroy the promise and the parameter copies, then emit - // "if (coro.free(CoroId, CoroBegin)) Deallocate;" - auto destroyBuilder = [&]() -> mlir::LogicalResult { - frameCleanups.emitIntoCleanupRegion(openCurlyLoc); - + // Emit "if (coro.free(CoroId, CoroBegin)) Deallocate;". + auto emitDeallocate = [&]() -> mlir::LogicalResult { Stmt *deallocate = s.getDeallocate(); if (emitStmt(deallocate, /*useCurrentScope=*/true).failed()) { cgm.error(deallocate->getBeginLoc(), @@ -574,9 +577,40 @@ CIRGenFunction::emitCoroutineBody(const CoroutineBodyStmt &s) { for (auto *op : opsToMove) op->moveBefore(yieldOp); } + return mlir::success(); + }; + + // Destroy the promise and the parameter copies, then free the frame. If an + // exception can leave the coroutine, emit them like classic CodeGen's + // cleanups: the deallocation is the cleanup of an outer cir.cleanup.scope, + // and each destructor has its own nested one, the first constructed object + // outermost. An exception from one of the destructors then still destroys + // the objects constructed before it and frees the frame. + auto destroyBuilder = [&]() -> mlir::LogicalResult { + if (!getLangOpts().Exceptions || inEHSpecTerminateScope() || + !frameHasDestructors) { + frameCleanups.emitIntoCleanupRegion(openCurlyLoc); + if (emitDeallocate().failed()) + return mlir::failure(); + cir::YieldOp::create(builder, openCurlyLoc); + return mlir::success(); + } + mlir::LogicalResult deallocRes = mlir::success(); + auto deallocScope = cir::CleanupScopeOp::create( + builder, openCurlyLoc, cir::CleanupKind::All, + /*bodyBuilder=*/[](mlir::OpBuilder &, mlir::Location) {}, + /*cleanupBuilder=*/ + [&](mlir::OpBuilder &b, mlir::Location loc) { + deallocRes = emitDeallocate(); + cir::YieldOp::create(b, loc); + }); + builder.setInsertionPointToEnd(&deallocScope.getBodyRegion().back()); + frameCleanups.emitAsNestedScopes(openCurlyLoc); cir::YieldOp::create(builder, openCurlyLoc); - return mlir::success(); + builder.setInsertionPointAfter(deallocScope); + cir::YieldOp::create(builder, openCurlyLoc); + return deallocRes; }; // Builds `exit`: coro.end(/*unwind*/ false) followed by the actual return diff --git a/clang/lib/CIR/CodeGen/CIRGenFunction.h b/clang/lib/CIR/CodeGen/CIRGenFunction.h index 2c49e3d5aa550..ce11c8bb885b9 100644 --- a/clang/lib/CIR/CodeGen/CIRGenFunction.h +++ b/clang/lib/CIR/CodeGen/CIRGenFunction.h @@ -1289,6 +1289,14 @@ class CIRGenFunction : public CIRGenTypeCache { void emitCapturedCleanups(EHScopeStack::stable_iterator depth, mlir::Location loc); + /// Like emitCapturedCleanups, but push the captured cleanups again as + /// ordinary cleanups, the first captured one outermost, and pop them, so that + /// each one is emitted as a cir.cleanup.scope around the ones captured after + /// it. An exception from one of them then still runs the ones captured + /// before it. None of them may have an active flag. + void emitCapturedCleanupsAsScopes(EHScopeStack::stable_iterator depth, + mlir::Location loc); + void terminateStructuredRegionBody(mlir::Region &r, mlir::Location loc); /// Deactivates the given cleanup block. The block cannot be reactivated. Pops @@ -1567,6 +1575,14 @@ class CIRGenFunction : public CIRGenTypeCache { cgf.emitCapturedCleanups(depth, loc); } + /// Emit the captured cleanups at the current insertion point as nested + /// cir.cleanup.scope operations, so that an exception from one of them + /// still runs the ones captured before it. + void emitAsNestedScopes(mlir::Location loc) { + if (active) + cgf.emitCapturedCleanupsAsScopes(depth, loc); + } + private: CapturedCleanups(const CapturedCleanups &) = delete; void operator=(const CapturedCleanups &) = delete; diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp index ede311d9c8f61..5d2ae16a56352 100644 --- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp +++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp @@ -3831,8 +3831,11 @@ 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 - // from a suspend_point being destroyed. + // from a suspend_point being destroyed. An exception that escapes body, for + // example one that unhandled_exception() rethrows, will leave through the + // unwind region once it is implemented. if (parent == &getBody()) { + assert(!cir::MissingFeatures::coroUnwindRegion()); regions.emplace_back(&getFinalSuspend()); regions.emplace_back(&getExit()); // any suspend_point inside body regions.emplace_back(&getDestroy()); // destroy() on any suspend inside body @@ -3853,9 +3856,6 @@ void cir::CoroutineOp::getSuccessorRegions( // 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; } // exit always terminates the op there's no region it loops back into. diff --git a/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp b/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp index 1e57ba6695b2d..6e09e7cfb89cf 100644 --- a/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp +++ b/clang/test/CIR/CodeGenCoroutines/coro-frame-dtors.cpp @@ -6,6 +6,8 @@ // 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 +// 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 +// RUN: FileCheck --input-file=%t-eh.ll %s -check-prefix=OGCG-EH // Ensure coroutine parameter copies, promises and the body are destroyed in the correct order. @@ -40,7 +42,10 @@ struct TrivialTask { 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. +// is constructed, and runs only in destroy. With exceptions, the destructors +// and the deallocation are nested cleanup scopes there, so that an exception +// from a destructor still destroys the rest of the coroutine state and frees +// the frame. // CIR-LABEL: cir.func coroutine {{.*}} @_Z12promise_dtorv( // CIR: %[[PROMISE:.*]] = cir.alloca "__promise" @@ -52,7 +57,16 @@ Task promise_dtor() { co_return; } // CIR: }, body : { // CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev // CIR: }, destroy : { -// CIR-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-EH-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { // CIR-NEXT: %[[MEM:.*]] = cir.coro.intrinsic.free // CIR-NOT: cir.call @_ZN4Task12promise_typeD1Ev // CIR: cir.call @_ZdlPvm(%[[MEM]], @@ -78,7 +92,9 @@ TrivialTask param_dtor(Dtor p) { // CIR: cir.coroutine initialSuspend : { // CIR-NOT: cir.call @_ZN4DtorD1Ev // CIR: }, destroy : { -// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH: } cleanup all { // CIR-NEXT: cir.coro.intrinsic.free // CIR-NOT: cir.call @_ZN4DtorD1Ev // CIR: }, exit : { @@ -125,9 +141,30 @@ Task promise_and_params(Dtor a, int i, Dtor b) { // 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-NOEH-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[B]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[A]]) +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-EH-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-EH-NEXT: cir.call @_ZN4DtorD1Ev(%[[B]]) +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-EH-NEXT: cir.call @_ZN4DtorD1Ev(%[[A]]) +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { // CIR-NEXT: cir.coro.intrinsic.free // CIR-NOT: cir.call {{@_ZN4DtorD1Ev|@_ZN4Task12promise_typeD1Ev}} // CIR: }, exit : { @@ -188,8 +225,11 @@ PreviewTask promise_preview(Dtor d) { co_return; } // 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-NOEH-NEXT: cir.call @_ZN11PreviewTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[D]]) +// CIR-EH: cir.call @_ZN11PreviewTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH: cir.call @_ZN4DtorD1Ev(%[[D]]) +// CIR-EH: } cleanup all { // CIR-NEXT: cir.coro.intrinsic.free // A temporary in a parameter copy's or the promise's initializer is still @@ -246,8 +286,11 @@ TmpTask init_temporaries(TmpMove m) { co_return; } // 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-NOEH-NEXT: cir.call @_ZN7TmpTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN7TmpMoveD1Ev(%[[M]]) +// CIR-EH: cir.call @_ZN7TmpTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH: cir.call @_ZN7TmpMoveD1Ev(%[[M]]) +// CIR-EH: } cleanup all { // CIR-NEXT: cir.coro.intrinsic.free // CIR-NOT: cir.call @_ZN3TmpD1Ev // CIR: }, exit : { @@ -361,7 +404,8 @@ TrivialTask cond_param_temporary(CondMove m) { co_return; } // CIR-NEXT: cir.call @_ZN7CondTmpD1Ev(%[[TMP]]) // CIR: cir.coroutine initialSuspend : { // CIR: }, destroy : { -// CIR-NEXT: cir.call @_ZN8CondMoveD1Ev(%[[M]]) +// CIR-NOEH-NEXT: cir.call @_ZN8CondMoveD1Ev(%[[M]]) +// CIR-EH: cir.call @_ZN8CondMoveD1Ev(%[[M]]) // CIR-NOT: cir.call @_ZN7CondTmpD1Ev // CIR: }, exit : { @@ -390,8 +434,11 @@ ValueTask no_final_await(Dtor p) { // CIR: }, finalSuspend : { // CIR-NEXT: cir.yield // CIR-NEXT: }, destroy : { -// CIR-NEXT: cir.call @_ZN9ValueTask12promise_typeD1Ev(%[[PROMISE]]) -// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-NOEH-NEXT: cir.call @_ZN9ValueTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH: cir.call @_ZN9ValueTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH: } cleanup all { // CIR-NEXT: cir.coro.intrinsic.free // A member coroutine copies its by-value parameters, but not *this. @@ -404,8 +451,11 @@ Task S::member(Dtor d) { co_return; } // 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-NOEH-NEXT: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[D]]) +// CIR-EH: cir.call @_ZN4Task12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH: cir.call @_ZN4DtorD1Ev(%[[D]]) +// CIR-EH: } cleanup all { // CIR-NEXT: cir.coro.intrinsic.free // An exception from get_return_object() propagates to the caller before the @@ -432,9 +482,69 @@ ThrowingGROTask eh_before_body(Dtor p) { co_return; } // CIR-NEXT: cir.call @_ZN15ThrowingGROTask12promise_type17get_return_objectEv( // CIR-NOT: cir.call @_ZN4DtorD1Ev // CIR: }, destroy : { -// CIR-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH: 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]]) + +// An exception from a destructor in destroy still destroys the objects +// constructed before it and frees the frame, like the cleanups of classic +// CodeGen. In a noexcept coroutine such an exception terminates, so the +// destructors and the deallocation stay a plain sequence, as without +// exceptions. +struct ThrowingDtorTask { + struct promise_type { + ~promise_type() noexcept(false); + ThrowingDtorTask get_return_object() noexcept; + std::suspend_always initial_suspend() noexcept; + std::suspend_always final_suspend() noexcept; + void return_void() noexcept; + void unhandled_exception() noexcept; + }; +}; + +ThrowingDtorTask destroy_throws(Dtor p) { co_return; } + +// CIR-LABEL: cir.func coroutine {{.*}} @_Z14destroy_throws4Dtor( +// CIR: %[[P:.*]] = cir.alloca "p" +// CIR: %[[PROMISE:.*]] = cir.alloca "__promise" +// CIR: }, destroy : { +// CIR-NOEH-NEXT: cir.call @_ZN16ThrowingDtorTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-NOEH-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.cleanup.scope { +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-EH-NEXT: cir.call @_ZN16ThrowingDtorTask12promise_typeD1Ev(%[[PROMISE]]) +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-EH-NEXT: cir.call @_ZN4DtorD1Ev(%[[P]]) +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } +// CIR-EH-NEXT: cir.yield +// CIR-EH-NEXT: } cleanup all { +// CIR-NEXT: %[[MEM:.*]] = cir.coro.intrinsic.free +// CIR: cir.call @_ZdlPvm(%[[MEM]], +// CIR: }, exit : { + +// OGCG-EH-LABEL: define {{.*}} @_Z14destroy_throws4Dtor( +// OGCG-EH: coro.cleanup: +// OGCG-EH-NEXT: invoke void @_ZN16ThrowingDtorTask12promise_typeD1Ev( +// OGCG-EH-NEXT: to label %{{.*}} unwind label %[[LPAD:.*]] +// OGCG-EH: [[LPAD]]: +// OGCG-EH: call void @_ZN4DtorD1Ev( +// OGCG-EH: call ptr @llvm.coro.free( + +ThrowingDtorTask destroy_throws_noexcept(Dtor p) noexcept { co_return; } + +// CIR-LABEL: cir.func coroutine {{.*}} @_Z23destroy_throws_noexcept4Dtor( +// CIR: }, destroy : { +// CIR-NEXT: cir.call @_ZN16ThrowingDtorTask12promise_typeD1Ev( +// CIR-NEXT: cir.call @_ZN4DtorD1Ev( +// CIR-NEXT: cir.coro.intrinsic.free diff --git a/clang/unittests/CIR/ControlFlowTest.cpp b/clang/unittests/CIR/ControlFlowTest.cpp index e3a6d46ffdbbb..a3e1d3e8de6cf 100644 --- a/clang/unittests/CIR/ControlFlowTest.cpp +++ b/clang/unittests/CIR/ControlFlowTest.cpp @@ -803,13 +803,12 @@ TEST_F(CIRControlFlowTest, CoroutineOp) { expectTerminatorSuccessors(coroOp.getFinalSuspend(), {&coroOp.getExit(), &coroOp.getDestroy()}); - // destroy: ordinary dispatch falls through to exit; + // destroy: always falls through to exit. RegionBranchTerminatorOpInterface destroyTerm = getTerminator(coroOp.getDestroy()); ASSERT_TRUE(destroyTerm); - expectSuccessors(coroOp, RegionBranchPoint(destroyTerm), - {&coroOp.getExit(), nullptr}); - expectTerminatorSuccessors(coroOp.getDestroy(), {&coroOp.getExit(), nullptr}); + expectSuccessors(coroOp, RegionBranchPoint(destroyTerm), {&coroOp.getExit()}); + expectTerminatorSuccessors(coroOp.getDestroy(), {&coroOp.getExit()}); // TODO: cir.return doesn't implement RegionBranchTerminatorOpInterface // exit always terminates the op. _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
