[llvm] [SCCP] Do not track returns from pre-split coroutines (PR #227232)
Karim Alweheshy via llvm-commits
llvm-commits at lists.llvm.org
Tue Sep 29 02:11:10 PDT 2026
https://github.com/karim-alweheshy created https://github.com/llvm/llvm-project/pull/227232
## Summary
Before coroutine splitting, a returned-continuation coroutine can end in `llvm.coro.end` and `unreachable` rather than an ordinary `ret`. IPSCCP currently tracks its return value anyway. When it sees no `ret`, it can replace a direct call's returned continuation/yield-pointer pair with `undef`; coroutine splitting later creates a concrete return, but the caller has already lost the value.
Exclude `presplitcoroutine` functions from interprocedural return tracking. This also keeps the related called-value propagation analysis from making the same assumption. Argument tracking and other IPO optimizations remain available.
## Test
The new IR regression test models a returned-continuation coroutine and a direct/indirect call join. It fails on unpatched LLVM because the direct incoming pair becomes `undef`, and passes with this change. It also verifies that coroutine splitting creates a concrete pair and that return propagation for an ordinary function is unchanged.
Validated with a locally built `opt` and both `RUN` pipelines. Broader integration testing is in progress.
>From b1888bd922d28c32e33a8d6aabb15c62a10310cb Mon Sep 17 00:00:00 2001
From: Karim Alweheshy <karim.alweheshy at reddit.com>
Date: Tue, 29 Sep 2026 11:06:43 +0200
Subject: [PATCH] [SCCP] Do not track returns from pre-split coroutines
---
llvm/lib/Analysis/ValueLatticeUtils.cpp | 8 +-
.../SCCP/ipsccp-presplit-coroutine.ll | 82 +++++++++++++++++++
2 files changed, 89 insertions(+), 1 deletion(-)
create mode 100644 llvm/test/Transforms/SCCP/ipsccp-presplit-coroutine.ll
diff --git a/llvm/lib/Analysis/ValueLatticeUtils.cpp b/llvm/lib/Analysis/ValueLatticeUtils.cpp
index acc19667d7764..ee24c987f6ff7 100644
--- a/llvm/lib/Analysis/ValueLatticeUtils.cpp
+++ b/llvm/lib/Analysis/ValueLatticeUtils.cpp
@@ -22,7 +22,13 @@ bool llvm::canTrackArgumentsInterprocedurally(Function *F) {
}
bool llvm::canTrackReturnsInterprocedurally(Function *F) {
- return F->hasExactDefinition() && !F->hasFnAttribute(Attribute::Naked);
+ // Before coroutine splitting, the returns from a coroutine may not be
+ // represented by ret instructions. In particular, returned-continuation
+ // coroutines end in coro.end followed by unreachable, but splitting later
+ // creates a concrete return value. Do not infer undef from the absence of
+ // a ret in the pre-split body.
+ return F->hasExactDefinition() && !F->hasFnAttribute(Attribute::Naked) &&
+ !F->hasFnAttribute(Attribute::PresplitCoroutine);
}
bool llvm::canTrackGlobalVariableInterprocedurally(GlobalVariable *GV) {
diff --git a/llvm/test/Transforms/SCCP/ipsccp-presplit-coroutine.ll b/llvm/test/Transforms/SCCP/ipsccp-presplit-coroutine.ll
new file mode 100644
index 0000000000000..8836cd5493f9c
--- /dev/null
+++ b/llvm/test/Transforms/SCCP/ipsccp-presplit-coroutine.ll
@@ -0,0 +1,82 @@
+; RUN: opt -passes=ipsccp -S %s | FileCheck %s
+; RUN: opt -passes='coro-early,ipsccp,cgscc(coro-split),verify' -S %s | FileCheck %s --check-prefix=SPLIT
+
+; A returned-continuation coroutine has no ret before splitting. Its
+; coro.end is replaced with a real pair of continuation and yield pointers
+; during splitting. IPSCCP must not replace the direct call's result with
+; undef just because the pre-split body ends in unreachable.
+
+declare token @llvm.coro.id.retcon.once(i32, i32, ptr, ptr, ptr, ptr)
+declare ptr @llvm.coro.begin(token, ptr)
+declare i1 @llvm.coro.suspend.retcon.i1(...)
+declare void @llvm.coro.end(ptr, i1, token)
+declare ptr @llvm.coro.prepare.retcon(ptr)
+declare ptr @malloc(i64)
+declare void @free(ptr)
+declare void @resume(ptr, i1)
+declare void @consume(ptr)
+
+define internal swiftcc { ptr, ptr } @accessor(ptr noalias %buffer,
+ ptr swiftself %object) #0 {
+entry:
+ %id = call token @llvm.coro.id.retcon.once(
+ i32 32, i32 8, ptr %buffer, ptr @resume, ptr @malloc, ptr @free)
+ %frame = call ptr @llvm.coro.begin(token %id, ptr null)
+ %field = getelementptr i8, ptr %object, i64 8
+ %suspended = call i1 (...) @llvm.coro.suspend.retcon.i1(ptr %field)
+ call void @llvm.coro.end(ptr %frame, i1 false, token none)
+ unreachable
+}
+
+define void @caller(ptr %target, ptr %buffer, ptr %object) {
+entry:
+ %prepared = call ptr @llvm.coro.prepare.retcon(ptr %target)
+ %is_direct = icmp eq ptr %prepared, @accessor
+ br i1 %is_direct, label %direct, label %indirect
+
+direct:
+ %direct_pair = call swiftcc { ptr, ptr } @accessor(
+ ptr noalias %buffer, ptr swiftself %object)
+ br label %join
+
+indirect:
+ %indirect_pair = call swiftcc { ptr, ptr } %prepared(
+ ptr noalias %buffer, ptr swiftself %object)
+ br label %join
+
+join:
+ %pair = phi { ptr, ptr } [ %direct_pair, %direct ],
+ [ %indirect_pair, %indirect ]
+ %continuation = extractvalue { ptr, ptr } %pair, 0
+ %field = extractvalue { ptr, ptr } %pair, 1
+ call void @consume(ptr %field)
+ call swiftcc void %continuation(ptr %buffer, i1 false)
+ ret void
+}
+
+; CHECK-LABEL: define void @caller(
+; CHECK: direct:
+; CHECK: %direct_pair = call swiftcc { ptr, ptr } @accessor(
+; CHECK: join:
+; CHECK: %pair = phi { ptr, ptr } [ %direct_pair, %direct ], [ %indirect_pair, %indirect ]
+
+; SPLIT-LABEL: define internal swiftcc { ptr, ptr } @accessor(
+; SPLIT: ret { ptr, ptr }
+; SPLIT-LABEL: define void @caller(
+; SPLIT: %pair = phi { ptr, ptr } [ %direct_pair, %direct ], [ %indirect_pair, %indirect ]
+
+define internal i32 @ordinary() {
+entry:
+ ret i32 7
+}
+
+define i32 @ordinary_caller() {
+entry:
+ %value = call i32 @ordinary()
+ ret i32 %value
+}
+
+; CHECK-LABEL: define i32 @ordinary_caller()
+; CHECK: ret i32 7
+
+attributes #0 = { noinline presplitcoroutine }
More information about the llvm-commits
mailing list