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

Reply via email to