Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 989d72f

Browse files
committedMar 16, 2025·
Implement guaranteed tail calls with the become keyword in the LLVM backend
1 parent 66678e6 commit 989d72f

File tree

10 files changed

+1395
-6
lines changed

10 files changed

+1395
-6
lines changed
 

‎compiler/rustc_codegen_llvm/src/builder.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,11 @@ impl<'a, 'll, 'tcx> BuilderMethods<'a, 'tcx> for Builder<'a, 'll, 'tcx> {
14191419
let cold_inline = llvm::AttributeKind::Cold.create_attr(self.llcx);
14201420
attributes::apply_to_callsite(llret, llvm::AttributePlace::Function, &[cold_inline]);
14211421
}
1422+
1423+
fn set_tail_call(&mut self, call_inst: &'ll Value) {
1424+
// LLVMSetTailCall is marked as safe in the FFI definition
1425+
llvm::LLVMSetTailCall(call_inst, llvm::True);
1426+
}
14221427
}
14231428

14241429
impl<'ll> StaticBuilderMethods for Builder<'_, 'll, '_> {

‎compiler/rustc_codegen_ssa/src/mir/block.rs

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,90 @@ impl<'a, 'tcx> TerminatorCodegenHelper<'tcx> {
342342

343343
/// Codegen implementations for some terminator variants.
344344
impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
345+
fn codegen_tail_call_terminator(
346+
&mut self,
347+
bx: &mut Bx,
348+
func: &mir::Operand<'tcx>,
349+
args: &[Spanned<mir::Operand<'tcx>>],
350+
fn_span: Span,
351+
) {
352+
// We don't need source_info as we already have fn_span for diagnostics
353+
let func = self.codegen_operand(bx, func);
354+
let fn_ty = func.layout.ty;
355+
356+
// Create the callee. This is a fn ptr or zero-sized and hence a kind of scalar.
357+
let (fn_ptr, fn_abi, instance) = match *fn_ty.kind() {
358+
ty::FnDef(def_id, substs) => {
359+
let instance = ty::Instance::expect_resolve(
360+
bx.tcx(),
361+
bx.typing_env(),
362+
def_id,
363+
substs,
364+
fn_span,
365+
);
366+
let fn_ptr = bx.get_fn_addr(instance);
367+
let fn_abi = bx.fn_abi_of_instance(instance, ty::List::empty());
368+
(fn_ptr, fn_abi, Some(instance))
369+
}
370+
ty::FnPtr(..) => {
371+
let sig = fn_ty.fn_sig(bx.tcx());
372+
let extra_args = bx.tcx().mk_type_list(&[]);
373+
let fn_ptr = func.immediate();
374+
let fn_abi = bx.fn_abi_of_fn_ptr(sig, extra_args);
375+
(fn_ptr, fn_abi, None)
376+
}
377+
_ => bug!("{} is not callable", func.layout.ty),
378+
};
379+
380+
let mut llargs = Vec::with_capacity(args.len());
381+
382+
// Process arguments
383+
for arg in args {
384+
let op = self.codegen_operand(bx, &arg.node);
385+
let arg_idx = llargs.len();
386+
387+
if arg_idx < fn_abi.args.len() {
388+
self.codegen_argument(bx, op, &mut llargs, &fn_abi.args[arg_idx]);
389+
} else {
390+
// This can happen in case of C-variadic functions
391+
let is_immediate = match op.val {
392+
Immediate(_) => true,
393+
_ => false,
394+
};
395+
396+
if is_immediate {
397+
llargs.push(op.immediate());
398+
} else {
399+
let temp = PlaceRef::alloca(bx, op.layout);
400+
op.val.store(bx, temp);
401+
llargs.push(bx.load(
402+
bx.backend_type(op.layout),
403+
temp.val.llval,
404+
temp.val.align,
405+
));
406+
}
407+
}
408+
}
409+
410+
// Call the function
411+
let fn_ty = bx.fn_decl_backend_type(fn_abi);
412+
let fn_attrs = if let Some(instance) = instance
413+
&& bx.tcx().def_kind(instance.def_id()).has_codegen_attrs()
414+
{
415+
Some(bx.tcx().codegen_fn_attrs(instance.def_id()))
416+
} else {
417+
None
418+
};
419+
420+
// Perform the actual function call
421+
let llret = bx.call(fn_ty, fn_attrs, Some(fn_abi), fn_ptr, &llargs, None, instance);
422+
423+
// Mark as tail call - this is the critical part
424+
bx.set_tail_call(llret);
425+
426+
// Return the result
427+
bx.ret(llret);
428+
}
345429
/// Generates code for a `Resume` terminator.
346430
fn codegen_resume_terminator(&mut self, helper: TerminatorCodegenHelper<'tcx>, bx: &mut Bx) {
347431
if let Some(funclet) = helper.funclet(self) {
@@ -1430,12 +1514,9 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
14301514
fn_span,
14311515
mergeable_succ(),
14321516
),
1433-
mir::TerminatorKind::TailCall { .. } => {
1434-
// FIXME(explicit_tail_calls): implement tail calls in ssa backend
1435-
span_bug!(
1436-
terminator.source_info.span,
1437-
"`TailCall` terminator is not yet supported by `rustc_codegen_ssa`"
1438-
)
1517+
mir::TerminatorKind::TailCall { ref func, ref args, fn_span } => {
1518+
self.codegen_tail_call_terminator(bx, func, args, fn_span);
1519+
MergingSucc::False
14391520
}
14401521
mir::TerminatorKind::CoroutineDrop | mir::TerminatorKind::Yield { .. } => {
14411522
bug!("coroutine ops in codegen")

‎compiler/rustc_codegen_ssa/src/traits/builder.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,10 @@ pub trait BuilderMethods<'a, 'tcx>:
555555
funclet: Option<&Self::Funclet>,
556556
instance: Option<Instance<'tcx>>,
557557
) -> Self::Value;
558+
559+
/// Mark a call instruction as a tail call (guaranteed tail call optimization)
560+
/// Used for implementing the `become` expression
561+
fn set_tail_call(&mut self, call_inst: Self::Value);
558562
fn zext(&mut self, val: Self::Value, dest_ty: Self::Type) -> Self::Value;
559563

560564
fn apply_attrs_to_cleanup_callsite(&mut self, llret: Self::Value);
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This test verifies that the `become` keyword generates tail call instructions in LLVM IR
2+
3+
# Use the Rust compiler from the build directory
4+
RUSTC ?= ../../../build/aarch64-apple-darwin/stage1/bin/rustc
5+
6+
all: verify-tail-call
7+
8+
verify-tail-call:
9+
# Create test file with debuginfo to ensure function names are preserved
10+
$(RUSTC) tail-call-test.rs --emit=llvm-ir -g -C opt-level=0
11+
12+
# Check that `become` generates 'tail call' instructions for with_tail function
13+
grep -q "tail call.*@_ZN14tail_call_test9with_tail" tail-call-test.ll || (echo "ERROR: 'tail call' instruction not found for with_tail"; exit 1)
14+
15+
# Check that regular recursive calls don't use tail call optimization by default
16+
grep -q "@_ZN14tail_call_test7no_tail" tail-call-test.ll || (echo "ERROR: no_tail function not found"; exit 1)
17+
! grep -q "tail call.*@_ZN14tail_call_test7no_tail" tail-call-test.ll || (echo "ERROR: Regular function call incorrectly marked as tail call"; exit 1)
18+
19+
# Check mutual recursion with 'become'
20+
grep -q "tail call.*@_ZN14tail_call_test14even_with_tail" tail-call-test.ll || (echo "ERROR: 'tail call' instruction not found for even_with_tail"; exit 1)
21+
grep -q "tail call.*@_ZN14tail_call_test13odd_with_tail" tail-call-test.ll || (echo "ERROR: 'tail call' instruction not found for odd_with_tail"; exit 1)
22+
23+
# The test passes only if all checks above confirm that:
24+
# 1. Functions using 'become' generate LLVM tail call instructions
25+
# 2. Regular recursive functions don't generate tail call instructions
26+
@echo "LLVM IR verification successful: 'become' correctly generates 'tail call' instructions"

‎tests/run-make/tail-call-llvm-ir/tail-call-test.ll

Lines changed: 1068 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#![feature(explicit_tail_calls)]
2+
3+
// A recursive function that uses explicit tail calls with 'become'
4+
// This should generate LLVM IR with "tail call" instructions
5+
pub fn with_tail(n: u32) -> u32 {
6+
if n == 0 { 0 } else { become with_tail(n - 1) }
7+
}
8+
9+
// A similar recursive function but without 'become'
10+
// This should NOT generate tail call instructions by default
11+
pub fn no_tail(n: u32) -> u32 {
12+
if n == 0 { 0 } else { no_tail(n - 1) }
13+
}
14+
15+
// Mutual recursion with 'become'
16+
// Both functions should generate tail call instructions
17+
pub fn even_with_tail(n: u32) -> bool {
18+
match n {
19+
0 => true,
20+
_ => become odd_with_tail(n - 1),
21+
}
22+
}
23+
24+
pub fn odd_with_tail(n: u32) -> bool {
25+
match n {
26+
0 => false,
27+
_ => become even_with_tail(n - 1),
28+
}
29+
}
30+
31+
// For comparison: mutual recursion without 'become'
32+
// These should NOT generate tail call instructions
33+
pub fn even_no_tail(n: u32) -> bool {
34+
match n {
35+
0 => true,
36+
_ => odd_no_tail(n - 1),
37+
}
38+
}
39+
40+
pub fn odd_no_tail(n: u32) -> bool {
41+
match n {
42+
0 => false,
43+
_ => even_no_tail(n - 1),
44+
}
45+
}
46+
47+
fn main() {
48+
// Actually use all the functions to ensure they're not optimized away
49+
let with_tail_result = with_tail(5);
50+
let no_tail_result = no_tail(5);
51+
let even_with_tail_result = even_with_tail(10);
52+
let odd_with_tail_result = odd_with_tail(10);
53+
let even_no_tail_result = even_no_tail(10);
54+
let odd_no_tail_result = odd_no_tail(10);
55+
56+
println!(
57+
"Results: {} {} {} {} {} {}",
58+
with_tail_result,
59+
no_tail_result,
60+
even_with_tail_result,
61+
odd_with_tail_result,
62+
even_no_tail_result,
63+
odd_no_tail_result
64+
);
65+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Simple Makefile for Rust tests
2+
3+
# Default rustc to use
4+
RUSTC ?= ../../../build/aarch64-apple-darwin/stage1/bin/rustc
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//@ compile-flags: -O
2+
//@ run-pass
3+
#![expect(incomplete_features)]
4+
#![feature(explicit_tail_calls)]
5+
6+
// A deep recursive function that uses explicit tail calls
7+
// This will cause stack overflow without tail call optimization
8+
fn deep_recursion(n: u32) -> u32 {
9+
match n {
10+
0 => 0,
11+
_ => become deep_recursion(n - 1)
12+
}
13+
}
14+
15+
// A deep recursive function without explicit tail calls
16+
// This will overflow the stack for large values
17+
fn deep_recursion_no_tail(n: u32) -> u32 {
18+
match n {
19+
0 => 0,
20+
_ => deep_recursion_no_tail(n - 1)
21+
}
22+
}
23+
24+
fn main() {
25+
// Verify correctness for small values
26+
assert_eq!(deep_recursion(10), 0);
27+
assert_eq!(deep_recursion_no_tail(10), 0);
28+
29+
// This will succeed only if tail call optimization is working
30+
// It would overflow the stack otherwise
31+
println!("Starting deep recursion with 50,000 calls");
32+
let result = deep_recursion(50_000);
33+
assert_eq!(result, 0);
34+
println!("Successfully completed 50,000 recursive calls with tail call optimization");
35+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//@ run-pass
2+
#![expect(incomplete_features)]
3+
#![feature(explicit_tail_calls)]
4+
5+
// This is a classic example of mutual recursion:
6+
// even(n) calls odd(n-1), and odd(n) calls even(n-1)
7+
// Without tail calls, this would quickly overflow the stack for large inputs
8+
9+
// Check if a number is even using mutual recursion
10+
fn is_even(n: u64) -> bool {
11+
match n {
12+
0 => true,
13+
_ => become is_odd(n - 1)
14+
}
15+
}
16+
17+
// Check if a number is odd using mutual recursion
18+
fn is_odd(n: u64) -> bool {
19+
match n {
20+
0 => false,
21+
_ => become is_even(n - 1)
22+
}
23+
}
24+
25+
// Versions without tail calls for comparison
26+
fn is_even_no_tail(n: u64) -> bool {
27+
match n {
28+
0 => true,
29+
_ => is_odd_no_tail(n - 1)
30+
}
31+
}
32+
33+
fn is_odd_no_tail(n: u64) -> bool {
34+
match n {
35+
0 => false,
36+
_ => is_even_no_tail(n - 1)
37+
}
38+
}
39+
40+
fn main() {
41+
// Verify correctness for small values
42+
assert_eq!(is_even(0), true);
43+
assert_eq!(is_odd(0), false);
44+
assert_eq!(is_even(1), false);
45+
assert_eq!(is_odd(1), true);
46+
assert_eq!(is_even(10), true);
47+
assert_eq!(is_odd(10), false);
48+
49+
// Test with an extremely large number that would definitely overflow the stack
50+
// without tail call optimization - each call creates 2 stack frames (alternating between functions)
51+
// so 100,000 would create 200,000 stack frames total
52+
assert_eq!(is_even(100_000), true);
53+
assert_eq!(is_odd(100_000), false);
54+
assert_eq!(is_even(100_001), false);
55+
assert_eq!(is_odd(100_001), true);
56+
57+
println!("Deep mutual recursion test passed with 100,000 alternating recursive calls!");
58+
59+
// Verify non-tail versions work for small values
60+
assert_eq!(is_even_no_tail(10), true);
61+
assert_eq!(is_odd_no_tail(10), false);
62+
// But would overflow for large values (not tested to avoid crashing)
63+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//@ run-pass
2+
#![expect(incomplete_features)]
3+
#![feature(explicit_tail_calls)]
4+
5+
// A function that causes deep recursion - counts down from n to 0
6+
// Without tail calls, this would overflow the stack for large n values
7+
fn countdown(n: u32) -> u32 {
8+
match n {
9+
0 => 0,
10+
_ => become countdown(n - 1)
11+
}
12+
}
13+
14+
// Same function but without tail call optimization
15+
fn countdown_no_tail(n: u32) -> u32 {
16+
match n {
17+
0 => 0,
18+
_ => countdown_no_tail(n - 1)
19+
}
20+
}
21+
22+
// This test is specifically designed to verify tail call optimization
23+
// We use an extremely large recursion depth (500,000) that would
24+
// absolutely overflow the stack without tail call optimization
25+
fn main() {
26+
// Small test to verify correctness
27+
assert_eq!(countdown(10), 0);
28+
29+
// Regular recursion would overflow here (500,000 stack frames)
30+
// Only works if tail call optimization is actually happening
31+
let result = countdown(500_000);
32+
assert_eq!(result, 0);
33+
println!("Successfully completed 500,000 recursive calls with tail call optimization");
34+
35+
// We can't test the non-tail version with a large number as it would crash,
36+
// but we can verify it works for small values
37+
assert_eq!(countdown_no_tail(10), 0);
38+
}

0 commit comments

Comments
 (0)
Please sign in to comment.