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
41/// The functions a call to which comes back more than once, which is what `setjmp` is and what
42/// gcc's `special_function_p` lists. A name with underscores in front of it is the same function.
43///
44/// Control coming back into the caller a second time needs the caller's frame, so a caller that
45/// makes one of these calls anywhere makes no tail call at all.
46const TWICE: &[&str] = &["setjmp", "sigsetjmp", "savectx", "vfork", "getcontext"];
47
48/// One call [`crate::lower`] built for a `tail_call`, and the pseudos that leave its answer where
49/// the caller's answer goes.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct Tail {
52    /// The call.
53    pub call: mir::Inst,
54    /// The return pseudos behind it, which write nothing and go with the call.
55    pub returns: Vec<mir::Inst>,
56}
57
58/// Why no call in this function can be made in tail position, or `None` when one can.
59#[must_use]
60pub fn refusal(func: &Func, names: &Interner) -> Option<&'static str> {
61    if func.attrs.set.contains(AttrSet::NAKED) {
62        return Some("the function is naked and writes its own ending");
63    }
64    let sret =
65        func.signature().params.first().is_some_and(|param| matches!(param.abi, Abi::Sret { .. }));
66    if sret {
67        return Some("the function gives its answer back through memory it was handed");
68    }
69    for block in func.blocks() {
70        for inst in func.insts(block) {
71            match func[inst].opcode {
72                Opcode::Alloca => return Some("a local lives in the frame"),
73                Opcode::VaStart => return Some("the function reads its own variable arguments"),
74                Opcode::ApplyArgs => return Some("the function keeps its arguments in the frame"),
75                Opcode::SetjmpMarker => return Some("the function saves a place to come back to"),
76                Opcode::Call if twice(func, inst, names) => {
77                    return Some("the function calls something that comes back twice");
78                }
79                _ => (),
80            }
81        }
82    }
83    None
84}
85
86/// Whether that call is to one of [`TWICE`].
87fn twice(func: &Func, inst: Inst, names: &Interner) -> bool {
88    let Extra::Call(info) = func[inst].extra else { return false };
89    let Some(callee) = func[info].callee else { return false };
90    TWICE.contains(&names.resolve(callee).trim_start_matches('_'))
91}
92
93/// Turns every call in tail position into a `tail_call`, unless [`refusal`] has a reason not to,
94/// and says how many it turned.
95pub fn mark(func: &mut Func, names: &Interner) -> usize {
96    if refusal(func, names).is_some() {
97        return 0;
98    }
99    let blocks: Vec<_> = func.blocks().collect();
100    let mut marked = 0;
101    for block in blocks {
102        let insts: Vec<Inst> = func.insts(block).collect();
103        let [.., call, ret] = insts[..] else { continue };
104        if !in_tail_position(func, call, ret) {
105            continue;
106        }
107        func.remove_inst(ret);
108        func[call].opcode = Opcode::TailCall;
109        marked += 1;
110    }
111    marked
112}
113
114/// Whether that call, straight in front of that instruction, is one the caller could jump to.
115///
116/// A direct call, because the address of an indirect one is in a register and the epilogue may put
117/// something else back in it. The `return` gives back the call's results, in order, and nothing
118/// else, and the two signatures say the same about them, so the callee leaves the answer where the
119/// caller's caller looks and in the form it expects.
120fn in_tail_position(func: &Func, call: Inst, ret: Inst) -> bool {
121    if func[ret].opcode != Opcode::Return || func[call].opcode != Opcode::Call {
122        return false;
123    }
124    let Extra::Call(info) = func[call].extra else { return false };
125    let info = func[info];
126    if info.callee.is_none() {
127        return false;
128    }
129    let results: Vec<Value> = func[call].results().collect();
130    if func[func[ret].args] != results[..] {
131        return false;
132    }
133    func[info.signature].returns == func.signature().returns
134}
135
136/// Turns each [`Tail`] that can be into the epilogue and a jump, and says how many it turned.
137///
138/// Nothing happens on a machine with no jump to a name, which is what [`FrameInsts::away`] says.
139pub fn jumps(
140    func: &mut mir::Func,
141    tails: &[Tail],
142    insts: &FrameInsts,
143    names: &mut Interner,
144) -> usize {
145    let Some(away) = insts.away else { return 0 };
146    let ret = mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.ret)));
147    let away = mir::Opcode::new(names.intern(&format!("{}{away}", insts.prefix)));
148    let mut jumped = 0;
149    for tail in tails {
150        let Some((last, callee)) = ending(func, tail, ret) else { continue };
151        let span = func.span(tail.call);
152        func.remove_inst(tail.call);
153        for &pseudo in &tail.returns {
154            func.remove_inst(pseudo);
155        }
156        // The same instruction rather than a new one, so the unwind rows the epilogue hung on the
157        // `ret` stay where they were: the frame is in the same state at the jump as it was there.
158        func[last].opcode = away;
159        func[last].symbol = Some(callee);
160        func.set_span(last, span);
161        jumped += 1;
162    }
163    jumped
164}
165
166/// The `ret` the tail's block ends in and the name it calls, when everything between the call and
167/// the `ret` can run before the callee does.
168///
169/// That is the return pseudos, which are nothing, and the epilogue, which puts back registers the
170/// callee saves for itself and moves the stack pointer. Anything that reads or writes a register
171/// the call names could be moving an argument or reading the answer, and anything with a name on
172/// it could be a call, so either keeps the call.
173fn ending(func: &mir::Func, tail: &Tail, ret: mir::Opcode) -> Option<(mir::Inst, Symbol)> {
174    let block = func.block_of(tail.call)?;
175    let callee = func[tail.call].symbol?;
176    if !func[block].succs.is_empty() {
177        return None;
178    }
179    let last = func.insts(block).last()?;
180    if func[last].opcode != ret {
181        return None;
182    }
183    let named: Vec<(mir::Reg, RegClass)> =
184        func[func[tail.call].operands].iter().map(|operand| (operand.reg, operand.class)).collect();
185    let mut at = func.next_inst(tail.call)?;
186    while at != last {
187        if !tail.returns.contains(&at) {
188            let data = &func[at];
189            let touches = func[data.operands]
190                .iter()
191                .any(|operand| named.contains(&(operand.reg, operand.class)));
192            if touches || data.symbol.is_some() {
193                return None;
194            }
195        }
196        at = func.next_inst(at)?;
197    }
198    Some((last, callee))
199}
200
201#[cfg(test)]
202mod tests {
203    use rucc_base::Interner;
204    use rucc_ir::{Block, Builder, Flags, Func, InstData, Opcode, Signature, Type, Value};
205
206    use super::{mark, refusal};
207
208    /// `int f(int a) { return g(a); }`, and whatever `between` puts in front of the return.
209    fn caller(
210        names: &mut Interner,
211        between: impl FnOnce(&mut Func, Block, Value) -> Value,
212    ) -> Func {
213        let i32 = Type::int(32);
214        let mut func =
215            Func::new(names.intern("f"), Signature::new().with_params(&[i32]).with_returns(&[i32]));
216        let block = func.create_block();
217        let arg = func.append_param(block, i32);
218        let sig = func.add_signature(Signature::new().with_params(&[i32]).with_returns(&[i32]));
219        let callee = names.intern("g");
220        let call = Builder::new(&mut func, block).call(callee, sig, &[arg]);
221        let got = func[call].first_result.expect("an integer comes back");
222        let answer = between(&mut func, block, got);
223        Builder::new(&mut func, block).ret(&[answer]);
224        func
225    }
226
227    fn opcodes(func: &Func) -> Vec<Opcode> {
228        func.blocks()
229            .flat_map(|block| func.insts(block).map(|inst| func[inst].opcode).collect::<Vec<_>>())
230            .collect()
231    }
232
233    #[test]
234    fn a_call_whose_answer_is_returned_becomes_a_tail_call() {
235        let mut names = Interner::new();
236        let mut func = caller(&mut names, |_, _, got| got);
237        assert_eq!(mark(&mut func, &names), 1);
238        assert_eq!(opcodes(&func), [Opcode::TailCall]);
239    }
240
241    #[test]
242    fn a_call_with_work_after_it_is_not_in_tail_position() {
243        let mut names = Interner::new();
244        let mut func = caller(&mut names, |func, block, got| {
245            Builder::new(func, block).binary(Opcode::Add, got, got, Flags::default())
246        });
247        assert_eq!(mark(&mut func, &names), 0);
248        assert_eq!(opcodes(&func), [Opcode::Call, Opcode::Add, Opcode::Return]);
249    }
250
251    #[test]
252    fn a_local_in_the_frame_turns_down_the_whole_function() {
253        let mut names = Interner::new();
254        let mut func = caller(&mut names, |func, block, got| {
255            Builder::new(func, block).value(InstData::new(Opcode::Alloca), Type::PTR);
256            got
257        });
258        assert_eq!(refusal(&func, &names), Some("a local lives in the frame"));
259        assert_eq!(mark(&mut func, &names), 0);
260    }
261
262    #[test]
263    fn a_call_to_setjmp_anywhere_turns_down_the_whole_function() {
264        let mut names = Interner::new();
265        let setjmp = names.intern("_setjmp");
266        let mut func = caller(&mut names, |func, block, got| {
267            let sig = func.add_signature(Signature::new().with_returns(&[Type::int(32)]));
268            Builder::new(func, block).call(setjmp, sig, &[]);
269            got
270        });
271        assert_eq!(
272            refusal(&func, &names),
273            Some("the function calls something that comes back twice")
274        );
275        assert_eq!(mark(&mut func, &names), 0);
276    }
277}