Skip to main content

rucc_codegen/
tail.rs

1//! A call in tail position, as a jump once the frame is given back.
2//!
3//! Design: `spec/optimizer/25-tail-calls.md` section 25.2, the sibling call.
4//!
5//! `return f(x)` needs nothing of the caller once `f` is running, so the caller can give its frame
6//! back first and jump to `f`, and `f` returns straight to whoever called the caller. That saves a
7//! call and a return, and it makes a chain of calls like that run in the same stack however long
8//! it is, which is what a state machine written as functions calling each other relies on. gcc does
9//! it at `-O2` and `-Os`, and so does this.
10//!
11//! # Three places
12//!
13//! [`mark`] works on the IR, before selection. It turns a direct call whose results are exactly
14//! what the block returns next into a `tail_call`, which ends the block the way a `return` did, and
15//! it turns down the whole function when the callee could see something of the caller's frame. Its
16//! answer is what [`refusal`] says, a reason rather than a no.
17//!
18//! [`crate::lower`] builds a `tail_call` as the call and the return it stands for, and writes the
19//! call down as a [`Tail`] when the convention put every argument in a register. A call that needs
20//! the argument area is left as the call it was: the area is the bottom of this function's frame,
21//! and the frame is gone by the time the callee would read it.
22//!
23//! [`jumps`] runs last, on machine code with every register handed out and the epilogue written.
24//! Each [`Tail`] whose block goes straight from the call to the epilogue, with nothing in between
25//! touching a register the call reads or writes, loses its call, and the `ret` at the end becomes a
26//! `jmp` to the callee. Anything else stays a call and a `ret`, which is right, just not as short.
27//!
28//! # Why the frame check is this strict
29//!
30//! The one thing that makes a tail call wrong is a pointer into the frame that is given back, and
31//! the section above asks for "provably not reachable" rather than "not known to be". The only
32//! addresses into a frame the IR has are the ones an `alloca` makes, so a function with no `alloca`
33//! has nothing a pointer could point at, and that is the rule. It turns down functions an escape
34//! analysis would let through, which costs a few calls and nothing else.
35
36use rucc_base::{Interner, Symbol};
37use rucc_ir::{Abi, AttrSet, Extra, Func, Inst, Opcode, Value};
38use rucc_mir as mir;
39use rucc_target::{FrameInsts, RegClass};
40
41use crate::elsewhere::Elsewhere;
42
43/// One call [`crate::lower`] built for a `tail_call`, and the pseudos that leave its answer where
44/// the caller's answer goes.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Tail {
47    /// The call.
48    pub call: mir::Inst,
49    /// The return pseudos behind it, which write nothing and go with the call.
50    pub returns: Vec<mir::Inst>,
51}
52
53/// Why no call in this function can be made in tail position, or `None` when one can.
54///
55/// Control coming back into the caller a second time needs the caller's frame, so a caller that
56/// calls something that comes back twice anywhere makes no tail call at all. Which calls those are
57/// is [`comes_back`]'s rule, and the inliner asks the same one of a callee.
58#[must_use]
59pub fn refusal(func: &Func, names: &Interner, elsewhere: &Elsewhere) -> Option<&'static str> {
60    if func.attrs.set.contains(AttrSet::NAKED) {
61        return Some("the function is naked and writes its own ending");
62    }
63    let sret =
64        func.signature().params.first().is_some_and(|param| matches!(param.abi, Abi::Sret { .. }));
65    if sret {
66        return Some("the function gives its answer back through memory it was handed");
67    }
68    for block in func.blocks() {
69        for inst in func.insts(block) {
70            match func[inst].opcode {
71                Opcode::Alloca => return Some("a local lives in the frame"),
72                Opcode::VaStart => return Some("the function reads its own variable arguments"),
73                Opcode::ApplyArgs => return Some("the function keeps its arguments in the frame"),
74                Opcode::SetjmpMarker => return Some("the function saves a place to come back to"),
75                Opcode::Call if twice(func, inst, names, elsewhere) => {
76                    return Some("the function calls something that comes back twice");
77                }
78                _ => (),
79            }
80        }
81    }
82    None
83}
84
85/// Whether control can come back into this function a second time from one call, through a
86/// `__builtin_setjmp` or a call to one of the names [`rucc_ir::twice_by_name`] knows or to a
87/// function declared `returns_twice`.
88///
89/// The frame of such a function is laid out with nothing sharing anything. A value computed before
90/// the `setjmp` and read after the `longjmp` is live across the call on the arm that reads it, and
91/// dead on the arm that ran first, so liveness lets that arm reuse its spill slot. Registers are
92/// safe, since the jump puts back the callee saved registers the `setjmp` wrote down, but a spill
93/// slot is only memory and comes back holding whatever the first arm left in it. 7.13.2.1p3 only
94/// lets a local that changed after the `setjmp` come back indeterminate, and this one did not.
95/// Postgres's `PG_TRY` is `sigsetjmp` with exactly this shape, and gcc answers the same way by
96/// giving nothing live across such a call a slot it shares.
97#[must_use]
98pub fn comes_back(func: &Func, names: &Interner, elsewhere: &Elsewhere) -> bool {
99    func.blocks().any(|block| {
100        func.insts(block).any(|inst| match func[inst].opcode {
101            Opcode::SetjmpMarker => true,
102            Opcode::Call => twice(func, inst, names, elsewhere),
103            _ => false,
104        })
105    })
106}
107
108/// Whether that call is to one of the names [`rucc_ir::twice_by_name`] knows or to a function
109/// declared to be like them.
110fn twice(func: &Func, inst: Inst, names: &Interner, elsewhere: &Elsewhere) -> bool {
111    let Extra::Call(info) = func[inst].extra else { return false };
112    let Some(callee) = func[info].callee else { return false };
113    elsewhere.twice(callee) || rucc_ir::twice_by_name(names.resolve(callee))
114}
115
116/// Turns every call in tail position into a `tail_call`, unless [`refusal`] has a reason not to,
117/// and says how many it turned.
118pub fn mark(func: &mut Func, names: &Interner, elsewhere: &Elsewhere) -> usize {
119    if refusal(func, names, elsewhere).is_some() {
120        return 0;
121    }
122    let blocks: Vec<_> = func.blocks().collect();
123    let mut marked = 0;
124    for block in blocks {
125        let insts: Vec<Inst> = func.insts(block).collect();
126        let [.., call, ret] = insts[..] else { continue };
127        if !in_tail_position(func, call, ret) {
128            continue;
129        }
130        func.remove_inst(ret);
131        func[call].opcode = Opcode::TailCall;
132        marked += 1;
133    }
134    marked
135}
136
137/// Whether that call, straight in front of that instruction, is one the caller could jump to.
138///
139/// A direct call, because the address of an indirect one is in a register and the epilogue may put
140/// something else back in it. The `return` gives back the call's results, in order, and nothing
141/// else, and the two signatures say the same about them, so the callee leaves the answer where the
142/// caller's caller looks and in the form it expects.
143fn in_tail_position(func: &Func, call: Inst, ret: Inst) -> bool {
144    if func[ret].opcode != Opcode::Return || func[call].opcode != Opcode::Call {
145        return false;
146    }
147    let Extra::Call(info) = func[call].extra else { return false };
148    let info = func[info];
149    if info.callee.is_none() {
150        return false;
151    }
152    let results: Vec<Value> = func[call].results().collect();
153    if func[func[ret].args] != results[..] {
154        return false;
155    }
156    // And the two are of one convention. A jump leaves the callee to return straight to this
157    // function's caller, who restores what its own convention says the callee kept. An `ms_abi`
158    // function jumping to an ordinary one on Linux would hand back `rsi`, `rdi` and the upper
159    // vector registers as the SysV callee left them, which its caller counted on it keeping, and
160    // the arguments would be in the other registers besides. gcc makes no sibling call across
161    // the difference either.
162    let callee = &func[info.signature];
163    callee.convention == func.signature().convention && callee.returns == func.signature().returns
164}
165
166/// Turns each [`Tail`] that can be into the epilogue and a jump, and says how many it turned.
167///
168/// Nothing happens on a machine with no jump to a name, which is what [`FrameInsts::away`] says.
169pub fn jumps(
170    func: &mut mir::Func,
171    tails: &[Tail],
172    insts: &FrameInsts,
173    names: &mut Interner,
174) -> usize {
175    let Some(away) = insts.away else { return 0 };
176    let ret = mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.ret)));
177    let away = mir::Opcode::new(names.intern(&format!("{}{away}", insts.prefix)));
178    let mut jumped = 0;
179    for tail in tails {
180        let Some((last, callee)) = ending(func, tail, ret) else { continue };
181        let span = func.span(tail.call);
182        func.remove_inst(tail.call);
183        for &pseudo in &tail.returns {
184            func.remove_inst(pseudo);
185        }
186        // The same instruction rather than a new one, so the unwind rows the epilogue hung on the
187        // `ret` stay where they were: the frame is in the same state at the jump as it was there.
188        func[last].opcode = away;
189        func[last].symbol = Some(callee);
190        func.set_span(last, span);
191        jumped += 1;
192    }
193    jumped
194}
195
196/// The `ret` the tail's block ends in and the name it calls, when everything between the call and
197/// the `ret` can run before the callee does.
198///
199/// That is the return pseudos, which are nothing, and the epilogue, which puts back registers the
200/// callee saves for itself and moves the stack pointer. Anything that reads or writes a register
201/// the call names could be moving an argument or reading the answer, and anything with a name on
202/// it could be a call, so either keeps the call.
203fn ending(func: &mir::Func, tail: &Tail, ret: mir::Opcode) -> Option<(mir::Inst, Symbol)> {
204    let block = func.block_of(tail.call)?;
205    let callee = func[tail.call].symbol?;
206    if !func[block].succs.is_empty() {
207        return None;
208    }
209    let last = func.insts(block).last()?;
210    if func[last].opcode != ret {
211        return None;
212    }
213    let named: Vec<(mir::Reg, RegClass)> =
214        func[func[tail.call].operands].iter().map(|operand| (operand.reg, operand.class)).collect();
215    let mut at = func.next_inst(tail.call)?;
216    while at != last {
217        if !tail.returns.contains(&at) {
218            let data = &func[at];
219            let touches = func[data.operands]
220                .iter()
221                .any(|operand| named.contains(&(operand.reg, operand.class)));
222            if touches || data.symbol.is_some() {
223                return None;
224            }
225        }
226        at = func.next_inst(at)?;
227    }
228    Some((last, callee))
229}
230
231#[cfg(test)]
232mod tests {
233    use rucc_base::Interner;
234    use rucc_ir::{Block, Builder, Flags, Func, InstData, Opcode, Signature, Type, Value};
235
236    use super::{comes_back, mark, refusal};
237    use crate::elsewhere::Elsewhere;
238
239    /// `int f(int a) { return g(a); }`, and whatever `between` puts in front of the return.
240    fn caller(
241        names: &mut Interner,
242        between: impl FnOnce(&mut Func, Block, Value) -> Value,
243    ) -> Func {
244        caller_of(names, rucc_target::Convention::Target, between)
245    }
246
247    /// The same caller, with `g` declared in the convention given.
248    fn caller_of(
249        names: &mut Interner,
250        convention: rucc_target::Convention,
251        between: impl FnOnce(&mut Func, Block, Value) -> Value,
252    ) -> Func {
253        let i32 = Type::int(32);
254        let mut func =
255            Func::new(names.intern("f"), Signature::new().with_params(&[i32]).with_returns(&[i32]));
256        let block = func.create_block();
257        let arg = func.append_param(block, i32);
258        let mut called = Signature::new().with_params(&[i32]).with_returns(&[i32]);
259        called.convention = convention;
260        let sig = func.add_signature(called);
261        let callee = names.intern("g");
262        let call = Builder::new(&mut func, block).call(callee, sig, &[arg]);
263        let got = func[call].first_result.expect("an integer comes back");
264        let answer = between(&mut func, block, got);
265        Builder::new(&mut func, block).ret(&[answer]);
266        func
267    }
268
269    fn opcodes(func: &Func) -> Vec<Opcode> {
270        func.blocks()
271            .flat_map(|block| func.insts(block).map(|inst| func[inst].opcode).collect::<Vec<_>>())
272            .collect()
273    }
274
275    /// A jump to a function of the other convention would leave the caller's caller with what
276    /// the callee did not keep, so the call stays a call.
277    #[test]
278    fn a_call_to_the_other_convention_stays_a_call() {
279        let mut names = Interner::new();
280        let mut func = caller_of(&mut names, rucc_target::Convention::Ms, |_, _, got| got);
281        assert_eq!(mark(&mut func, &names, &Elsewhere::default()), 0);
282        assert_eq!(opcodes(&func), [Opcode::Call, Opcode::Return]);
283    }
284
285    #[test]
286    fn a_call_whose_answer_is_returned_becomes_a_tail_call() {
287        let mut names = Interner::new();
288        let mut func = caller(&mut names, |_, _, got| got);
289        assert_eq!(mark(&mut func, &names, &Elsewhere::default()), 1);
290        assert_eq!(opcodes(&func), [Opcode::TailCall]);
291    }
292
293    #[test]
294    fn a_call_with_work_after_it_is_not_in_tail_position() {
295        let mut names = Interner::new();
296        let mut func = caller(&mut names, |func, block, got| {
297            Builder::new(func, block).binary(Opcode::Add, got, got, Flags::default())
298        });
299        assert_eq!(mark(&mut func, &names, &Elsewhere::default()), 0);
300        assert_eq!(opcodes(&func), [Opcode::Call, Opcode::Add, Opcode::Return]);
301    }
302
303    #[test]
304    fn a_local_in_the_frame_turns_down_the_whole_function() {
305        let mut names = Interner::new();
306        let mut func = caller(&mut names, |func, block, got| {
307            Builder::new(func, block).value(InstData::new(Opcode::Alloca), Type::PTR);
308            got
309        });
310        assert_eq!(
311            refusal(&func, &names, &Elsewhere::default()),
312            Some("a local lives in the frame")
313        );
314        assert_eq!(mark(&mut func, &names, &Elsewhere::default()), 0);
315    }
316
317    #[test]
318    fn a_call_to_setjmp_anywhere_turns_down_the_whole_function() {
319        let mut names = Interner::new();
320        let setjmp = names.intern("_setjmp");
321        let mut func = caller(&mut names, |func, block, got| {
322            let sig = func.add_signature(Signature::new().with_returns(&[Type::int(32)]));
323            Builder::new(func, block).call(setjmp, sig, &[]);
324            got
325        });
326        assert_eq!(
327            refusal(&func, &names, &Elsewhere::default()),
328            Some("the function calls something that comes back twice")
329        );
330        assert_eq!(mark(&mut func, &names, &Elsewhere::default()), 0);
331    }
332
333    #[test]
334    fn glibcs_name_for_sigsetjmp_comes_back_and_an_ordinary_call_does_not() {
335        let mut names = Interner::new();
336        let setjmp = names.intern("__sigsetjmp");
337        let func = caller(&mut names, |func, block, got| {
338            let sig = func.add_signature(Signature::new().with_returns(&[Type::int(32)]));
339            Builder::new(func, block).call(setjmp, sig, &[]);
340            got
341        });
342        assert!(comes_back(&func, &names, &Elsewhere::default()));
343        let plain = caller(&mut names, |_, _, got| got);
344        assert!(!comes_back(&plain, &names, &Elsewhere::default()));
345    }
346}