https://github.com/mmha created https://github.com/llvm/llvm-project/pull/229743

When a coroutine body flows off its end, CIRGen calls the promise's 
`return_void()` while the local variables of the body are still alive, and 
destroys them afterwards.

Flowing off the end of the function-body is equivalent to a `co_return` with no 
operand. Control only flows off the end of the function-body when it leaves the 
body's block, and leaving the block destroys the block's automatic variables.

This patch wraps the body in a `RunCleanupsScope`, so the body's cleanups are 
popped when the body ends, before the fall-through handler is emitted. The 
body's `cir.cleanup.scope` now ends with a `cir.yield`, and `return_void()` and 
`cir.co_return` follow it.


>From 0da89bd3e33b4b4e178c380ec5ab44d9903bac28 Mon Sep 17 00:00:00 2001
From: Morris Hafner <[email protected]>
Date: Wed, 7 Oct 2026 02:10:28 +0000
Subject: [PATCH] [CIR] Destroy coroutine body locals before the implicit
 co_return

When a coroutine body flows off its end, the fall-through handler, the
implicit `co_return;`, calls the promise's return_void().
emitBodyAndFallthrough emitted the body with useCurrentScope=true and
the fall-through handler right after it, in the same cleanup scope. The
cir.cleanup.scope of a local variable declared at the top level of the
body, or of a temporary that such a variable extends, therefore also
enclosed the return_void() call and the cir.co_return, so return_void()
ran before the local variables were destroyed. Classic CodeGen emits the
body as a statement of its own and the fall-through handler after it,
so the locals are destroyed first.

Pop the body's cleanups before emitting the fall-through handler, with a
RunCleanupsScope around the body. The body stays in the current lexical
scope, so a co_return at its top level is still recorded in currLexScope
and still suppresses the fall-through handler. With exceptions, the
return_void() call stays inside the body's cir.try, as in classic
CodeGen, so an exception from it goes to unhandled_exception(). An
explicit co_return is unchanged: it calls return_void() while the locals
are alive and leaves their scope through cir.co_return.

coro-fallthrough.cpp checks the order in CIR with and without
exceptions, for a local variable and for a lifetime-extended temporary,
and against classic CodeGen.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp     |  13 +-
 .../CodeGenCoroutines/coro-fallthrough.cpp    | 193 ++++++++++++++++++
 2 files changed, 204 insertions(+), 2 deletions(-)
 create mode 100644 clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp

diff --git a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp 
b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
index d92c65dc43c5f90..0e1bf9c9a532e5e 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
@@ -155,8 +155,17 @@ static mlir::LogicalResult
 emitBodyAndFallthrough(CIRGenFunction &cgf, const CoroutineBodyStmt &s,
                        Stmt *body,
                        const CIRGenFunction::LexicalScope *currLexScope) {
-  if (cgf.emitStmt(body, /*useCurrentScope=*/true).failed())
-    return mlir::failure();
+  {
+    // Destroy the body's local variables, and the
+    // temporaries they extend, when the body ends, before the fall-through
+    // handler, the implicit `co_return;`, calls return_void(). The body stays
+    // in the current lexical scope so that a co_return at its top level is
+    // recorded in currLexScope.
+    CIRGenFunction::RunCleanupsScope bodyScope(cgf);
+    if (cgf.emitStmt(body, /*useCurrentScope=*/true).failed())
+      return mlir::failure();
+  }
+
   // Note that classic codegen checks CanFallthrough by looking into the
   // availability of the insert block which is kinda brittle and unintuitive,
   // seems to be related with how landing pads are handled.
diff --git a/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp 
b/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp
new file mode 100644
index 000000000000000..ff3e966ac3324d8
--- /dev/null
+++ b/clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp
@@ -0,0 +1,193 @@
+// 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 -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
+// RUN: FileCheck --input-file=%t-eh.ll %s -check-prefixes=OGCG,OGCG-EH
+
+// When the body of a coroutine falls off its end, the implicit `co_return;`
+// runs after the body: the body's local variables are destroyed first, then
+// return_void() is called, then the coroutine goes to its final suspend point.
+// An explicit co_return calls return_void() while the local variables are
+// still alive and destroys them on its way to the final suspend point.
+
+#include "Inputs/coroutine.h"
+
+struct S {
+  ~S();
+};
+
+struct Task {
+  struct promise_type {
+    Task get_return_object() noexcept;
+    std::suspend_always initial_suspend() noexcept;
+    std::suspend_always final_suspend() noexcept;
+    // Not noexcept: with exceptions, the implicit co_return is still inside
+    // the body's try/catch, so an exception from return_void() goes to
+    // unhandled_exception().
+    void return_void();
+    void unhandled_exception();
+  };
+};
+
+S make();
+
+Task falls_off_end() {
+  co_await std::suspend_always{};
+  S s;
+}
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z13falls_off_endv(
+// CIR:         %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-NOEH:    %[[S:.*]] = cir.alloca "s"
+// CIR:         }, body : {
+// CIR-EH:        %[[S:.*]] = cir.alloca "s"
+// CIR-EH:        cir.try {
+// CIR:             cir.await(user,
+// CIR:             cir.cleanup.scope {
+// CIR-NEXT:          cir.yield
+// CIR-NOEH-NEXT:   } cleanup normal {
+// CIR-EH-NEXT:     } cleanup all {
+// CIR-NEXT:          cir.call @_ZN1SD1Ev(%[[S]])
+// CIR-NEXT:          cir.yield
+// CIR-NEXT:        }
+// CIR-NEXT:        cir.call 
@_ZN4Task12promise_type11return_voidEv(%[[PROMISE]])
+// CIR-NEXT:        cir.co_return
+// CIR-EH-NEXT:   } catch all
+// CIR:         }, finalSuspend : {
+
+// OGCG-LABEL: define {{.*}} void @_Z13falls_off_endv(
+// OGCG:         call void @_ZN1SD1Ev(ptr {{.*}} %[[S:.*]])
+// OGCG-NEXT:    call void @llvm.lifetime.end.p0(ptr %[[S]])
+// OGCG-NOEH-NEXT: call void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-NOEH-NEXT: br label %coro.final
+// OGCG-EH-NEXT:   invoke void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-EH-NEXT:     to label %[[CONT:.*]] unwind label %[[LPAD:.*]]
+// OGCG-EH:      [[CONT]]:
+// OGCG-EH-NEXT:   br label %coro.final
+// OGCG-EH:      [[LPAD]]:
+// OGCG-EH-NEXT:   landingpad
+// OGCG-EH-NEXT:     catch ptr null
+
+// A temporary that a local variable at the top level of the body extends is
+// destroyed before the implicit co_return as well.
+
+Task falls_off_end_with_extended_temporary() {
+  co_await std::suspend_always{};
+  const S &r = make();
+}
+
+// CIR-LABEL: cir.func coroutine {{.*}} 
@_Z37falls_off_end_with_extended_temporaryv(
+// CIR:         %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR:         }, body : {
+// CIR:             cir.await(user,
+// CIR:             cir.call @_Z4makev(%[[TMP:[0-9]+]])
+// CIR-NEXT:        cir.cleanup.scope {
+// CIR-NEXT:          cir.store {{.*}} %[[TMP]], %{{.+}} :
+// CIR-NEXT:          cir.yield
+// CIR-NOEH-NEXT:   } cleanup normal {
+// CIR-EH-NEXT:     } cleanup all {
+// CIR-NEXT:          cir.call @_ZN1SD1Ev(%[[TMP]])
+// CIR-NEXT:          cir.yield
+// CIR-NEXT:        }
+// CIR-NEXT:        cir.call 
@_ZN4Task12promise_type11return_voidEv(%[[PROMISE]])
+// CIR-NEXT:        cir.co_return
+// CIR-EH-NEXT:   } catch all
+// CIR:         }, finalSuspend : {
+
+// OGCG-LABEL: define {{.*}} void @_Z37falls_off_end_with_extended_temporaryv(
+// OGCG:         {{call|invoke}} void @_Z4makev(ptr {{.*}} %[[TMP:[a-z.0-9]+]])
+// OGCG:         call void @_ZN1SD1Ev(ptr {{.*}} %[[TMP]])
+// OGCG-NOT:     return_void
+// OGCG:         call void @llvm.lifetime.end.p0(ptr %[[TMP]])
+// OGCG-NEXT:    call void @llvm.lifetime.end.p0(
+// OGCG-NOEH-NEXT: call void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-EH-NEXT:   invoke void @_ZN4Task12promise_type11return_voidEv(
+
+// 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.
+
+Task co_return_in_if(bool b) {
+  S s;
+  if (b)
+    co_return;
+}
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z15co_return_in_ifb(
+// CIR:         %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-NOEH:    %[[S:.*]] = cir.alloca "s"
+// CIR:         }, body : {
+// CIR-EH:        %[[S:.*]] = cir.alloca "s"
+// CIR-EH:        cir.try {
+// CIR:             cir.cleanup.scope {
+// CIR-NEXT:          cir.scope {
+// CIR-NEXT:            %[[B:.*]] = cir.load
+// CIR-NEXT:            cir.if %[[B]] {
+// CIR-NEXT:              cir.call 
@_ZN4Task12promise_type11return_voidEv(%[[PROMISE]])
+// CIR-NEXT:              cir.co_return
+// CIR-NEXT:            }
+// CIR-NEXT:          }
+// CIR-NEXT:          cir.yield
+// CIR-NOEH-NEXT:   } cleanup normal {
+// CIR-EH-NEXT:     } cleanup all {
+// CIR-NEXT:          cir.call @_ZN1SD1Ev(%[[S]])
+// CIR-NEXT:          cir.yield
+// CIR-NEXT:        }
+// CIR-NEXT:        cir.call 
@_ZN4Task12promise_type11return_voidEv(%[[PROMISE]])
+// CIR-NEXT:        cir.co_return
+// CIR-EH-NEXT:   } catch all
+// CIR:         }, finalSuspend : {
+
+// OGCG-LABEL: define {{.*}} void @_Z15co_return_in_ifb(
+// OGCG:       if.then:
+// OGCG-NOEH-NEXT: call void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-EH-NEXT:   invoke void @_ZN4Task12promise_type11return_voidEv(
+// OGCG:       cleanup{{[0-9]*}}:
+// OGCG-NEXT:    %[[DEST:.*]] = phi i32
+// OGCG-NEXT:    call void @_ZN1SD1Ev(
+// OGCG-NEXT:    call void @llvm.lifetime.end.p0(
+// OGCG-NEXT:    switch i32 %[[DEST]], label %unreachable [
+// OGCG-NEXT:      i32 {{[0-9]+}}, label %[[FALLTHROUGH:.*]]
+// OGCG-NEXT:      i32 {{[0-9]+}}, label %coro.final
+// OGCG-NEXT:    ]
+// OGCG:       [[FALLTHROUGH]]:
+// OGCG-NOEH-NEXT: call void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-NOEH-NEXT: br label %coro.final
+// OGCG-EH-NEXT:   invoke void @_ZN4Task12promise_type11return_voidEv(
+
+Task co_return_at_end() {
+  co_await std::suspend_always{};
+  S s;
+  co_return;
+}
+
+// CIR-LABEL: cir.func coroutine {{.*}} @_Z16co_return_at_endv(
+// CIR:         %[[PROMISE:.*]] = cir.alloca "__promise"
+// CIR-NOEH:    %[[S:.*]] = cir.alloca "s"
+// CIR:         }, body : {
+// CIR-EH:        %[[S:.*]] = cir.alloca "s"
+// CIR-EH:        cir.try {
+// CIR:             cir.await(user,
+// CIR:             cir.cleanup.scope {
+// CIR-NEXT:          cir.call 
@_ZN4Task12promise_type11return_voidEv(%[[PROMISE]])
+// CIR-NEXT:          cir.co_return
+// CIR-NOEH-NEXT:   } cleanup normal {
+// CIR-EH-NEXT:     } cleanup all {
+// CIR-NEXT:          cir.call @_ZN1SD1Ev(%[[S]])
+// CIR-NEXT:          cir.yield
+// CIR-NEXT:        }
+// CIR-NOT:         cir.call @_ZN4Task12promise_type11return_voidEv
+// CIR:         }, finalSuspend : {
+
+// OGCG-LABEL: define {{.*}} void @_Z16co_return_at_endv(
+// OGCG-NOEH:    call void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-NOEH-NEXT: call void @_ZN1SD1Ev(
+// OGCG-EH:      invoke void @_ZN4Task12promise_type11return_voidEv(
+// OGCG-EH-NEXT:   to label %[[CONT:.*]] unwind label
+// OGCG-EH:      [[CONT]]:
+// OGCG-EH-NEXT:   call void @_ZN1SD1Ev(
+// OGCG-NOT:     return_void
+// OGCG:         br label %coro.final

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to