[clang] [CIR] Destroy coroutine body locals before the implicit co_return (PR #229743)
Morris Hafner via cfe-commits
cfe-commits at lists.llvm.org
Wed Oct 7 05:33:43 PDT 2026
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 <mhafner at nvidia.com>
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 <noreply at anthropic.com>
---
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
More information about the cfe-commits
mailing list