Skip to main content

rucc_codegen/
abi.rs

1//! Where a function's arguments already are when it starts running, and where a call puts its own.
2//!
3//! Design: `spec/12-abi-and-runtime.md`.
4//!
5//! This is the one part of the calling convention that is not a lowering rule, and it is worth
6//! saying why, because everything else in this crate is. A rule matches a term and rewrites it,
7//! and which register the third argument arrives in is not a fact about any term: it depends on
8//! the argument's position and on the classification of every argument before it. A pattern has
9//! nowhere to put that. So the arguments are built here, by hand, out of what the convention
10//! says, the same way [`crate::finish`] builds a prologue.
11//!
12//! The classification itself is not here either. `rucc-lower` has already run it by the time a
13//! function reaches this crate, which is why the parameters read here are plain scalars: an
14//! aggregate has been split into the pieces it travels in, and a return through memory is an
15//! ordinary pointer parameter in front of the rest. What is left for this is the step after
16//! classification, from how a value travels to which register it is actually in, which is
17//! [`rucc_target::Places`].
18//!
19//! # What it writes
20//!
21//! One `x64.arg_val_*` per parameter, at the top of the entry block, each defining a fresh
22//! register constrained to the one the argument arrived in. They encode to nothing. The point of
23//! them is that a parameter has to be defined somewhere for the allocator to have anything to
24//! move, and the entry block cannot define it as a block parameter: there is no edge into the
25//! entry block for the move to go on, which is what `rucc_regalloc::rewrite` asserts.
26//!
27//! What the allocator does with them is the whole of the argument sequence. A parameter that is
28//! read where it arrived costs nothing, and one that is not gets a copy, which is the same
29//! bargain the return already makes and is decided by the same code.
30//!
31//! # A call
32//!
33//! The same reasoning the other way round, and one instruction rather than several. `x64.call` is
34//! the only opcode in the description whose operand vector is empty there, because nothing about
35//! a call's operands is the same from one call to the next, so they are built here: one read per
36//! argument constrained to the register the convention passes it in, one definition for the value
37//! that comes back constrained to the register it comes back in, and one definition per register
38//! the convention does not preserve.
39//!
40//! Those last ones are the clobbers, and they are the whole of what the allocator has to know
41//! about a call besides where the values go. Each is a definition of the physical register itself
42//! rather than of a value, since there is no value: it says the register is written here, which
43//! is exactly what stops the allocator from leaving something in one across the call. A register
44//! an argument or the result already names is not repeated, because naming it once already blocks
45//! it for the length of the instruction, which is all a clobber does.
46//!
47//! What is not here is the bytes an argument past the last register goes in. That is a place in
48//! the frame and no frame exists yet, the same reason a parameter arriving there is reported
49//! rather than read, so a call is asked how many bytes it would need and reports it, and a call
50//! that would need any is turned down for now.
51
52use rucc_base::{Interner, Symbol};
53use rucc_ir::Type;
54use rucc_mir as mir;
55use rucc_target::{CallRegs, Constraint, PhysReg, Places, Where};
56
57/// Why a parameter could not be brought in.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Missing {
60    /// It arrives on the stack, which is somewhere nothing reads from yet: the offset is a
61    /// distance into a frame, and a frame is not worked out until after allocation.
62    OnStack,
63    /// It arrives in a vector register. Every rule in the set is about an integer, so a value in
64    /// one has nothing downstream that could use it.
65    InVector,
66    /// It is a width no pseudo covers, which is anything a machine register does not hold.
67    Width,
68}
69
70impl Missing {
71    /// What it says when a function could not be compiled because of it.
72    ///
73    /// Worded so that it reads the same about a value arriving and a value being passed, since
74    /// the two are the same fact seen from the two ends of one call.
75    #[must_use]
76    pub fn why(self) -> &'static str {
77        match self {
78            Missing::OnStack => "is passed on the stack",
79            Missing::InVector => "is in a vector register",
80            Missing::Width => "is a width no argument register holds",
81        }
82    }
83}
84
85/// Binds a function's parameters to the registers the convention says they arrive in.
86///
87/// The registers come back in the order the parameters were given, so the caller can bind each
88/// IR parameter to the one at its position.
89///
90/// # Errors
91///
92/// The first parameter this cannot bring in, and why. A function with one is reported rather
93/// than compiled, because the alternative is a function that reads an argument from wherever the
94/// last one happened to leave a register.
95pub fn entry(
96    out: &mut mir::Func,
97    block: mir::Block,
98    params: &[Type],
99    conv: &CallRegs,
100    names: &mut Interner,
101) -> Result<Vec<mir::Reg>, (usize, Missing)> {
102    let mut places = Places::new(conv);
103    let mut regs = Vec::with_capacity(params.len());
104    for (index, &ty) in params.iter().enumerate() {
105        // Asking for the place of a parameter that cannot be brought in is still worth doing
106        // before giving up, and it costs nothing, because every place after it depends on it and
107        // a reader stepping through this in a debugger should see the same numbers a working
108        // version would.
109        let at = if ty.is_float() { places.float() } else { places.integer() };
110        if ty.is_float() {
111            return Err((index, Missing::InVector));
112        }
113        let head = head_of(ty).ok_or((index, Missing::Width))?;
114        let Where::Reg(arrived) = at else { return Err((index, Missing::OnStack)) };
115
116        let reg = out.new_vreg(conv.int_class);
117        let opcode = mir::Opcode::new(names.intern(head));
118        let operand = mir::Operand::write(reg, conv.int_class).with(Constraint::Fixed(arrived));
119        out.build(block, opcode).operand(operand).finish();
120        regs.push(reg);
121    }
122    Ok(regs)
123}
124
125/// What the instruction that calls a name is called.
126///
127/// Here rather than in a rule for the same reason the arguments are: a rule pattern sees one term
128/// and a call's operands are whatever the signature made them, so no pattern could name them.
129pub const CALL: &str = "x64.call";
130
131/// What one call came to.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct Made {
134    /// The register the value came back in, or `None` for a call that gives nothing back.
135    pub result: Option<mir::Reg>,
136    /// How many bytes below the stack pointer this call needs for the arguments it passes there.
137    ///
138    /// Not always zero for a call that passes everything in registers: a Windows caller reserves
139    /// thirty two bytes for the callee to spill its register arguments into whether it uses them
140    /// or not, and that reservation is this.
141    pub outgoing: u32,
142}
143
144/// Which of a call's values could not be passed, and why.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct Refused {
147    /// Its position among the arguments, or `None` for the value that comes back.
148    pub argument: Option<usize>,
149    /// What is wrong with where it travels.
150    pub missing: Missing,
151}
152
153/// One call, as everything about it that is not the function it is being built into.
154#[derive(Debug, Clone, Copy)]
155pub struct Calling<'a> {
156    /// The name it calls.
157    pub callee: Symbol,
158    /// What it passes, as the type each value travels as and the register it is in, in the order
159    /// the signature holds them, which is the order the convention places them in.
160    pub args: &'a [(Type, mir::Reg)],
161    /// What comes back, or `None` for a call that gives nothing back.
162    pub returns: Option<Type>,
163    /// Whether the callee takes arguments beyond the ones its signature names, which is what says
164    /// whether it reads the count of vector registers the call passed arguments in.
165    pub variadic: bool,
166}
167
168/// Builds one call: what it passes, what comes back, and what it destroys.
169///
170/// # Errors
171///
172/// The first value this cannot pass, and why, before anything is written. A call with one is
173/// reported rather than compiled, because the alternative is a call that leaves an argument
174/// wherever the last one happened to put a register.
175pub fn call(
176    out: &mut mir::Func,
177    block: mir::Block,
178    made: &Calling<'_>,
179    conv: &CallRegs,
180    names: &mut Interner,
181) -> Result<Made, Refused> {
182    let &Calling { callee, args, returns, variadic } = made;
183    // Where everything goes, worked out before anything is built, so that a call this cannot make
184    // leaves no half of one behind.
185    let mut places = Places::new(conv);
186    let mut passed = Vec::with_capacity(args.len());
187    for (index, &(ty, reg)) in args.iter().enumerate() {
188        let refused = |missing| Refused { argument: Some(index), missing };
189        let at = if ty.is_float() { places.float() } else { places.integer() };
190        if ty.is_float() {
191            return Err(refused(Missing::InVector));
192        }
193        if head_of(ty).is_none() {
194            return Err(refused(Missing::Width));
195        }
196        let Where::Reg(at) = at else { return Err(refused(Missing::OnStack)) };
197        passed.push((reg, at));
198    }
199    let comes_back = match returns {
200        None => None,
201        Some(ty) if ty.is_float() => {
202            return Err(Refused { argument: None, missing: Missing::InVector });
203        }
204        Some(ty) if head_of(ty).is_none() => {
205            return Err(Refused { argument: None, missing: Missing::Width });
206        }
207        // Which register a value comes back in depends on nothing but the value, which is why the
208        // return side of the convention is a rule and this side is not. There is no rule here
209        // because the arguments are in the same instruction.
210        Some(_) => Some(
211            *conv.int_returns.first().ok_or(Refused { argument: None, missing: Missing::Width })?,
212        ),
213    };
214
215    // A variadic callee on SysV reads how many vector registers the call passed arguments in and
216    // skips saving them when the answer is none, which is what makes `printf` with no floating
217    // point argument cheap. It is an obligation rather than an optimization: leaving whatever was
218    // in the register there makes the callee save a register file it was not given. The answer is
219    // zero because a float argument is refused above, and it will stop being a constant on the
220    // day one is not.
221    let counted = if variadic { conv.vector_count } else { None };
222
223    // The definitions first and the reads after, which is the order every operand vector in the
224    // machine IR is in and the order `rucc_mir::defs` counts.
225    let mut operands = Vec::with_capacity(args.len() + conv.int_order.len() + 2);
226    let result = comes_back.map(|at| {
227        let reg = out.new_vreg(conv.int_class);
228        operands.push(mir::Operand::write(reg, conv.int_class).with(Constraint::Fixed(at)));
229        reg
230    });
231    let named: Vec<PhysReg> =
232        comes_back.into_iter().chain(counted).chain(passed.iter().map(|&(_, at)| at)).collect();
233    for &reg in conv.int_order {
234        if !conv.preserves_int(reg) && !named.contains(&reg) {
235            operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.int_class));
236        }
237    }
238    for &reg in conv.sse_order {
239        if !conv.preserves_sse(reg) {
240            operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.sse_class));
241        }
242    }
243    for (reg, at) in passed {
244        operands.push(mir::Operand::read(reg, conv.int_class).with(Constraint::Fixed(at)));
245    }
246    if let Some(at) = counted {
247        let count = out.new_vreg(conv.int_class);
248        let zero = mir::Opcode::new(names.intern("x64.mov_ri_32"));
249        out.build(block, zero).def(count, conv.int_class).imm(0).finish();
250        operands.push(mir::Operand::read(count, conv.int_class).with(Constraint::Fixed(at)));
251    }
252
253    let opcode = mir::Opcode::new(names.intern(CALL));
254    let mut build = out.build(block, opcode).symbol(callee);
255    for operand in operands {
256        build = build.operand(operand);
257    }
258    build.finish();
259    Ok(Made { result, outgoing: places.size() })
260}
261
262/// What the pseudo for an argument of that type is called.
263///
264/// The width is in the name for the same reason it is in every other opcode here: it is what the
265/// instruction is about. Nothing encodes it, so nothing depends on it being right, but a listing
266/// that says an argument arrived and does not say how much of it did is a listing worth less.
267///
268/// Which widths there are is the question the rule set asks of a type, and not a list of its own,
269/// because it has to be the same list. An argument brought in at a width the rules have no name
270/// for is a register
271/// nothing downstream could then read, and a width the rules cover that this refuses is a
272/// function turned away for no reason. Asking one question in one place is what keeps the two
273/// answers from drifting, and an address is what they used to disagree about.
274#[must_use]
275pub fn head_of(ty: Type) -> Option<&'static str> {
276    let names = ["x64.arg_val_8", "x64.arg_val_16", "x64.arg_val_32", "x64.arg_val_64"];
277    Some(names[crate::term::slot(ty)?])
278}
279
280#[cfg(test)]
281mod tests {
282    use rucc_target::x86_64::{REGS, SYSV, WIN64};
283
284    use super::*;
285
286    /// The parameters of a function under a convention, as machine IR text.
287    fn bind(params: &[Type], conv: &CallRegs) -> String {
288        let mut names = Interner::new();
289        let mut out = mir::Func::new(names.intern("f"));
290        let block = out.create_block();
291        entry(&mut out, block, params, conv, &mut names).expect("every parameter arrives");
292        mir::print_func(&out, &names, &REGS)
293    }
294
295    #[test]
296    fn the_first_arguments_arrive_where_the_convention_puts_them() {
297        let i32 = Type::int(32);
298        assert_eq!(
299            bind(&[i32, i32, Type::int(64)], &SYSV),
300            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_32\n    \
301             %1:gpr($rsi) = x64.arg_val_32\n    %2:gpr($rdx) = x64.arg_val_64\n}\n"
302        );
303    }
304
305    #[test]
306    fn the_other_convention_puts_the_same_arguments_somewhere_else() {
307        // The first argument is in `rcx` here and in `rdi` above, which is the difference that
308        // makes a SysV binary calling a Windows one read the wrong value rather than fail.
309        let i64 = Type::int(64);
310        assert_eq!(
311            bind(&[i64, i64], &WIN64),
312            "mfunc @f {\nblock0:\n    %0:gpr($rcx) = x64.arg_val_64\n    \
313             %1:gpr($rdx) = x64.arg_val_64\n}\n"
314        );
315    }
316
317    #[test]
318    fn an_argument_past_the_last_register_is_reported_rather_than_read_from_nowhere() {
319        let i64 = Type::int(64);
320        let mut names = Interner::new();
321        let mut out = mir::Func::new(names.intern("f"));
322        let block = out.create_block();
323        let seven = vec![i64; 7];
324        assert_eq!(entry(&mut out, block, &seven, &SYSV, &mut names), Err((6, Missing::OnStack)));
325        // Six of them still got registers, and the seventh is what stopped it. Windows runs out
326        // three arguments earlier, which is the same answer at a different position.
327        assert_eq!(entry(&mut out, block, &seven, &WIN64, &mut names), Err((4, Missing::OnStack)));
328    }
329
330    #[test]
331    fn an_argument_in_a_vector_register_is_reported_because_nothing_here_uses_one() {
332        let mut names = Interner::new();
333        let mut out = mir::Func::new(names.intern("f"));
334        let block = out.create_block();
335        let params = [Type::int(32), Type::float(rucc_ir::Float::F64)];
336        assert_eq!(entry(&mut out, block, &params, &SYSV, &mut names), Err((1, Missing::InVector)));
337    }
338
339    /// One call to `g`, with a register for each argument arriving in the block that makes it.
340    fn make(
341        args: &[Type],
342        returns: Option<Type>,
343        variadic: bool,
344        conv: &CallRegs,
345    ) -> (Interner, mir::Func, Result<Made, Refused>) {
346        let mut names = Interner::new();
347        let mut out = mir::Func::new(names.intern("f"));
348        let block = out.create_block();
349        let passed: Vec<(Type, mir::Reg)> =
350            args.iter().map(|&ty| (ty, out.append_param(block, conv.int_class))).collect();
351        let callee = names.intern("g");
352        let what = Calling { callee, args: &passed, returns, variadic };
353        let made = call(&mut out, block, &what, conv, &mut names);
354        (names, out, made)
355    }
356
357    /// What the call in that function reads and writes, by register name, in the order the
358    /// operands are in.
359    fn operands(func: &mir::Func) -> (Vec<String>, Vec<String>) {
360        let block = func.entry().expect("a function with a block in it");
361        let call = func.terminator(block).expect("the call is the last thing in the block");
362        let name = |operand: &mir::Operand| match (operand.reg.phys(), operand.constraint) {
363            (Some(reg), _) | (None, Constraint::Fixed(reg)) => {
364                REGS.name(operand.class, reg).expect("a register the file describes").to_string()
365            }
366            _ => format!("{:?}", operand.reg),
367        };
368        let mut written = Vec::new();
369        let mut read = Vec::new();
370        for operand in &func[func[call].operands] {
371            let into = if operand.role == mir::Role::Use { &mut read } else { &mut written };
372            into.push(name(operand));
373        }
374        (written, read)
375    }
376
377    #[test]
378    fn a_call_passes_its_arguments_where_the_convention_puts_them() {
379        let i32 = Type::int(32);
380        let (_, func, made) = make(&[i32, i32, i32], None, false, &SYSV);
381        assert_eq!(made.expect("three integers all fit in registers").result, None);
382        assert_eq!(operands(&func).1, ["rdi", "rsi", "rdx"]);
383    }
384
385    #[test]
386    fn the_other_convention_passes_the_same_arguments_somewhere_else() {
387        let i64 = Type::int(64);
388        let (_, func, made) = make(&[i64, i64], None, false, &WIN64);
389        // Thirty two bytes of stack for a call that passes nothing on the stack, which is what
390        // Windows asks a caller to leave the callee whether the callee uses it or not.
391        assert_eq!(made.expect("two integers fit in registers").outgoing, 32);
392        assert_eq!(operands(&func).1, ["rcx", "rdx"]);
393    }
394
395    #[test]
396    fn what_a_call_gives_back_comes_out_of_the_register_the_convention_returns_in() {
397        let (names, func, made) = make(&[], Some(Type::int(32)), false, &SYSV);
398        let result = made.expect("an integer comes back").result.expect("in a register");
399        // The first thing written is the result, and it is the only thing written that is a value
400        // rather than a register the callee destroyed.
401        assert_eq!(operands(&func).0.first().map(String::as_str), Some("rax"));
402        assert_eq!(func.class_of(result), Some(SYSV.int_class));
403        assert!(mir::print_func(&func, &names, &REGS).contains("x64.call"));
404    }
405
406    #[test]
407    fn every_register_the_callee_may_destroy_is_written_by_the_call() {
408        let (_, func, _) = make(&[Type::int(64)], Some(Type::int(64)), false, &SYSV);
409        let (written, read) = operands(&func);
410        // The callee saved registers are not here, because a value in one of those survives a
411        // call and that is the whole difference between the two halves of the convention.
412        for saved in ["rbx", "rbp", "r12", "r13", "r14", "r15"] {
413            assert!(!written.contains(&saved.to_string()), "{saved} survives a call");
414        }
415        // Every other integer register is, once. The two named ones are named by the result and
416        // by the argument instead, and naming one twice would be blocking it twice.
417        for destroyed in ["rcx", "rdx", "rsi", "r8", "r9", "r10", "r11"] {
418            let count = written.iter().filter(|name| *name == destroyed).count();
419            assert_eq!(count, 1, "{destroyed} is destroyed by a call and is written {count} times");
420        }
421        assert_eq!(written.iter().filter(|name| *name == "rax").count(), 1);
422        assert_eq!(read, ["rdi"]);
423        // The vector registers are all destroyed on SysV, and they are in the other class.
424        assert!(written.contains(&"xmm0".to_string()));
425    }
426
427    #[test]
428    fn a_variadic_call_says_how_many_vector_registers_it_passed_arguments_in() {
429        let (names, func, made) = make(&[Type::int(64)], None, true, &SYSV);
430        made.expect("an integer argument to a variadic callee");
431        let (_, read) = operands(&func);
432        // Zero of them, which is the answer while a float argument is refused, and `al` is where
433        // a SysV callee looks for it. Leaving whatever was in the register there would make a
434        // callee that saves its vector registers save ones it was never given.
435        assert_eq!(read, ["rdi", "rax"]);
436        assert_eq!(
437            mir::print_func(&func, &names, &REGS).lines().nth(2),
438            Some("    %1:gpr = x64.mov_ri_32 0")
439        );
440    }
441
442    #[test]
443    fn a_call_that_would_pass_an_argument_on_the_stack_is_reported() {
444        let i64 = Type::int(64);
445        let seven = vec![i64; 7];
446        let (_, func, made) = make(&seven, None, false, &SYSV);
447        assert_eq!(made, Err(Refused { argument: Some(6), missing: Missing::OnStack }));
448        // Nothing was written, so a call this cannot make leaves no half of one behind.
449        let block = func.entry().expect("a function with a block in it");
450        assert_eq!(func.insts(block).count(), 0);
451        // Windows runs out three arguments earlier, which is the same answer at a different
452        // position and the reason this is a fact about the convention rather than about the call.
453        assert_eq!(
454            make(&seven, None, false, &WIN64).2,
455            Err(Refused { argument: Some(4), missing: Missing::OnStack })
456        );
457    }
458
459    #[test]
460    fn a_call_that_travels_in_a_vector_register_is_reported_on_either_side() {
461        let f64 = Type::float(rucc_ir::Float::F64);
462        assert_eq!(
463            make(&[Type::int(32), f64], None, false, &SYSV).2,
464            Err(Refused { argument: Some(1), missing: Missing::InVector })
465        );
466        assert_eq!(
467            make(&[], Some(f64), false, &SYSV).2,
468            Err(Refused { argument: None, missing: Missing::InVector })
469        );
470    }
471
472    #[test]
473    fn a_call_at_a_width_no_register_holds_is_reported_on_either_side() {
474        let i128 = Type::int(128);
475        assert_eq!(
476            make(&[i128], None, false, &SYSV).2,
477            Err(Refused { argument: Some(0), missing: Missing::Width })
478        );
479        assert_eq!(
480            make(&[], Some(i128), false, &SYSV).2,
481            Err(Refused { argument: None, missing: Missing::Width })
482        );
483    }
484
485    #[test]
486    fn an_argument_wider_than_a_register_has_no_name() {
487        assert_eq!(head_of(Type::int(128)), None);
488        assert_eq!(head_of(Type::int(8)), Some("x64.arg_val_8"));
489        assert_eq!(head_of(Type::int(64)), Some("x64.arg_val_64"));
490    }
491
492    /// An address arrives in a general purpose register like any other integer of its width, and
493    /// used to be turned away here as a width no register holds, which is what issue 274 is.
494    /// `int g(char *s)` is the smallest program that was.
495    #[test]
496    fn an_address_arrives_in_a_register_like_the_integer_it_is() {
497        assert_eq!(head_of(Type::PTR), Some("x64.arg_val_64"));
498        assert_eq!(
499            bind(&[Type::PTR], &SYSV),
500            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_64\n}\n"
501        );
502        // And it travels the same way at a call, on both sides of one.
503        assert!(make(&[Type::PTR], Some(Type::PTR), false, &SYSV).2.is_ok());
504    }
505}