Skip to content

Commit fc79542

Browse files
committed
guaranteed tail calls: support indirect arguments
1 parent 9b81629 commit fc79542

5 files changed

Lines changed: 257 additions & 70 deletions

File tree

compiler/rustc_codegen_ssa/src/mir/block.rs

Lines changed: 99 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,19 +1140,70 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
11401140
(args, None)
11411141
};
11421142

1143+
// Special logic for tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments.
1144+
//
1145+
// Normally an indirect argument with `on_stack: false` would be passed as a pointer into
1146+
// the caller's stack frame. For tail calls, that would be unsound, because the caller's
1147+
// stack frame is overwritten by the callee's stack frame.
1148+
//
1149+
// Therefore we store the argument for the callee in the corresponding caller's slot.
1150+
// Because guaranteed tail calls demand that the caller's signature matches the callee's,
1151+
// the corresponding slot has the correct type.
1152+
//
1153+
// To handle cases like the one below, the tail call arguments must first be copied to a
1154+
// temporary, and only then copied to the caller's argument slots.
1155+
//
1156+
// ```
1157+
// // A struct big enough that it is not passed via registers.
1158+
// pub struct Big([u64; 4]);
1159+
//
1160+
// fn swapper(a: Big, b: Big) -> (Big, Big) {
1161+
// become swapper_helper(b, a);
1162+
// }
1163+
// ```
1164+
let mut caller_indirect_arg_places = Vec::new();
1165+
if kind == CallKind::Tail {
1166+
let mut temporaries = vec![None; first_args.len()];
1167+
caller_indirect_arg_places = vec![None; first_args.len()];
1168+
1169+
// First copy the arguments of this call to temporary stack allocations.
1170+
for (i, arg) in first_args.iter().enumerate() {
1171+
if !matches!(fn_abi.args[i].mode, PassMode::Indirect { on_stack: false, .. }) {
1172+
continue;
1173+
}
1174+
1175+
let mut op = self.codegen_operand(bx, &arg.node);
1176+
1177+
let tmp = PlaceRef::alloca(bx, op.layout);
1178+
bx.lifetime_start(tmp.val.llval, tmp.layout.size);
1179+
op.store_with_annotation(bx, tmp);
1180+
1181+
op.val = Ref(tmp.val);
1182+
temporaries[i] = Some(tmp);
1183+
}
1184+
1185+
// Then actually copy them into the corresponding argument place of our caller.
1186+
for (i, opt_tmp) in temporaries.into_iter().enumerate() {
1187+
let Some(tmp) = opt_tmp else { continue };
1188+
1189+
let local = self.mir.args_iter().nth(i).unwrap();
1190+
1191+
let dst = match &self.locals[local] {
1192+
LocalRef::Place(p) => *p,
1193+
_ => todo!(),
1194+
};
1195+
caller_indirect_arg_places[i] = Some(dst);
1196+
1197+
bx.typed_place_copy(dst.val, tmp.val, fn_abi.args[i].layout);
1198+
bx.lifetime_end(tmp.val.llval, tmp.layout.size);
1199+
}
1200+
}
1201+
11431202
// When generating arguments we sometimes introduce temporary allocations with lifetime
11441203
// that extend for the duration of a call. Keep track of those allocations and their sizes
11451204
// to generate `lifetime_end` when the call returns.
11461205
let mut lifetime_ends_after_call: Vec<(Bx::Value, Size)> = Vec::new();
11471206
'make_args: for (i, arg) in first_args.iter().enumerate() {
1148-
if kind == CallKind::Tail && matches!(fn_abi.args[i].mode, PassMode::Indirect { .. }) {
1149-
// FIXME: https://github.com/rust-lang/rust/pull/144232#discussion_r2218543841
1150-
span_bug!(
1151-
fn_span,
1152-
"arguments using PassMode::Indirect are currently not supported for tail calls"
1153-
);
1154-
}
1155-
11561207
let mut op = self.codegen_operand(bx, &arg.node);
11571208

11581209
if let (0, Some(ty::InstanceKind::Virtual(_, idx))) = (i, instance.map(|i| i.def)) {
@@ -1203,18 +1254,47 @@ impl<'a, 'tcx, Bx: BuilderMethods<'a, 'tcx>> FunctionCx<'a, 'tcx, Bx> {
12031254
}
12041255
}
12051256

1206-
// The callee needs to own the argument memory if we pass it
1207-
// by-ref, so make a local copy of non-immediate constants.
1208-
match (&arg.node, op.val) {
1209-
(&mir::Operand::Copy(_), Ref(PlaceValue { llextra: None, .. }))
1210-
| (&mir::Operand::Constant(_), Ref(PlaceValue { llextra: None, .. })) => {
1211-
let tmp = PlaceRef::alloca(bx, op.layout);
1212-
bx.lifetime_start(tmp.val.llval, tmp.layout.size);
1213-
op.store_with_annotation(bx, tmp);
1214-
op.val = Ref(tmp.val);
1215-
lifetime_ends_after_call.push((tmp.val.llval, tmp.layout.size));
1257+
match kind {
1258+
CallKind::Tail => {
1259+
match fn_abi.args[i].mode {
1260+
PassMode::Indirect { on_stack: false, .. } => {
1261+
let Some(dst) = caller_indirect_arg_places[i] else {
1262+
span_bug!(fn_span, "missing caller place for tail call arg {i}");
1263+
};
1264+
1265+
// The argument has been stored in the caller's argument place above.
1266+
// Now forward that place to the callee.
1267+
op.val = Ref(dst.val);
1268+
}
1269+
PassMode::Indirect { on_stack: true, .. } => {
1270+
// FIXME: some LLVM backends (notably x86) do not correctly pass byval
1271+
// arguments to tail calls (as of LLVM 21). See also:
1272+
//
1273+
// - https://github.com/rust-lang/rust/pull/144232#discussion_r2218543841
1274+
// - https://github.com/rust-lang/rust/issues/144855
1275+
span_bug!(
1276+
fn_span,
1277+
"arguments using PassMode::Indirect {{ on_stack: true, .. }} are currently not supported for tail calls"
1278+
)
1279+
}
1280+
_ => (),
1281+
}
1282+
}
1283+
CallKind::Normal => {
1284+
// The callee needs to own the argument memory if we pass it
1285+
// by-ref, so make a local copy of non-immediate constants.
1286+
match (&arg.node, op.val) {
1287+
(&mir::Operand::Copy(_), Ref(PlaceValue { llextra: None, .. }))
1288+
| (&mir::Operand::Constant(_), Ref(PlaceValue { llextra: None, .. })) => {
1289+
let tmp = PlaceRef::alloca(bx, op.layout);
1290+
bx.lifetime_start(tmp.val.llval, tmp.layout.size);
1291+
op.store_with_annotation(bx, tmp);
1292+
op.val = Ref(tmp.val);
1293+
lifetime_ends_after_call.push((tmp.val.llval, tmp.layout.size));
1294+
}
1295+
_ => {}
1296+
}
12161297
}
1217-
_ => {}
12181298
}
12191299

12201300
self.codegen_argument(

compiler/rustc_mir_transform/src/deduce_param_attrs.rs

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,29 @@ impl<'tcx> Visitor<'tcx> for DeduceParamAttrs {
121121
// `f` passes. Note that function arguments are the only situation in which this problem can
122122
// arise: every other use of `move` in MIR doesn't actually write to the value it moves
123123
// from.
124-
if let TerminatorKind::Call { ref args, .. } = terminator.kind {
125-
for arg in args {
126-
if let Operand::Move(place) = arg.node
127-
&& !place.is_indirect_first_projection()
128-
&& let Some(i) = self.as_param(place.local)
129-
{
130-
self.usage[i] |= UsageSummary::MUTATE;
131-
self.usage[i] |= UsageSummary::CAPTURE;
124+
match terminator.kind {
125+
TerminatorKind::Call { ref args, .. } => {
126+
for arg in args {
127+
if let Operand::Move(place) = arg.node
128+
&& !place.is_indirect_first_projection()
129+
&& let Some(i) = self.as_param(place.local)
130+
{
131+
self.usage[i] |= UsageSummary::MUTATE;
132+
self.usage[i] |= UsageSummary::CAPTURE;
133+
}
132134
}
133135
}
134-
};
136+
137+
// Like a call, but more conservative because the backend may introduce writes to an
138+
// argument if the argument is passed as `PassMode::Indirect { on_stack: false, ... }`.
139+
TerminatorKind::TailCall { .. } => {
140+
for usage in self.usage.iter_mut() {
141+
*usage |= UsageSummary::MUTATE;
142+
*usage |= UsageSummary::CAPTURE;
143+
}
144+
}
145+
_ => {}
146+
}
135147

136148
self.super_terminator(terminator, location);
137149
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//@ add-minicore
2+
//@ assembly-output: emit-asm
3+
//@ compile-flags: -Copt-level=3 -C llvm-args=-x86-asm-syntax=intel -C target-cpu=x86-64
4+
5+
#![feature(no_core, explicit_tail_calls)]
6+
#![expect(incomplete_features)]
7+
#![no_core]
8+
#![crate_type = "lib"]
9+
10+
// Test tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments.
11+
//
12+
// Normally an indirect argument with `on_stack: false` would be passed as a pointer to the
13+
// caller's stack frame. For tail calls, that would be unsound, because the caller's stack
14+
// frame is overwritten by the callee's stack frame.
15+
//
16+
// The solution is to write the argument into the caller's argument place (stored somewhere further
17+
// up the stack), and forward that place.
18+
19+
extern crate minicore;
20+
use minicore::*;
21+
22+
#[repr(C)]
23+
struct S {
24+
x: u64,
25+
y: u64,
26+
z: u64,
27+
}
28+
29+
unsafe extern "C" {
30+
safe fn force_usage(_: u64, _: u64, _: u64) -> u64;
31+
}
32+
33+
// CHECK-LABEL: callee:
34+
// CHECK-NEXT: .cfi_startproc
35+
//
36+
// CHECK-NEXT: mov rax, qword ptr [rdi]
37+
// CHECK-NEXT: mov rsi, qword ptr [rdi + 8]
38+
// CHECK-NEXT: mov rdx, qword ptr [rdi + 16]
39+
// CHECK-NEXT: mov rdi, rax
40+
//
41+
// CHECK-NEXT: jmp qword ptr [rip + force_usage@GOTPCREL]
42+
#[inline(never)]
43+
#[unsafe(no_mangle)]
44+
fn callee(s: S) -> u64 {
45+
force_usage(s.x, s.y, s.z)
46+
}
47+
48+
// CHECK-LABEL: caller1:
49+
// CHECK-NEXT: .cfi_startproc
50+
//
51+
// Just forward the argument:
52+
//
53+
// CHECK-NEXT: jmp qword ptr [rip + callee@GOTPCREL]
54+
#[unsafe(no_mangle)]
55+
fn caller1(s: S) -> u64 {
56+
become callee(s);
57+
}
58+
59+
// CHECK-LABEL: caller2:
60+
// CHECK-NEXT: .cfi_startproc
61+
//
62+
// Construct the S value directly into the argument slot:
63+
//
64+
// CHECK-NEXT: mov qword ptr [rdi], 1
65+
// CHECK-NEXT: mov qword ptr [rdi + 8], 2
66+
// CHECK-NEXT: mov qword ptr [rdi + 16], 3
67+
//
68+
// CHECK-NEXT: jmp qword ptr [rip + callee@GOTPCREL]
69+
#[unsafe(no_mangle)]
70+
fn caller2(_: S) -> u64 {
71+
let s = S { x: 1, y: 2, z: 3 };
72+
become callee(s);
73+
}

tests/crashes/144293-indirect-ops-llvm.rs

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//@ run-pass
2+
//@ ignore-backends: gcc
3+
#![feature(explicit_tail_calls)]
4+
#![expect(incomplete_features)]
5+
6+
// Test tail calls with `PassMode::Indirect { on_stack: false, .. }` arguments.
7+
//
8+
// Normally an indirect argument with `on_stack: false` would be passed as a pointer to the
9+
// caller's stack frame. For tail calls, that would be unsound, because the caller's stack
10+
// frame is overwritten by the callee's stack frame.
11+
//
12+
// The solution is to write the argument into the caller's argument place (stored somewhere further
13+
// up the stack), and forward that place.
14+
15+
// A struct big enough that it is not passed via registers, so that the rust calling convention uses
16+
// `Indirect { on_stack: false, .. }`.
17+
#[repr(C)]
18+
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
19+
pub struct Big([u64; 4]);
20+
21+
#[inline(never)]
22+
fn f(y: Big) -> u64 {
23+
let Big([y0, ..]) = y;
24+
let x = Big([y0, 2, 3, 4]);
25+
26+
become g(x)
27+
}
28+
29+
#[inline(never)]
30+
fn g(x: Big) -> u64 {
31+
let Big([a, b, c, d]) = x;
32+
a + b + c + d
33+
}
34+
35+
#[inline(never)]
36+
fn swapper(a: Big, b: Big) -> (Big, Big) {
37+
become swapper_helper(b, a);
38+
}
39+
40+
#[inline(never)]
41+
fn swapper_helper(a: Big, b: Big) -> (Big, Big) {
42+
(a, b)
43+
}
44+
45+
#[inline(never)]
46+
fn swapper_derived(a: Big, _: (u64, u64), b: Big, _: (u64, u64)) -> ((u64, u64), (u64, u64)) {
47+
become swapper_derived_helper(b, (a.0[0], b.0[0]), a, (a.0[0], b.0[0]));
48+
}
49+
50+
#[inline(never)]
51+
fn swapper_derived_helper(
52+
_: Big,
53+
x: (u64, u64),
54+
_: Big,
55+
y: (u64, u64),
56+
) -> ((u64, u64), (u64, u64)) {
57+
(x, y)
58+
}
59+
60+
fn main() {
61+
assert_eq!(f(Big::default()), 0 + 2 + 3 + 4);
62+
assert_eq!(swapper(Big([1; 4]), Big([2; 4])), (Big([2; 4]), Big([1; 4])));
63+
assert_eq!(swapper_derived(Big([1; 4]), (0, 0), Big([2; 4]), (0, 0)), ((1, 2), (1, 2)));
64+
}

0 commit comments

Comments
 (0)