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 !ty.is_int() || !matches!(ty.bits(), 8 | 16 | 32 | 64) {
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 !ty.is_int() || !matches!(ty.bits(), 8 | 16 | 32 | 64) => {
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/// An integer and nothing else, which is the same answer `crate::term` gives about a value in a
269/// register, and deliberately the same: an address is not an `i64` to the rule set today, so
270/// bringing one in here would produce a register no rule could then name. Whoever widens one of
271/// the two should widen both.
272#[must_use]
273pub fn head_of(ty: Type) -> Option<&'static str> {
274    let slot = match ty.is_int().then(|| ty.bits())? {
275        8 => 0,
276        16 => 1,
277        32 => 2,
278        64 => 3,
279        _ => return None,
280    };
281    Some(["x64.arg_val_8", "x64.arg_val_16", "x64.arg_val_32", "x64.arg_val_64"][slot])
282}
283
284#[cfg(test)]
285mod tests {
286    use rucc_target::x86_64::{REGS, SYSV, WIN64};
287
288    use super::*;
289
290    /// The parameters of a function under a convention, as machine IR text.
291    fn bind(params: &[Type], conv: &CallRegs) -> String {
292        let mut names = Interner::new();
293        let mut out = mir::Func::new(names.intern("f"));
294        let block = out.create_block();
295        entry(&mut out, block, params, conv, &mut names).expect("every parameter arrives");
296        mir::print_func(&out, &names, &REGS)
297    }
298
299    #[test]
300    fn the_first_arguments_arrive_where_the_convention_puts_them() {
301        let i32 = Type::int(32);
302        assert_eq!(
303            bind(&[i32, i32, Type::int(64)], &SYSV),
304            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_32\n    \
305             %1:gpr($rsi) = x64.arg_val_32\n    %2:gpr($rdx) = x64.arg_val_64\n}\n"
306        );
307    }
308
309    #[test]
310    fn the_other_convention_puts_the_same_arguments_somewhere_else() {
311        // The first argument is in `rcx` here and in `rdi` above, which is the difference that
312        // makes a SysV binary calling a Windows one read the wrong value rather than fail.
313        let i64 = Type::int(64);
314        assert_eq!(
315            bind(&[i64, i64], &WIN64),
316            "mfunc @f {\nblock0:\n    %0:gpr($rcx) = x64.arg_val_64\n    \
317             %1:gpr($rdx) = x64.arg_val_64\n}\n"
318        );
319    }
320
321    #[test]
322    fn an_argument_past_the_last_register_is_reported_rather_than_read_from_nowhere() {
323        let i64 = Type::int(64);
324        let mut names = Interner::new();
325        let mut out = mir::Func::new(names.intern("f"));
326        let block = out.create_block();
327        let seven = vec![i64; 7];
328        assert_eq!(entry(&mut out, block, &seven, &SYSV, &mut names), Err((6, Missing::OnStack)));
329        // Six of them still got registers, and the seventh is what stopped it. Windows runs out
330        // three arguments earlier, which is the same answer at a different position.
331        assert_eq!(entry(&mut out, block, &seven, &WIN64, &mut names), Err((4, Missing::OnStack)));
332    }
333
334    #[test]
335    fn an_argument_in_a_vector_register_is_reported_because_nothing_here_uses_one() {
336        let mut names = Interner::new();
337        let mut out = mir::Func::new(names.intern("f"));
338        let block = out.create_block();
339        let params = [Type::int(32), Type::float(rucc_ir::Float::F64)];
340        assert_eq!(entry(&mut out, block, &params, &SYSV, &mut names), Err((1, Missing::InVector)));
341    }
342
343    /// One call to `g`, with a register for each argument arriving in the block that makes it.
344    fn make(
345        args: &[Type],
346        returns: Option<Type>,
347        variadic: bool,
348        conv: &CallRegs,
349    ) -> (Interner, mir::Func, Result<Made, Refused>) {
350        let mut names = Interner::new();
351        let mut out = mir::Func::new(names.intern("f"));
352        let block = out.create_block();
353        let passed: Vec<(Type, mir::Reg)> =
354            args.iter().map(|&ty| (ty, out.append_param(block, conv.int_class))).collect();
355        let callee = names.intern("g");
356        let what = Calling { callee, args: &passed, returns, variadic };
357        let made = call(&mut out, block, &what, conv, &mut names);
358        (names, out, made)
359    }
360
361    /// What the call in that function reads and writes, by register name, in the order the
362    /// operands are in.
363    fn operands(func: &mir::Func) -> (Vec<String>, Vec<String>) {
364        let block = func.entry().expect("a function with a block in it");
365        let call = func.terminator(block).expect("the call is the last thing in the block");
366        let name = |operand: &mir::Operand| match (operand.reg.phys(), operand.constraint) {
367            (Some(reg), _) | (None, Constraint::Fixed(reg)) => {
368                REGS.name(operand.class, reg).expect("a register the file describes").to_string()
369            }
370            _ => format!("{:?}", operand.reg),
371        };
372        let mut written = Vec::new();
373        let mut read = Vec::new();
374        for operand in &func[func[call].operands] {
375            let into = if operand.role == mir::Role::Use { &mut read } else { &mut written };
376            into.push(name(operand));
377        }
378        (written, read)
379    }
380
381    #[test]
382    fn a_call_passes_its_arguments_where_the_convention_puts_them() {
383        let i32 = Type::int(32);
384        let (_, func, made) = make(&[i32, i32, i32], None, false, &SYSV);
385        assert_eq!(made.expect("three integers all fit in registers").result, None);
386        assert_eq!(operands(&func).1, ["rdi", "rsi", "rdx"]);
387    }
388
389    #[test]
390    fn the_other_convention_passes_the_same_arguments_somewhere_else() {
391        let i64 = Type::int(64);
392        let (_, func, made) = make(&[i64, i64], None, false, &WIN64);
393        // Thirty two bytes of stack for a call that passes nothing on the stack, which is what
394        // Windows asks a caller to leave the callee whether the callee uses it or not.
395        assert_eq!(made.expect("two integers fit in registers").outgoing, 32);
396        assert_eq!(operands(&func).1, ["rcx", "rdx"]);
397    }
398
399    #[test]
400    fn what_a_call_gives_back_comes_out_of_the_register_the_convention_returns_in() {
401        let (names, func, made) = make(&[], Some(Type::int(32)), false, &SYSV);
402        let result = made.expect("an integer comes back").result.expect("in a register");
403        // The first thing written is the result, and it is the only thing written that is a value
404        // rather than a register the callee destroyed.
405        assert_eq!(operands(&func).0.first().map(String::as_str), Some("rax"));
406        assert_eq!(func.class_of(result), Some(SYSV.int_class));
407        assert!(mir::print_func(&func, &names, &REGS).contains("x64.call"));
408    }
409
410    #[test]
411    fn every_register_the_callee_may_destroy_is_written_by_the_call() {
412        let (_, func, _) = make(&[Type::int(64)], Some(Type::int(64)), false, &SYSV);
413        let (written, read) = operands(&func);
414        // The callee saved registers are not here, because a value in one of those survives a
415        // call and that is the whole difference between the two halves of the convention.
416        for saved in ["rbx", "rbp", "r12", "r13", "r14", "r15"] {
417            assert!(!written.contains(&saved.to_string()), "{saved} survives a call");
418        }
419        // Every other integer register is, once. The two named ones are named by the result and
420        // by the argument instead, and naming one twice would be blocking it twice.
421        for destroyed in ["rcx", "rdx", "rsi", "r8", "r9", "r10", "r11"] {
422            let count = written.iter().filter(|name| *name == destroyed).count();
423            assert_eq!(count, 1, "{destroyed} is destroyed by a call and is written {count} times");
424        }
425        assert_eq!(written.iter().filter(|name| *name == "rax").count(), 1);
426        assert_eq!(read, ["rdi"]);
427        // The vector registers are all destroyed on SysV, and they are in the other class.
428        assert!(written.contains(&"xmm0".to_string()));
429    }
430
431    #[test]
432    fn a_variadic_call_says_how_many_vector_registers_it_passed_arguments_in() {
433        let (names, func, made) = make(&[Type::int(64)], None, true, &SYSV);
434        made.expect("an integer argument to a variadic callee");
435        let (_, read) = operands(&func);
436        // Zero of them, which is the answer while a float argument is refused, and `al` is where
437        // a SysV callee looks for it. Leaving whatever was in the register there would make a
438        // callee that saves its vector registers save ones it was never given.
439        assert_eq!(read, ["rdi", "rax"]);
440        assert_eq!(
441            mir::print_func(&func, &names, &REGS).lines().nth(2),
442            Some("    %1:gpr = x64.mov_ri_32 0")
443        );
444    }
445
446    #[test]
447    fn a_call_that_would_pass_an_argument_on_the_stack_is_reported() {
448        let i64 = Type::int(64);
449        let seven = vec![i64; 7];
450        let (_, func, made) = make(&seven, None, false, &SYSV);
451        assert_eq!(made, Err(Refused { argument: Some(6), missing: Missing::OnStack }));
452        // Nothing was written, so a call this cannot make leaves no half of one behind.
453        let block = func.entry().expect("a function with a block in it");
454        assert_eq!(func.insts(block).count(), 0);
455        // Windows runs out three arguments earlier, which is the same answer at a different
456        // position and the reason this is a fact about the convention rather than about the call.
457        assert_eq!(
458            make(&seven, None, false, &WIN64).2,
459            Err(Refused { argument: Some(4), missing: Missing::OnStack })
460        );
461    }
462
463    #[test]
464    fn a_call_that_travels_in_a_vector_register_is_reported_on_either_side() {
465        let f64 = Type::float(rucc_ir::Float::F64);
466        assert_eq!(
467            make(&[Type::int(32), f64], None, false, &SYSV).2,
468            Err(Refused { argument: Some(1), missing: Missing::InVector })
469        );
470        assert_eq!(
471            make(&[], Some(f64), false, &SYSV).2,
472            Err(Refused { argument: None, missing: Missing::InVector })
473        );
474    }
475
476    #[test]
477    fn a_call_at_a_width_no_register_holds_is_reported_on_either_side() {
478        let i128 = Type::int(128);
479        assert_eq!(
480            make(&[i128], None, false, &SYSV).2,
481            Err(Refused { argument: Some(0), missing: Missing::Width })
482        );
483        assert_eq!(
484            make(&[], Some(i128), false, &SYSV).2,
485            Err(Refused { argument: None, missing: Missing::Width })
486        );
487    }
488
489    #[test]
490    fn an_argument_wider_than_a_register_has_no_name() {
491        assert_eq!(head_of(Type::int(128)), None);
492        assert_eq!(head_of(Type::int(8)), Some("x64.arg_val_8"));
493        assert_eq!(head_of(Type::int(64)), Some("x64.arg_val_64"));
494    }
495}