[clang] 92ab900 - [CIR] Destroy coroutine body locals before the implicit co_return (#229743)
via cfe-commits
cfe-commits at lists.llvm.org
Thu Oct 8 15:01:44 PDT 2026
Author: Morris Hafner
Date: 2026-10-08T22:01:38Z
New Revision: 92ab900192cad2fc8c99766c765ce0ae70edbdc0
URL: https://github.com/llvm/llvm-project/commit/92ab900192cad2fc8c99766c765ce0ae70edbdc0
DIFF: https://github.com/llvm/llvm-project/commit/92ab900192cad2fc8c99766c765ce0ae70edbdc0.diff
LOG: [CIR] Destroy coroutine body locals before the implicit co_return (#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.
---------
Co-authored-by: Claude Opus 5.5 <noreply at anthropic.com>
Added:
clang/test/CIR/CodeGenCoroutines/coro-fallthrough.cpp
Modified:
clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
Removed:
################################################################################
diff --git a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
index 413150dacde0e..ad34df762fe19 100644
--- a/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
+++ b/clang/lib/CIR/CodeGen/CIRGenCoroutine.cpp
@@ -155,8 +155,16 @@ 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 0000000000000..ff3e966ac3324
--- /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