[clang] [clang][CodeGenCoroutine] Emit missing cleanup scope for lazy GRO conversion (PR #194281)

Weibo He via cfe-commits cfe-commits at lists.llvm.org
Mon Apr 27 02:37:26 PDT 2026


https://github.com/NewSigma updated https://github.com/llvm/llvm-project/pull/194281

>From 9784315537fc3431ff9cf77b860cef2e8d31a901 Mon Sep 17 00:00:00 2001
From: NewSigma <NewSigma at 163.com>
Date: Thu, 23 Apr 2026 21:06:43 +0800
Subject: [PATCH 1/2] [clang][CodeGenCoroutine] Emit missing cleanup scope for
 lazy GRO conversion

---
 clang/lib/CodeGen/CGCoroutine.cpp             | 19 +++++---
 .../coro-await-resume-eh.cpp                  |  3 +-
 .../test/CodeGenCoroutines/coro-dest-slot.cpp |  8 +---
 clang/test/CodeGenCoroutines/coro-gro.cpp     | 13 +++---
 clang/test/CodeGenCoroutines/coro-gro4.cpp    | 46 +++++++++++++++++++
 .../coro-suspend-cleanups.cpp                 |  2 +-
 6 files changed, 69 insertions(+), 22 deletions(-)
 create mode 100644 clang/test/CodeGenCoroutines/coro-gro4.cpp

diff --git a/clang/lib/CodeGen/CGCoroutine.cpp b/clang/lib/CodeGen/CGCoroutine.cpp
index 9d9d2450c3d68..14d297a646d45 100644
--- a/clang/lib/CodeGen/CGCoroutine.cpp
+++ b/clang/lib/CodeGen/CGCoroutine.cpp
@@ -713,14 +713,14 @@ struct GetReturnObjectManager {
     Builder.CreateStore(Builder.getFalse(), GroActiveFlag);
   }
 
-  void EmitGroAlloca() {
+  std::unique_ptr<CodeGenFunction::RunCleanupsScope> EmitGroAlloca() {
     if (DirectEmit)
-      return;
+      return nullptr;
 
     auto *GroDeclStmt = dyn_cast_or_null<DeclStmt>(S.getResultDecl());
     if (!GroDeclStmt) {
       // If get_return_object returns void, no need to do an alloca.
-      return;
+      return nullptr;
     }
 
     auto *GroVarDecl = cast<VarDecl>(GroDeclStmt->getSingleDecl());
@@ -736,6 +736,7 @@ struct GetReturnObjectManager {
                              llvm::MDNode::get(CGF.CGM.getLLVMContext(), {}));
     }
 
+    auto GroScope = std::make_unique<CodeGenFunction::RunCleanupsScope>(CGF);
     // Remember the top of EHStack before emitting the cleanup.
     auto old_top = CGF.EHStack.stable_begin();
     CGF.EmitAutoVarCleanups(GroEmission);
@@ -751,6 +752,7 @@ struct GetReturnObjectManager {
         Cleanup->setTestFlagInNormalCleanup();
       }
     }
+    return GroScope;
   }
 
   void EmitGroInit() {
@@ -878,6 +880,7 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
   auto *AllocBB = createBasicBlock("coro.alloc");
   auto *InitBB = createBasicBlock("coro.init");
   auto *FinalBB = createBasicBlock("coro.final");
+  auto *CleanupBB = createBasicBlock("coro.cleanup");
   auto *RetBB = createBasicBlock("coro.ret");
 
   auto *CoroId = Builder.CreateCall(
@@ -930,8 +933,6 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
   auto *CoroBegin = Builder.CreateCall(
       CGM.getIntrinsic(llvm::Intrinsic::coro_begin), {CoroId, Phi});
   CurCoro.Data->CoroBegin = CoroBegin;
-
-  CurCoro.Data->CleanupJD = getJumpDestInCurrentScope(RetBB);
   {
     CGDebugInfo *DI = getDebugInfo();
     ParamReferenceReplacerRAII ParamReplacer(LocalDeclMap);
@@ -982,11 +983,12 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
     CoroId->setArgOperand(1, PromiseAddr.emitRawPointer(*this));
 
     // Now we have the promise, initialize the GRO
-    GroManager.EmitGroAlloca();
+    auto GroScope = GroManager.EmitGroAlloca();
     GroManager.EmitGroInit();
 
     EHStack.pushCleanup<CallCoroEnd>(EHCleanup);
 
+    CurCoro.Data->CleanupJD = getJumpDestInCurrentScope(CleanupBB);
     CurCoro.Data->CurrentAwaitKind = AwaitKind::Init;
     CurCoro.Data->ExceptionHandler = S.getExceptionHandler();
     EmitStmt(S.getInitSuspendStmt());
@@ -1041,8 +1043,11 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
 
     // We need conversion if get_return_object's type doesn't matches the
     // coroutine return type.
-    if (!GroManager.DirectEmit)
+    if (!GroManager.DirectEmit) {
       GroManager.EmitGroConv(RetBB);
+      GroScope->ForceCleanup();
+    }
+    EmitBlock(CleanupBB);
   }
 
   EmitBlock(RetBB);
diff --git a/clang/test/CodeGenCoroutines/coro-await-resume-eh.cpp b/clang/test/CodeGenCoroutines/coro-await-resume-eh.cpp
index 8253337ba7390..da1ce8a5f174b 100644
--- a/clang/test/CodeGenCoroutines/coro-await-resume-eh.cpp
+++ b/clang/test/CodeGenCoroutines/coro-await-resume-eh.cpp
@@ -58,8 +58,9 @@ throwing_task f() {
   // CHECK: [[RESUMETRYCONT]]:
   // CHECK-NEXT: br label %[[CLEANUP:.+]]
   // CHECK: [[CLEANUP]]:
-  // CHECK: switch i32 %{{.+}}, label %{{.+}} [
+  // CHECK: switch i32 %{{.+}}, label %unreachable [
   // CHECK-NEXT: i32 0, label %[[CLEANUPCONT:.+]]
+  // CHECK-NEXT: i32 2, label %{{.+}}
   // CHECK-NEXT: ]
 
   // The variable RESUMETHREW is loaded and if true, then 'await_resume'
diff --git a/clang/test/CodeGenCoroutines/coro-dest-slot.cpp b/clang/test/CodeGenCoroutines/coro-dest-slot.cpp
index d794c74cd52d6..2b78d4e71a97d 100644
--- a/clang/test/CodeGenCoroutines/coro-dest-slot.cpp
+++ b/clang/test/CodeGenCoroutines/coro-dest-slot.cpp
@@ -32,9 +32,5 @@ extern "C" coro f(int) { co_return; }
 
 // CHECK: call void @_ZNSt13suspend_never12await_resumeEv(
 // CHECK: %[[CLEANUP_DEST1:.+]] = phi i32 [ 0, %[[FINAL_READY]] ], [ 2, %[[FINAL_CLEANUP]] ]
-// CHECK: %[[CLEANUP_DEST2:.+]] = phi i32 [ %[[CLEANUP_DEST0]], %{{.+}} ], [ %[[CLEANUP_DEST1]], %{{.+}} ], [ 0, %{{.+}} ]
-// CHECK: call ptr @llvm.coro.free(
-// CHECK: switch i32 %[[CLEANUP_DEST2]], label %{{.+}} [
-// CHECK-NEXT: i32 0
-// CHECK-NEXT: i32 2
-// CHECK-NEXT: ]
+
+// CHECK-NOT: phi i32
diff --git a/clang/test/CodeGenCoroutines/coro-gro.cpp b/clang/test/CodeGenCoroutines/coro-gro.cpp
index 3ba6f349fdde1..02f20c1a64fda 100644
--- a/clang/test/CodeGenCoroutines/coro-gro.cpp
+++ b/clang/test/CodeGenCoroutines/coro-gro.cpp
@@ -66,23 +66,22 @@ int f() {
   // CHECK-NEXT: br label %[[AfterGroConv]]
 
   // CHECK: [[AfterGroConv]]:
-  // CHECK-NEXT: br i1  %[[IsFinalExit]], label %cleanup.cont10, label %[[CoroRet:.+]]
-
-  // CHECK: cleanup.cont10:
-  // CHECK-NEXT: br label %[[Cleanup:.+]]
+  // CHECK-NEXT: br i1  %[[IsFinalExit]], label %[[Cleanup:.+]], label %[[CoroRet:.+]]
 
   // CHECK: [[Cleanup]]:
-  // CHECK-NEXT: %{{.*}} = phi i32
   // CHECK-NEXT: %[[IsActive:.+]] = load i1, ptr %[[GroActive]]
   // CHECK-NEXT: br i1 %[[IsActive]], label %[[CleanupGro:.+]], label %[[Done:.+]]
 
   // CHECK: [[CleanupGro]]:
   // CHECK-NEXT: call void @_ZN7GroTypeD1Ev(
-  // CHECK-NEXT: br label %[[Done]]
+  // CHECK-NEXT: br label %cleanup.done
+
+  // CHECK: cleanup.done:
+  // CHECK-NEXT: br label %coro.cleanup
 
   // Destroy promise and free the memory.
 
-  // CHECK: [[Done]]:
+  // CHECK: coro.cleanup:
   // CHECK-NEXT: call void @llvm.lifetime.end.p0(ptr %[[CoroGro]])
   // CHECK-NEXT: call void @_ZNSt16coroutine_traitsIiJEE12promise_typeD1Ev(
   // CHECK-NEXT: call void @llvm.lifetime.end.p0(ptr %[[Promise]])
diff --git a/clang/test/CodeGenCoroutines/coro-gro4.cpp b/clang/test/CodeGenCoroutines/coro-gro4.cpp
new file mode 100644
index 0000000000000..e4c0b980e019d
--- /dev/null
+++ b/clang/test/CodeGenCoroutines/coro-gro4.cpp
@@ -0,0 +1,46 @@
+// Test that the GRO destructor does not enter the resume or destroy parts
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++20 -O2 -disable-llvm-passes -emit-llvm %s -o - | opt -passes='default<O0>,default<O2>' -S | FileCheck %s
+// RUN: %clang_cc1 -triple x86_64-unknown-linux-gnu -std=c++20 -O2 -emit-llvm %s -o - | FileCheck %s
+
+#include "Inputs/coroutine.h"
+
+extern "C" void gro_destroy() noexcept;
+
+struct task {
+  struct promise_type {
+    task get_return_object() noexcept { return task{std::coroutine_handle<promise_type>::from_promise(*this)}; }
+    std::suspend_always initial_suspend() noexcept { return {}; }
+    std::suspend_always final_suspend() noexcept { return {}; }
+    void return_void() noexcept {}
+    void unhandled_exception() {}
+  };
+
+  task(std::coroutine_handle<promise_type> handle) : m_coro(handle) {}
+  task(task &&o) noexcept : m_coro(o.m_coro) { o.m_coro = nullptr; }
+  task(const task&) = delete;
+  task& operator=(const task&) = delete;
+  task& operator=(task&&) = delete;
+
+  ~task() {
+    gro_destroy();
+    if (m_coro)
+      m_coro.destroy();
+  }
+
+  std::coroutine_handle<promise_type> m_coro;
+};
+
+struct wrapper {
+  using promise_type = task::promise_type;
+
+  wrapper(task &&t) noexcept : m_task(static_cast<task&&>(t)) {}
+
+  task m_task;
+};
+
+wrapper fn() { co_return; }
+
+// CHECK: define internal fastcc void @_Z2fnv.resume
+// CHECK-NOT: call void @gro_destroy()
+// CHECK: define internal fastcc void @_Z2fnv.destroy
+// CHECK-NOT: call void @gro_destroy()
diff --git a/clang/test/CodeGenCoroutines/coro-suspend-cleanups.cpp b/clang/test/CodeGenCoroutines/coro-suspend-cleanups.cpp
index 5872a75b26de5..6c05d02d0642f 100644
--- a/clang/test/CodeGenCoroutines/coro-suspend-cleanups.cpp
+++ b/clang/test/CodeGenCoroutines/coro-suspend-cleanups.cpp
@@ -50,7 +50,7 @@ coroutine ArrayInitCoro() {
     // CHECK-NEXT:  store ptr %arrayinit.element, ptr %arrayinit.endOfInit.reload.addr, align 8
     co_await Awaiter{}
     // CHECK-NEXT:  @_ZNSt14suspend_always11await_readyEv
-    // CHECK-NEXT:  br i1 %{{.+}}, label %await.ready, label %CoroSave30
+    // CHECK-NEXT:  br i1 %{{.+}}, label %await.ready, label %{{CoroSave[0-9]+}}
   };
   // CHECK:       await.cleanup:                                    ; preds = %AfterCoroSuspend{{.*}}
   // CHECK-NEXT:    br label %cleanup{{.*}}.from.await.cleanup

>From f9d68460a6949f228255c8a8adac870aff624905 Mon Sep 17 00:00:00 2001
From: NewSigma <NewSigma at 163.com>
Date: Mon, 27 Apr 2026 17:29:25 +0800
Subject: [PATCH 2/2] Add positive test

---
 clang/lib/CodeGen/CGCoroutine.cpp          | 17 ++++++++---------
 clang/test/CodeGenCoroutines/coro-gro.cpp  | 13 +++++--------
 clang/test/CodeGenCoroutines/coro-gro4.cpp |  5 +++++
 3 files changed, 18 insertions(+), 17 deletions(-)

diff --git a/clang/lib/CodeGen/CGCoroutine.cpp b/clang/lib/CodeGen/CGCoroutine.cpp
index 14d297a646d45..2972f015aa893 100644
--- a/clang/lib/CodeGen/CGCoroutine.cpp
+++ b/clang/lib/CodeGen/CGCoroutine.cpp
@@ -661,6 +661,7 @@ struct GetReturnObjectManager {
 
   Address GroActiveFlag;
   CodeGenFunction::AutoVarEmission GroEmission;
+  std::unique_ptr<CodeGenFunction::RunCleanupsScope> GroScope;
 
   GetReturnObjectManager(CodeGenFunction &CGF, const CoroutineBodyStmt &S)
       : CGF(CGF), Builder(CGF.Builder), S(S), GroActiveFlag(Address::invalid()),
@@ -713,14 +714,14 @@ struct GetReturnObjectManager {
     Builder.CreateStore(Builder.getFalse(), GroActiveFlag);
   }
 
-  std::unique_ptr<CodeGenFunction::RunCleanupsScope> EmitGroAlloca() {
+  void EmitGroAlloca() {
     if (DirectEmit)
-      return nullptr;
+      return;
 
     auto *GroDeclStmt = dyn_cast_or_null<DeclStmt>(S.getResultDecl());
     if (!GroDeclStmt) {
       // If get_return_object returns void, no need to do an alloca.
-      return nullptr;
+      return;
     }
 
     auto *GroVarDecl = cast<VarDecl>(GroDeclStmt->getSingleDecl());
@@ -736,7 +737,7 @@ struct GetReturnObjectManager {
                              llvm::MDNode::get(CGF.CGM.getLLVMContext(), {}));
     }
 
-    auto GroScope = std::make_unique<CodeGenFunction::RunCleanupsScope>(CGF);
+    GroScope = std::make_unique<CodeGenFunction::RunCleanupsScope>(CGF);
     // Remember the top of EHStack before emitting the cleanup.
     auto old_top = CGF.EHStack.stable_begin();
     CGF.EmitAutoVarCleanups(GroEmission);
@@ -752,7 +753,6 @@ struct GetReturnObjectManager {
         Cleanup->setTestFlagInNormalCleanup();
       }
     }
-    return GroScope;
   }
 
   void EmitGroInit() {
@@ -850,6 +850,7 @@ struct GetReturnObjectManager {
     CGF.EmitAnyExprToMem(S.getReturnValue(), CGF.ReturnValue,
                          S.getReturnValue()->getType().getQualifiers(),
                          /*IsInit*/ true);
+    GroScope->ForceCleanup();
     Builder.CreateBr(AfterConvBB);
 
     CGF.EmitBlock(AfterConvBB);
@@ -983,7 +984,7 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
     CoroId->setArgOperand(1, PromiseAddr.emitRawPointer(*this));
 
     // Now we have the promise, initialize the GRO
-    auto GroScope = GroManager.EmitGroAlloca();
+    GroManager.EmitGroAlloca();
     GroManager.EmitGroInit();
 
     EHStack.pushCleanup<CallCoroEnd>(EHCleanup);
@@ -1043,10 +1044,8 @@ void CodeGenFunction::EmitCoroutineBody(const CoroutineBodyStmt &S) {
 
     // We need conversion if get_return_object's type doesn't matches the
     // coroutine return type.
-    if (!GroManager.DirectEmit) {
+    if (!GroManager.DirectEmit)
       GroManager.EmitGroConv(RetBB);
-      GroScope->ForceCleanup();
-    }
     EmitBlock(CleanupBB);
   }
 
diff --git a/clang/test/CodeGenCoroutines/coro-gro.cpp b/clang/test/CodeGenCoroutines/coro-gro.cpp
index 02f20c1a64fda..99b0afcedccf6 100644
--- a/clang/test/CodeGenCoroutines/coro-gro.cpp
+++ b/clang/test/CodeGenCoroutines/coro-gro.cpp
@@ -63,20 +63,17 @@ int f() {
   // CHECK: [[GroConv]]:
   // CHECK-NEXT: %[[Conv:.+]] = call noundef i32 @_ZN7GroTypecviEv(
   // CHECK-NEXT: store i32 %[[Conv]], ptr %[[RetVal]]
-  // CHECK-NEXT: br label %[[AfterGroConv]]
-
-  // CHECK: [[AfterGroConv]]:
-  // CHECK-NEXT: br i1  %[[IsFinalExit]], label %[[Cleanup:.+]], label %[[CoroRet:.+]]
-
-  // CHECK: [[Cleanup]]:
   // CHECK-NEXT: %[[IsActive:.+]] = load i1, ptr %[[GroActive]]
-  // CHECK-NEXT: br i1 %[[IsActive]], label %[[CleanupGro:.+]], label %[[Done:.+]]
+  // CHECK-NEXT: br i1 %[[IsActive]], label %[[CleanupGro:.+]], label %{{.*}}
 
   // CHECK: [[CleanupGro]]:
   // CHECK-NEXT: call void @_ZN7GroTypeD1Ev(
   // CHECK-NEXT: br label %cleanup.done
 
-  // CHECK: cleanup.done:
+  // CHECK: after.gro.conv:
+  // CHECK-NEXT: br i1  %[[IsFinalExit]], label %cleanup.cont10, label %[[CoroRet:.+]]
+
+  // CHECK: cleanup.cont10:
   // CHECK-NEXT: br label %coro.cleanup
 
   // Destroy promise and free the memory.
diff --git a/clang/test/CodeGenCoroutines/coro-gro4.cpp b/clang/test/CodeGenCoroutines/coro-gro4.cpp
index e4c0b980e019d..4ee15bb688981 100644
--- a/clang/test/CodeGenCoroutines/coro-gro4.cpp
+++ b/clang/test/CodeGenCoroutines/coro-gro4.cpp
@@ -40,7 +40,12 @@ struct wrapper {
 
 wrapper fn() { co_return; }
 
+// CHECK: define dso_local void @_Z2fnv
+// CHECK: call void @gro_destroy()
+// CHECK: ret void
+
 // CHECK: define internal fastcc void @_Z2fnv.resume
 // CHECK-NOT: call void @gro_destroy()
+
 // CHECK: define internal fastcc void @_Z2fnv.destroy
 // CHECK-NOT: call void @gro_destroy()



More information about the cfe-commits mailing list