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
