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

Reply via email to