[llvm] [TailCallElim] Do not mark a call tail when it is handed the frame (PR #218797)
Matt Turner via llvm-commits
llvm-commits at lists.llvm.org
Wed Aug 26 19:01:34 PDT 2026
https://github.com/mattst88 updated https://github.com/llvm/llvm-project/pull/218797
>From 4e85c48bdcb4ae8c0a365b4bdb8bb462fbd5b876 Mon Sep 17 00:00:00 2001
From: Matt Turner <mattst88 at gmail.com>
Date: Wed, 5 Aug 2026 15:49:01 -0400
Subject: [PATCH] [TailCallElim] Do not mark a call tail when it is handed the
frame
markTails treats allocas and byval arguments as the local stack, but the
intrinsics that hand out an address of the current frame name it just as
directly. Passing one of those to a call that then got marked tail let
the frame be torn down before the callee ran.
llvm.localaddress is the one with an in-tree user: it hands out the
frame pointer llvm.localrecover consumes, and Windows SEH and C++ EH
pass it to an outlined funclet by value.
gcc.c-torture/execute/frame-address.c aborts on this on every target.
llvm.frameaddress with a non-zero level names a caller's frame, which a
tail call does not tear down, so only level zero blocks the marking.
llvm.returnaddress is a code address and is not affected.
While here, stop treating llvm.stackrestore as an escape. It only
assigns its argument to the stack pointer, so it cannot leak it, and
llvm.stacksave joining the list above would otherwise cost every call
after a VLA scope its tail marking.
LangRef now states that the callee of a tail call may not access the
caller's frame address, which is what makes this an IR rule rather than
a choice this pass makes.
Assisted-by: Claude Code
---
llvm/docs/LangRef.md | 19 ++-
.../Scalar/TailRecursionElimination.cpp | 62 +++++--
.../Transforms/TailCallElim/frame-address.ll | 161 ++++++++++++++++++
.../Transforms/TailCallElim/stackrestore.ll | 50 ++++++
4 files changed, 275 insertions(+), 17 deletions(-)
create mode 100644 llvm/test/Transforms/TailCallElim/frame-address.ll
diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index ac758261c80e1..0255c9a629043 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -13725,9 +13725,13 @@ This instruction requires several arguments:
the return value of the callee is returned to the caller's caller, even
if a void return type is in use.
- Both markers imply that the callee does not access allocas, va_args, or
- byval arguments from the caller. As an exception to that, an alloca or byval
- argument may be passed to the callee as a byval argument, which can be
+ Both markers imply that the callee does not access any value derived from
+ the caller's stack frame, which is torn down before the callee runs. That
+ covers allocas, va_args, and byval arguments, and equally an address of the
+ frame itself, however obtained -- for example the result of
+ `llvm.frameaddress` with a level of zero, `llvm.localaddress`, or
+ `llvm.stacksave` evaluated in the caller. As an exception, an alloca or
+ byval argument may be passed to the callee as a byval argument, which can be
dereferenced inside the callee. For example:
```llvm
@@ -13782,6 +13786,15 @@ This instruction requires several arguments:
tail call void @take_ptr(ptr %x)
ret void
}
+
+ ; Invalid (assuming @take_ptr dereferences the pointer), because the frame
+ ; @frameaddress names is torn down before @take_ptr runs.
+ define void @invalid_frameaddress() {
+ entry:
+ %fp = call ptr @llvm.frameaddress.p0(i32 0)
+ tail call void @take_ptr(ptr %fp)
+ ret void
+ }
```
Calls marked `musttail` must obey the following additional rules:
diff --git a/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp b/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp
index 880b0bda90d70..dac631c4c1ee2 100644
--- a/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/TailRecursionElimination.cpp
@@ -155,10 +155,10 @@ static bool canTRE(Function &F) {
}
namespace {
-struct AllocaDerivedValueTracker {
+struct LocalStackValueTracker {
// Start at a root value and walk its use-def chain to mark calls that use the
- // value or a derived value in AllocaUsers, and places where it may escape in
- // EscapePoints.
+ // value or a derived value in LocalStackUsers, and places where it may
+ // escape in EscapePoints.
void walk(Value *Root) {
SmallVector<Use *, 32> Worklist;
SmallPtrSet<Use *, 32> Visited;
@@ -181,6 +181,14 @@ struct AllocaDerivedValueTracker {
case Instruction::Call:
case Instruction::Invoke: {
auto &CB = cast<CallBase>(*I);
+ // llvm.stackrestore only assigns its argument to the stack pointer. It
+ // neither captures the value nor hands it to anything that could, so it
+ // is not an escape even though it writes memory and its argument is not
+ // marked nocapture. Without this every call after a VLA scope or a
+ // __builtin_stack_save would lose its tail marking.
+ if (auto *II = dyn_cast<IntrinsicInst>(I);
+ II && II->getIntrinsicID() == Intrinsic::stackrestore)
+ continue;
// If the alloca-derived argument is passed byval it is not an escape
// point, or a use of an alloca. Calling with byval copies the contents
// of the alloca into argument registers or stack slots, which exist
@@ -223,8 +231,8 @@ struct AllocaDerivedValueTracker {
}
void callUsesLocalStack(CallBase &CB, bool IsNocapture) {
- // Add it to the list of alloca users.
- AllocaUsers.insert(&CB);
+ // Add it to the list of calls that use the local stack.
+ LocalStackUsers.insert(&CB);
// If it's nocapture then it can't capture this alloca.
if (IsNocapture)
@@ -235,26 +243,51 @@ struct AllocaDerivedValueTracker {
EscapePoints.insert(&CB);
}
- SmallPtrSet<Instruction *, 32> AllocaUsers;
+ SmallPtrSet<Instruction *, 32> LocalStackUsers;
SmallPtrSet<Instruction *, 32> EscapePoints;
};
} // namespace
+/// Does \p II produce an address of the frame of the function containing it?
+/// Such an address is dangling once a tail call has torn that frame down, so a
+/// call that receives one cannot be marked tail. llvm.returnaddress is not in
+/// this class: it produces a code address, not a frame address.
+static bool namesCurrentFrame(const IntrinsicInst *II) {
+ if (!II)
+ return false;
+ switch (II->getIntrinsicID()) {
+ case Intrinsic::frameaddress:
+ // A non-zero level names a frame further up the stack, which outlives the
+ // frame a tail call destroys.
+ return cast<ConstantInt>(II->getArgOperand(0))->isZero();
+ case Intrinsic::addressofreturnaddress:
+ case Intrinsic::eh_dwarf_cfa:
+ case Intrinsic::localaddress:
+ case Intrinsic::sponentry:
+ case Intrinsic::stackaddress:
+ case Intrinsic::stacksave:
+ case Intrinsic::swift_async_context_addr:
+ return true;
+ default:
+ return false;
+ }
+}
+
static bool markTails(Function &F, OptimizationRemarkEmitter *ORE,
ProfileSummaryInfo *PSI, BlockFrequencyInfo *BFI) {
if (F.callsFunctionThatReturnsTwice())
return false;
- // The local stack holds all alloca instructions and all byval arguments.
- AllocaDerivedValueTracker Tracker;
+ // The local stack is reachable through allocas, through byval arguments, and
+ // through the intrinsics that hand out an address of the current frame.
+ LocalStackValueTracker Tracker;
for (Argument &Arg : F.args()) {
if (Arg.hasByValAttr())
Tracker.walk(&Arg);
}
- for (auto &BB : F) {
- for (auto &I : BB)
- if (AllocaInst *AI = dyn_cast<AllocaInst>(&I))
- Tracker.walk(AI);
+ for (Instruction &I : instructions(F)) {
+ if (isa<AllocaInst>(&I) || namesCurrentFrame(dyn_cast<IntrinsicInst>(&I)))
+ Tracker.walk(&I);
}
bool Modified = false;
@@ -320,7 +353,7 @@ static bool markTails(Function &F, OptimizationRemarkEmitter *ORE,
// global anyhow.
//
// Note that this runs whether we know an alloca has escaped or not. If
- // it has, then we can't trust Tracker.AllocaUsers to be accurate.
+ // it has, then we can't trust Tracker.LocalStackUsers to be accurate.
bool SafeToTail = true;
for (auto &Arg : CI->args()) {
if (isa<Constant>(Arg.getUser()))
@@ -343,7 +376,8 @@ static bool markTails(Function &F, OptimizationRemarkEmitter *ORE,
}
}
- if (!IsNoTail && Escaped == UNESCAPED && !Tracker.AllocaUsers.count(CI))
+ if (!IsNoTail && Escaped == UNESCAPED &&
+ !Tracker.LocalStackUsers.count(CI))
DeferredTails.push_back(CI);
}
diff --git a/llvm/test/Transforms/TailCallElim/frame-address.ll b/llvm/test/Transforms/TailCallElim/frame-address.ll
new file mode 100644
index 0000000000000..42bda9605452e
--- /dev/null
+++ b/llvm/test/Transforms/TailCallElim/frame-address.ll
@@ -0,0 +1,161 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; RUN: opt < %s -passes=tailcallelim -S | FileCheck %s
+
+; A tail call tears the frame down before the callee runs, so a call handed a
+; pointer to this function's own frame must not be marked tail.
+
+declare ptr @llvm.frameaddress.p0(i32)
+declare ptr @llvm.returnaddress.p0(i32)
+declare ptr @llvm.addressofreturnaddress.p0()
+declare ptr @llvm.eh.dwarf.cfa(i32)
+declare ptr @llvm.localaddress()
+declare ptr @llvm.sponentry.p0()
+declare ptr @llvm.stackaddress.p0()
+declare ptr @llvm.stacksave.p0()
+declare ptr @llvm.swift.async.context.addr()
+declare void @callee(ptr)
+
+define void @pass_frameaddress() {
+; CHECK-LABEL: define void @pass_frameaddress() {
+; CHECK-NEXT: [[FA:%.*]] = tail call ptr @llvm.frameaddress.p0(i32 0)
+; CHECK-NEXT: call void @callee(ptr [[FA]])
+; CHECK-NEXT: ret void
+;
+ %fa = call ptr @llvm.frameaddress.p0(i32 0)
+ call void @callee(ptr %fa)
+ ret void
+}
+
+; A non-zero level names a caller's frame, which this tail call does not tear
+; down.
+define void @pass_parent_frameaddress() {
+; CHECK-LABEL: define void @pass_parent_frameaddress() {
+; CHECK-NEXT: [[FA:%.*]] = tail call ptr @llvm.frameaddress.p0(i32 1)
+; CHECK-NEXT: tail call void @callee(ptr [[FA]])
+; CHECK-NEXT: ret void
+;
+ %fa = call ptr @llvm.frameaddress.p0(i32 1)
+ call void @callee(ptr %fa)
+ ret void
+}
+
+define void @pass_addressofreturnaddress() {
+; CHECK-LABEL: define void @pass_addressofreturnaddress() {
+; CHECK-NEXT: [[RA:%.*]] = tail call ptr @llvm.addressofreturnaddress.p0()
+; CHECK-NEXT: call void @callee(ptr [[RA]])
+; CHECK-NEXT: ret void
+;
+ %ra = call ptr @llvm.addressofreturnaddress.p0()
+ call void @callee(ptr %ra)
+ ret void
+}
+
+; llvm.eh.dwarf.cfa hands out the canonical frame address of this frame.
+define void @pass_dwarf_cfa() {
+; CHECK-LABEL: define void @pass_dwarf_cfa() {
+; CHECK-NEXT: [[CFA:%.*]] = tail call ptr @llvm.eh.dwarf.cfa(i32 0)
+; CHECK-NEXT: call void @callee(ptr [[CFA]])
+; CHECK-NEXT: ret void
+;
+ %cfa = call ptr @llvm.eh.dwarf.cfa(i32 0)
+ call void @callee(ptr %cfa)
+ ret void
+}
+
+; llvm.localaddress hands out the frame pointer llvm.localrecover consumes, and
+; Windows SEH and C++ EH pass it to an outlined funclet by value.
+define void @pass_localaddress() {
+; CHECK-LABEL: define void @pass_localaddress() {
+; CHECK-NEXT: [[LA:%.*]] = tail call ptr @llvm.localaddress()
+; CHECK-NEXT: call void @callee(ptr [[LA]])
+; CHECK-NEXT: ret void
+;
+ %la = call ptr @llvm.localaddress()
+ call void @callee(ptr %la)
+ ret void
+}
+
+define void @pass_sponentry() {
+; CHECK-LABEL: define void @pass_sponentry() {
+; CHECK-NEXT: [[SP:%.*]] = tail call ptr @llvm.sponentry.p0()
+; CHECK-NEXT: call void @callee(ptr [[SP]])
+; CHECK-NEXT: ret void
+;
+ %sp = call ptr @llvm.sponentry.p0()
+ call void @callee(ptr %sp)
+ ret void
+}
+
+define void @pass_stackaddress() {
+; CHECK-LABEL: define void @pass_stackaddress() {
+; CHECK-NEXT: [[SA:%.*]] = tail call ptr @llvm.stackaddress.p0()
+; CHECK-NEXT: call void @callee(ptr [[SA]])
+; CHECK-NEXT: ret void
+;
+ %sa = call ptr @llvm.stackaddress.p0()
+ call void @callee(ptr %sa)
+ ret void
+}
+
+define void @pass_stacksave() {
+; CHECK-LABEL: define void @pass_stacksave() {
+; CHECK-NEXT: [[SS:%.*]] = tail call ptr @llvm.stacksave.p0()
+; CHECK-NEXT: call void @callee(ptr [[SS]])
+; CHECK-NEXT: ret void
+;
+ %ss = call ptr @llvm.stacksave.p0()
+ call void @callee(ptr %ss)
+ ret void
+}
+
+define void @pass_swift_async_context_addr() {
+; CHECK-LABEL: define void @pass_swift_async_context_addr() {
+; CHECK-NEXT: [[CTX:%.*]] = tail call ptr @llvm.swift.async.context.addr()
+; CHECK-NEXT: call void @callee(ptr [[CTX]])
+; CHECK-NEXT: ret void
+;
+ %ctx = call ptr @llvm.swift.async.context.addr()
+ call void @callee(ptr %ctx)
+ ret void
+}
+
+; llvm.returnaddress hands out a code address, not an address of this frame.
+define void @pass_returnaddress() {
+; CHECK-LABEL: define void @pass_returnaddress() {
+; CHECK-NEXT: [[RA:%.*]] = tail call ptr @llvm.returnaddress.p0(i32 0)
+; CHECK-NEXT: tail call void @callee(ptr [[RA]])
+; CHECK-NEXT: ret void
+;
+ %ra = call ptr @llvm.returnaddress.p0(i32 0)
+ call void @callee(ptr %ra)
+ ret void
+}
+
+; The pointer reaching the callee through a gep is still this frame's.
+define void @pass_frameaddress_gep() {
+; CHECK-LABEL: define void @pass_frameaddress_gep() {
+; CHECK-NEXT: [[FA:%.*]] = tail call ptr @llvm.frameaddress.p0(i32 0)
+; CHECK-NEXT: [[P:%.*]] = getelementptr i8, ptr [[FA]], i64 8
+; CHECK-NEXT: call void @callee(ptr [[P]])
+; CHECK-NEXT: ret void
+;
+ %fa = call ptr @llvm.frameaddress.p0(i32 0)
+ %p = getelementptr i8, ptr %fa, i64 8
+ call void @callee(ptr %p)
+ ret void
+}
+
+; A frame address the callee is not handed does not block the tail call.
+define i1 @keep_frameaddress_local(ptr %p) {
+; CHECK-LABEL: define i1 @keep_frameaddress_local(
+; CHECK-SAME: ptr [[P:%.*]]) {
+; CHECK-NEXT: [[FA:%.*]] = tail call ptr @llvm.frameaddress.p0(i32 0)
+; CHECK-NEXT: tail call void @callee(ptr [[P]])
+; CHECK-NEXT: [[CMP:%.*]] = icmp ult ptr [[FA]], [[P]]
+; CHECK-NEXT: ret i1 [[CMP]]
+;
+ %fa = call ptr @llvm.frameaddress.p0(i32 0)
+ call void @callee(ptr %p)
+ %cmp = icmp ult ptr %fa, %p
+ ret i1 %cmp
+}
diff --git a/llvm/test/Transforms/TailCallElim/stackrestore.ll b/llvm/test/Transforms/TailCallElim/stackrestore.ll
index 55b1ec933e1ec..7befb247a25f0 100644
--- a/llvm/test/Transforms/TailCallElim/stackrestore.ll
+++ b/llvm/test/Transforms/TailCallElim/stackrestore.ll
@@ -10,5 +10,55 @@ entry:
ret void
}
+; llvm.stackrestore does not capture its argument, so it does not make the
+; local stack escape and the call after it is still a tail call. The token
+; reaches it directly here and through a phi below, which is the shape clang
+; emits for a variable length array declared in both arms of an if.
+define void @stackrestore_is_not_an_escape(ptr %p) {
+; CHECK-LABEL: define void @stackrestore_is_not_an_escape(
+; CHECK: tail call void @callee
+;
+entry:
+ %ss = call ptr @llvm.stacksave.p0()
+ call void @llvm.stackrestore.p0(ptr %ss)
+ call void @callee(ptr %p)
+ ret void
+}
+
+define void @stackrestore_of_a_phi_is_not_an_escape(ptr %p, i1 %c) {
+; CHECK-LABEL: define void @stackrestore_of_a_phi_is_not_an_escape(
+; CHECK: tail call void @callee
+;
+entry:
+ %ss = call ptr @llvm.stacksave.p0()
+ br i1 %c, label %then, label %else
+
+then:
+ %ss2 = call ptr @llvm.stacksave.p0()
+ br label %join
+
+else:
+ br label %join
+
+join:
+ %sink = phi ptr [ %ss2, %then ], [ %ss, %else ]
+ call void @llvm.stackrestore.p0(ptr %sink)
+ call void @callee(ptr %p)
+ ret void
+}
+
+; An alloca handed to llvm.stackrestore is not captured by it either.
+define void @stackrestore_of_an_alloca_is_not_an_escape(ptr %p) {
+; CHECK-LABEL: define void @stackrestore_of_an_alloca_is_not_an_escape(
+; CHECK: tail call void @callee
+;
+entry:
+ %a = alloca i8
+ call void @llvm.stackrestore.p0(ptr %a)
+ call void @callee(ptr %p)
+ ret void
+}
+
+declare void @callee(ptr)
declare ptr @llvm.stacksave.p0()
declare void @llvm.stackrestore.p0(ptr)
More information about the llvm-commits
mailing list