Skip to content

Commit a61a2b4

Browse files
committed
explicit tail calls: pass caller's argument slots as arguments for indirect arguments
1 parent 9030e34 commit a61a2b4

3 files changed

Lines changed: 175 additions & 43 deletions

File tree

compiler/rustc_codegen_ssa/src/mir/block.rs

Lines changed: 61 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ enum MergingSucc {
3939

4040
/// Indicates to the call terminator codegen whether a call
4141
/// is a normal call or an explicit tail call.
42-
#[derive(Debug, PartialEq)]
42+
#[derive(Debug, PartialEq, Clone, Copy)]
4343
enum CallKind {
4444
Normal,
4545
Tail,
@@ -1195,8 +1195,8 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
11951195

11961196
// Special logic for tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments.
11971197
//
1198-
// Normally an indirect argument that is allocated in the caller's stack frame
1199-
// would be passed as a pointer into the callee's stack frame.
1198+
// Normally an indirect pointer that is allocated in the caller's stack frame
1199+
// would be passed as an argument.
12001200
// For tail calls, that would be unsound, because the caller's
12011201
// stack frame is overwritten by the callee's stack frame.
12021202
//
@@ -1227,7 +1227,7 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
12271227

12281228
let op = self.codegen_operand(bx, &arg.node);
12291229
let tmp = PlaceRef::alloca(bx, op.layout);
1230-
bx.lifetime_start(tmp.val.llval, tmp.layout.size);
1230+
tmp.storage_live(bx);
12311231
op.store_with_annotation(bx, tmp);
12321232

12331233
tail_call_temporaries[i] = Some(tmp);
@@ -1289,69 +1289,59 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
12891289
}
12901290
}
12911291

1292-
let by_move = if let PassMode::Indirect { on_stack: false, .. } = fn_abi.args[i].mode
1292+
let tmp = if let PassMode::Indirect { on_stack: false, .. } = fn_abi.args[i].mode
12931293
&& kind == CallKind::Tail
12941294
{
12951295
// Special logic for tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments.
12961296
//
1297-
// Normally an indirect argument that is allocated in the caller's stack frame
1298-
// would be passed as a pointer into the callee's stack frame.
1297+
// Normally an indirect pointer that is allocated in the caller's stack frame
1298+
// would be passed as an argument.
12991299
// For tail calls, that would be unsound, because the caller's
13001300
// stack frame is overwritten by the callee's stack frame.
13011301
//
13021302
// To handle the case, we introduce `tail_call_temporaries` to copy arguments into
13031303
// temporaries, then copy back to the caller's argument slots.
1304-
// Finally, we pass the caller's argument slots as arguments.
1305-
//
1306-
// To do that, the argument must be MUST-by-move value.
1304+
// Finally, we pass the caller's argument slots as arguments which is implemented in
1305+
// `codegen_argument`.
13071306
let Some(tmp) = tail_call_temporaries[i].take() else {
13081307
span_bug!(fn_span, "missing temporary for indirect tail call argument #{i}")
13091308
};
1310-
1311-
let local = self.mir.args_iter().nth(i).unwrap();
1312-
1313-
match &self.locals[local] {
1314-
LocalRef::Place(arg) => {
1315-
bx.typed_place_copy(arg.val, tmp.val, fn_abi.args[i].layout);
1316-
op.val = Ref(arg.val);
1317-
}
1318-
LocalRef::Operand(arg) => {
1319-
let Ref(place_value) = arg.val else {
1320-
bug!("only `Ref` should use `PassMode::Indirect`");
1321-
};
1322-
bx.typed_place_copy(place_value, tmp.val, fn_abi.args[i].layout);
1323-
op.val = arg.val;
1324-
}
1325-
LocalRef::UnsizedPlace(_) => {
1326-
span_bug!(fn_span, "unsized types are not supported")
1327-
}
1328-
LocalRef::PendingOperand => {
1329-
span_bug!(fn_span, "argument local should not be pending")
1330-
}
1331-
};
1332-
1333-
bx.lifetime_end(tmp.val.llval, tmp.layout.size);
1334-
true
1309+
op.val = Ref(tmp.val);
1310+
Some(tmp)
13351311
} else {
1336-
matches!(arg.node, mir::Operand::Move(_))
1312+
None
13371313
};
13381314

13391315
self.codegen_argument(
13401316
bx,
13411317
op,
1342-
by_move,
1318+
matches!(arg.node, mir::Operand::Move(_)),
13431319
&mut llargs,
13441320
&fn_abi.args[i],
13451321
&mut lifetime_ends_after_call,
1322+
fn_span,
1323+
kind,
13461324
);
1325+
1326+
if let Some(tmp) = tmp {
1327+
tmp.storage_dead(bx);
1328+
}
13471329
}
13481330
let num_untupled = untuple.map(|tup| {
1331+
// For untupled arguments, it is safe to store them directly into the caller's
1332+
// argument slots without temporaries, because the untupled arguments are
1333+
// always passed through a tuple alloca. The alloca serves as a temporary
1334+
// that does not overlap with any caller argument.
1335+
// No temporaries are needed for the caller's untupled arguments either,
1336+
// because they are used last.
13491337
self.codegen_arguments_untupled(
13501338
bx,
13511339
&tup.node,
13521340
&mut llargs,
13531341
&fn_abi.args[first_args.len()..],
13541342
&mut lifetime_ends_after_call,
1343+
fn_span,
1344+
kind,
13551345
)
13561346
});
13571347

@@ -1382,6 +1372,8 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
13821372
&mut llargs,
13831373
last_arg,
13841374
&mut lifetime_ends_after_call,
1375+
fn_span,
1376+
kind,
13851377
);
13861378
}
13871379

@@ -1701,6 +1693,8 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
17011693
llargs: &mut Vec<Bx::Value>,
17021694
arg: &ArgAbi<'tcx, Ty<'tcx>>,
17031695
lifetime_ends_after_call: &mut Vec<(Bx::Value, Size)>,
1696+
fn_span: Span,
1697+
kind: CallKind,
17041698
) {
17051699
match arg.mode {
17061700
PassMode::Ignore => return,
@@ -1753,23 +1747,34 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
17531747
PassMode::Ignore | PassMode::Pair(..) => unreachable!("handled above"),
17541748
},
17551749
Ref(op_place_val) => match arg.mode {
1756-
PassMode::Indirect { attrs, on_stack, .. } => {
1750+
PassMode::Indirect { attrs, meta_attrs, on_stack, .. } => {
17571751
// For `foo(packed.large_field)`, and types with <4 byte alignment on x86,
17581752
// alignment requirements may be higher than the type's alignment, so copy
17591753
// to a higher-aligned alloca.
17601754
let required_align = match attrs.pointee_align {
17611755
Some(pointee_align) => cmp::max(pointee_align, arg.layout.align.abi),
17621756
None => arg.layout.align.abi,
17631757
};
1764-
// Copy to an alloca when the argument is neither by-val nor by-move.
1765-
if op_place_val.align < required_align || (!on_stack && !by_move) {
1758+
if kind == CallKind::Tail && !on_stack {
1759+
// Special logic for tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments.
1760+
// We pass the caller's argument slots as arguments,
1761+
// because the caller's stack frame is overwritten by the callee's stack frame.
1762+
if meta_attrs.is_some() {
1763+
span_bug!(fn_span, "unsized types are not supported")
1764+
};
1765+
let caller_arg =
1766+
PlaceRef::new_sized(bx.get_param(llargs.len()), arg.layout);
1767+
op.store_with_annotation(bx, caller_arg);
1768+
(caller_arg.val.llval, caller_arg.val.align, true)
1769+
} else if op_place_val.align >= required_align && (on_stack || by_move) {
1770+
// We can skip copy to an alloca when the argument is by-val or by-move.
1771+
(op_place_val.llval, op_place_val.align, true)
1772+
} else {
17661773
let scratch = PlaceValue::alloca(bx, arg.layout.size, required_align);
17671774
bx.lifetime_start(scratch.llval, arg.layout.size);
17681775
op.store_with_annotation(bx, scratch.with_type(arg.layout));
17691776
lifetime_ends_after_call.push((scratch.llval, arg.layout.size));
17701777
(scratch.llval, scratch.align, true)
1771-
} else {
1772-
(op_place_val.llval, op_place_val.align, true)
17731778
}
17741779
}
17751780
_ => (op_place_val.llval, op_place_val.align, true),
@@ -1849,6 +1854,8 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
18491854
llargs: &mut Vec<Bx::Value>,
18501855
args: &[ArgAbi<'tcx, Ty<'tcx>>],
18511856
lifetime_ends_after_call: &mut Vec<(Bx::Value, Size)>,
1857+
fn_span: Span,
1858+
kind: CallKind,
18521859
) -> usize {
18531860
let tuple = self.codegen_operand(bx, operand);
18541861
let by_move = matches!(operand, mir::Operand::Move(_));
@@ -1869,13 +1876,24 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
18691876
llargs,
18701877
&args[i],
18711878
lifetime_ends_after_call,
1879+
fn_span,
1880+
kind,
18721881
);
18731882
}
18741883
} else {
18751884
// If the tuple is immediate, the elements are as well.
18761885
for i in 0..tuple.layout.fields.count() {
18771886
let op = tuple.extract_field(self, bx, i);
1878-
self.codegen_argument(bx, op, by_move, llargs, &args[i], lifetime_ends_after_call);
1887+
self.codegen_argument(
1888+
bx,
1889+
op,
1890+
by_move,
1891+
llargs,
1892+
&args[i],
1893+
lifetime_ends_after_call,
1894+
fn_span,
1895+
kind,
1896+
);
18791897
}
18801898
}
18811899
tuple.layout.fields.count()
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//! Regression test for issue <https://github.com/rust-lang/rust/issues/158017>.
2+
// Checks that we pass the caller's argument slots as arguments at tail call,
3+
// because the caller's stack frame is overwritten by the callee's stack frame.
4+
// In LLVM, Calls marked 'tail' cannot read or write allocas from the current frame
5+
// because the current frame might be destroyed by the time they run. These writes will be
6+
// eliminated by DSE.
7+
//@ add-minicore
8+
//@ revisions: x64-linux i686-linux i686-windows
9+
//@ compile-flags: -C opt-level=3
10+
//@[x64-linux] compile-flags: --target x86_64-unknown-linux-gnu
11+
//@[x64-linux] needs-llvm-components: x86
12+
//@[i686-linux] compile-flags: --target i686-unknown-linux-gnu
13+
//@[i686-linux] needs-llvm-components: x86
14+
//@[i686-windows] compile-flags: --target i686-pc-windows-msvc
15+
//@[i686-windows] needs-llvm-components: x86
16+
17+
#![crate_type = "lib"]
18+
#![feature(explicit_tail_calls, no_core, unboxed_closures)]
19+
#![expect(incomplete_features)]
20+
#![no_std]
21+
#![no_core]
22+
23+
extern crate minicore;
24+
25+
struct Indirect(u64, u64, u64, u64);
26+
27+
// CHECK-LABEL: @caller_untuple_1
28+
// CHECK-SAME: (ptr {{.*}}[[A:%.*]])
29+
// CHECK-NEXT: start:
30+
// CHECK-NEXT: musttail call {{.*}}i64 @callee_untuple_1(ptr {{.*}}[[A]])
31+
#[unsafe(no_mangle)]
32+
extern "rust-call" fn caller_untuple_1((a,): (Indirect,)) -> u64 {
33+
become callee_untuple_1((a,));
34+
}
35+
36+
// CHECK-LABEL: @caller_untuple_1_const
37+
// CHECK-SAME: (ptr {{.*}}[[A:%.*]])
38+
// x64-linux: store i64 1, ptr [[A]]
39+
// i686-linux: store <2 x i64> <i64 1, i64 2>, ptr [[A]]
40+
// i686-windows: store <2 x i64> <i64 1, i64 2>, ptr [[A]]
41+
// CHECK: musttail call {{.*}}i64 @callee_untuple_1(ptr {{.*}}[[A]])
42+
#[unsafe(no_mangle)]
43+
extern "rust-call" fn caller_untuple_1_const((_,): (Indirect,)) -> u64 {
44+
become callee_untuple_1((Indirect(1, 2, 3, 4),));
45+
}
46+
47+
// CHECK-LABEL: @caller_untuple_2
48+
// CHECK-SAME: (ptr {{.*}}[[A:%.*]], ptr {{.*}}[[B:%.*]])
49+
// CHECK-NEXT: start:
50+
// CHECK-NEXT: musttail call {{.*}}i64 @callee_untuple_2(ptr {{.*}}[[A]], ptr {{.*}}[[B]])
51+
#[unsafe(no_mangle)]
52+
extern "rust-call" fn caller_untuple_2((a, b): (Indirect, Indirect)) -> u64 {
53+
become callee_untuple_2((a, b));
54+
}
55+
56+
// CHECK-LABEL: @caller_untuple_2_swapper
57+
// CHECK-SAME: (ptr {{.*}}[[A:%.*]], ptr {{.*}}[[B:%.*]])
58+
// CHECK: call void @llvm.memcpy.{{.+}}(ptr {{.*}}[[TMP:%.*]], ptr {{.*}}[[A]]
59+
// CHECK: call void @llvm.memcpy.{{.+}}(ptr {{.*}}[[A]], ptr {{.*}}[[B]]
60+
// CHECK: call void @llvm.memcpy.{{.+}}(ptr {{.*}}[[B]], ptr {{.*}}[[TMP]]
61+
// CHECK: musttail call {{.*}}i64 @callee_untuple_2(ptr {{.*}}[[A]], ptr {{.*}}[[B]])
62+
#[unsafe(no_mangle)]
63+
extern "rust-call" fn caller_untuple_2_swapper((a, b): (Indirect, Indirect)) -> u64 {
64+
become callee_untuple_2((b, a));
65+
}
66+
67+
unsafe extern "Rust" {
68+
safe fn opaque(_: u64);
69+
}
70+
71+
#[inline(never)]
72+
#[unsafe(no_mangle)]
73+
extern "rust-call" fn callee_untuple_1((a,): (Indirect,)) -> u64 {
74+
opaque(a.0);
75+
a.0
76+
}
77+
78+
#[inline(never)]
79+
#[unsafe(no_mangle)]
80+
extern "rust-call" fn callee_untuple_2((a, b): (Indirect, Indirect)) -> u64 {
81+
opaque(a.0);
82+
opaque(b.0);
83+
a.0
84+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
//! Regression test for issue <https://github.com/rust-lang/rust/issues/158017>.
2+
//@ run-pass
3+
//@ compile-flags: -C opt-level=3
4+
//@ ignore-backends: gcc
5+
//@ ignore-wasm
6+
//@ ignore-riscv64
7+
8+
#![feature(explicit_tail_calls, unboxed_closures)]
9+
#![expect(incomplete_features)]
10+
11+
#[inline(never)]
12+
fn seed_stack() {
13+
let mut values = [100_u64; 8];
14+
std::hint::black_box(&mut values);
15+
}
16+
17+
#[inline(never)]
18+
extern "rust-call" fn callee((value,): ([u64; 4],)) -> u64 {
19+
value[0]
20+
}
21+
22+
#[inline(never)]
23+
extern "rust-call" fn caller((_,): ([u64; 4],)) -> u64 {
24+
become callee(([5, 6, 7, 8],));
25+
}
26+
27+
fn main() {
28+
seed_stack();
29+
assert_eq!(5, caller(([1, 2, 3, 4],)));
30+
}

0 commit comments

Comments
 (0)