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`
34//! and `x64.call_reg` are the only opcodes in the description whose operand vector is empty
35//! there, because nothing about a call's operands is the same from one call to the next, so they
36//! are built here: one read per argument constrained to the register the convention passes it in,
37//! one definition for the value that comes back constrained to the register it comes back in, and
38//! one definition per register the convention does not preserve.
39//!
40//! A call through an address has one operand more, which is the address, and it is the one
41//! operand of a call that is a fact about the instruction rather than about the signature. It
42//! goes in front of the arguments, because the assembler has to find it and an index into a
43//! vector whose length depends on the convention is not a way of finding anything.
44//!
45//! Those last ones are the clobbers, and they are the whole of what the allocator has to know
46//! about a call besides where the values go. Each is a definition of the physical register itself
47//! rather than of a value, since there is no value: it says the register is written here, which
48//! is exactly what stops the allocator from leaving something in one across the call. A register
49//! an argument or the result already names is not repeated, because naming it once already blocks
50//! it for the length of the instruction, which is all a clobber does.
51//!
52//! What is not here is the bytes an argument past the last register goes in. That is a place in
53//! the frame and no frame exists yet, the same reason a parameter arriving there is reported
54//! rather than read, so a call is asked how many bytes it would need and reports it, and a call
55//! that would need any is turned down for now.
56
57use rucc_base::{Interner, Symbol};
58use rucc_ir::Type;
59use rucc_mir as mir;
60use rucc_target::{CallRegs, Constraint, PhysReg, Places, RegClass, Where};
61
62/// Why a parameter could not be brought in.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Missing {
65    /// It arrives on the stack, which is somewhere nothing reads from yet: the offset is a
66    /// distance into a frame, and a frame is not worked out until after allocation.
67    OnStack,
68    /// It travels on the x87 stack, which is a `long double` and nothing else. That stack is a
69    /// third register file, it is not one the allocator has, and no instruction in the
70    /// description touches it.
71    OnX87,
72    /// It is a float passed to a callee that takes arguments beyond the ones its signature names,
73    /// on a convention that puts such a float in a vector register and in the general purpose
74    /// register at the same position at once. Which arguments are the ones beyond the signature is
75    /// what decides whether the second copy is needed, and a call does not carry that yet.
76    InBothFiles,
77    /// It is a width no pseudo covers, which is anything a machine register does not hold.
78    Width,
79}
80
81impl Missing {
82    /// What it says when a function could not be compiled because of it.
83    ///
84    /// Worded so that it reads the same about a value arriving and a value being passed, since
85    /// the two are the same fact seen from the two ends of one call.
86    #[must_use]
87    pub fn why(self) -> &'static str {
88        match self {
89            Missing::OnStack => "is passed on the stack",
90            Missing::OnX87 => "is on the x87 stack",
91            Missing::InBothFiles => "is a float passed to a variadic callee on this convention",
92            Missing::Width => "is a width no argument register holds",
93        }
94    }
95}
96
97/// Which register file a value of that type travels in.
98///
99/// The whole of what the two files mean to this module. A float is in the vector one and
100/// everything else is in the general purpose one, which is what both of this machine's conventions
101/// say, and the `long double` that is in neither is turned away by [`refuses`] before this is
102/// asked.
103fn class_of(ty: Type, conv: &CallRegs) -> RegClass {
104    if ty.is_float() { conv.sse_class } else { conv.int_class }
105}
106
107/// Why a value of that type cannot travel at all, or nothing if it can.
108///
109/// The width question and the file question in one place, so that the two ends of a call give the
110/// same answer about the same type.
111fn refuses(ty: Type) -> Option<Missing> {
112    if head_of(ty).is_some() {
113        return None;
114    }
115    // A `long double` is the one type here that is in neither of the two files. Saying so is worth
116    // more than calling it a width, because eighty bits is a width this machine computes in and
117    // the file it computes in is what actually stands in the way.
118    if ty.is_float() && ty.bits() == 80 {
119        return Some(Missing::OnX87);
120    }
121    Some(Missing::Width)
122}
123
124/// Binds a function's parameters to the registers the convention says they arrive in.
125///
126/// The registers come back in the order the parameters were given, so the caller can bind each
127/// IR parameter to the one at its position.
128///
129/// # Errors
130///
131/// The first parameter this cannot bring in, and why. A function with one is reported rather
132/// than compiled, because the alternative is a function that reads an argument from wherever the
133/// last one happened to leave a register.
134pub fn entry(
135    out: &mut mir::Func,
136    block: mir::Block,
137    params: &[Type],
138    conv: &CallRegs,
139    names: &mut Interner,
140) -> Result<Vec<mir::Reg>, (usize, Missing)> {
141    let mut places = Places::new(conv);
142    let mut regs = Vec::with_capacity(params.len());
143    for (index, &ty) in params.iter().enumerate() {
144        // Asking for the place of a parameter that cannot be brought in is still worth doing
145        // before giving up, and it costs nothing, because every place after it depends on it and
146        // a reader stepping through this in a debugger should see the same numbers a working
147        // version would.
148        let at = if ty.is_float() { places.float() } else { places.integer() };
149        if let Some(missing) = refuses(ty) {
150            return Err((index, missing));
151        }
152        let head = head_of(ty).ok_or((index, Missing::Width))?;
153        let Where::Reg(arrived) = at else { return Err((index, Missing::OnStack)) };
154
155        let class = class_of(ty, conv);
156        let reg = out.new_vreg(class);
157        let opcode = mir::Opcode::new(names.intern(head));
158        let operand = mir::Operand::write(reg, class).with(Constraint::Fixed(arrived));
159        out.build(block, opcode).operand(operand).finish();
160        regs.push(reg);
161    }
162    Ok(regs)
163}
164
165/// What the instruction that calls a name is called.
166///
167/// Here rather than in a rule for the same reason the arguments are: a rule pattern sees one term
168/// and a call's operands are whatever the signature made them, so no pattern could name them.
169pub const CALL: &str = "x64.call";
170
171/// What the instruction that calls an address in a register is called.
172///
173/// A different instruction rather than the same one with a different operand, which is what the
174/// machine says too: one carries the distance to somewhere in the program and takes a relocation,
175/// and the other carries the register the address is in and takes none. Sharing an opcode would
176/// mean an instruction whose bytes depend on whether a field beside it happens to be set.
177pub const CALL_REG: &str = "x64.call_reg";
178
179/// What one call came to.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub struct Made {
182    /// The register the value came back in, or `None` for a call that gives nothing back.
183    pub result: Option<mir::Reg>,
184    /// How many bytes below the stack pointer this call needs for the arguments it passes there.
185    ///
186    /// Not always zero for a call that passes everything in registers: a Windows caller reserves
187    /// thirty two bytes for the callee to spill its register arguments into whether it uses them
188    /// or not, and that reservation is this.
189    pub outgoing: u32,
190}
191
192/// Which of a call's values could not be passed, and why.
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub struct Refused {
195    /// Its position among the arguments, or `None` for the value that comes back.
196    pub argument: Option<usize>,
197    /// What is wrong with where it travels.
198    pub missing: Missing,
199}
200
201/// What a call goes to.
202///
203/// The whole of the difference between the two calls. Everything else about them, which is what
204/// they pass and what comes back and which registers they destroy, is the signature's answer and
205/// is the same answer either way.
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum Callee {
208    /// A name, which the linker resolves.
209    Named(Symbol),
210    /// An address in a register, which nothing resolves because there is nothing to resolve: the
211    /// value is not known until the program runs.
212    ///
213    /// The register is unconstrained, and it has to be, because every register the convention
214    /// does not preserve is one this instruction writes and every register an argument travels in
215    /// is spoken for. What is left is the registers the callee has to put back, which is where
216    /// the allocator will put the address, and it is the right answer for the same reason it is
217    /// the only one.
218    Through(mir::Reg),
219}
220
221/// One call, as everything about it that is not the function it is being built into.
222#[derive(Debug, Clone, Copy)]
223pub struct Calling<'a> {
224    /// What it calls.
225    pub callee: Callee,
226    /// What it passes, as the type each value travels as and the register it is in, in the order
227    /// the signature holds them, which is the order the convention places them in.
228    pub args: &'a [(Type, mir::Reg)],
229    /// What comes back, or `None` for a call that gives nothing back.
230    pub returns: Option<Type>,
231    /// Whether the callee takes arguments beyond the ones its signature names, which is what says
232    /// whether it reads the count of vector registers the call passed arguments in.
233    pub variadic: bool,
234}
235
236/// Builds one call: what it passes, what comes back, and what it destroys.
237///
238/// # Errors
239///
240/// The first value this cannot pass, and why, before anything is written. A call with one is
241/// reported rather than compiled, because the alternative is a call that leaves an argument
242/// wherever the last one happened to put a register.
243pub fn call(
244    out: &mut mir::Func,
245    block: mir::Block,
246    made: &Calling<'_>,
247    conv: &CallRegs,
248    names: &mut Interner,
249) -> Result<Made, Refused> {
250    let &Calling { callee, args, returns, variadic } = made;
251    // Where everything goes, worked out before anything is built, so that a call this cannot make
252    // leaves no half of one behind.
253    let mut places = Places::new(conv);
254    let mut passed = Vec::with_capacity(args.len());
255    // How many of them went in vector registers, which is what a SysV variadic callee is told.
256    let mut vectors = 0u32;
257    for (index, &(ty, reg)) in args.iter().enumerate() {
258        let refused = |missing| Refused { argument: Some(index), missing };
259        let at = if ty.is_float() { places.float() } else { places.integer() };
260        if let Some(missing) = refuses(ty) {
261            return Err(refused(missing));
262        }
263        // Windows passes a float to a variadic callee in the vector register and in the general
264        // purpose register at the same position, both at once, because the callee has no
265        // prototype to tell it which file to look in. Doing that needs to know which arguments are
266        // the ones the signature does not name, and a call carries whether the callee is variadic
267        // rather than how many arguments it names, so this is turned down rather than passed in
268        // one file and read from the other.
269        if ty.is_float() && variadic && conv.shared_positions {
270            return Err(refused(Missing::InBothFiles));
271        }
272        let Where::Reg(at) = at else { return Err(refused(Missing::OnStack)) };
273        let class = class_of(ty, conv);
274        if class == conv.sse_class {
275            vectors += 1;
276        }
277        passed.push((reg, at, class));
278    }
279    let comes_back = match returns {
280        None => None,
281        Some(ty) if refuses(ty).is_some() => {
282            return Err(Refused { argument: None, missing: refuses(ty).unwrap_or(Missing::Width) });
283        }
284        // Which register a value comes back in depends on nothing but the value, which is why the
285        // return side of the convention is a rule and this side is not. There is no rule here
286        // because the arguments are in the same instruction.
287        Some(ty) => {
288            let class = class_of(ty, conv);
289            let file = if class == conv.sse_class { conv.sse_returns } else { conv.int_returns };
290            let at = *file.first().ok_or(Refused { argument: None, missing: Missing::Width })?;
291            Some((at, class))
292        }
293    };
294
295    // A variadic callee on SysV reads how many vector registers the call passed arguments in and
296    // skips saving them when the answer is none, which is what makes `printf` with no floating
297    // point argument cheap. It is an obligation rather than an optimization: leaving whatever was
298    // in the register there makes the callee save a register file it was not given, and a count
299    // that is too low makes it read an argument out of a register nothing put one in.
300    let counted = if variadic { conv.vector_count } else { None };
301
302    // The definitions first and the reads after, which is the order every operand vector in the
303    // machine IR is in and the order `rucc_mir::defs` counts.
304    let mut operands = Vec::with_capacity(args.len() + conv.int_order.len() + 2);
305    let result = comes_back.map(|(at, class)| {
306        let reg = out.new_vreg(class);
307        operands.push(mir::Operand::write(reg, class).with(Constraint::Fixed(at)));
308        reg
309    });
310    // One list per file, because a physical register is a number and the class is what says which
311    // file it is a number in. One list would have `xmm0` blocking `rax`.
312    let spoken_for = |class: RegClass| -> Vec<PhysReg> {
313        comes_back
314            .filter(|&(_, at)| at == class)
315            .map(|(reg, _)| reg)
316            .into_iter()
317            .chain(counted.filter(|_| class == conv.int_class))
318            .chain(passed.iter().filter(|&&(_, _, at)| at == class).map(|&(_, reg, _)| reg))
319            .collect()
320    };
321    let named = spoken_for(conv.int_class);
322    for &reg in conv.int_order {
323        if !conv.preserves_int(reg) && !named.contains(&reg) {
324            operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.int_class));
325        }
326    }
327    let named = spoken_for(conv.sse_class);
328    for &reg in conv.sse_order {
329        if !conv.preserves_sse(reg) && !named.contains(&reg) {
330            operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.sse_class));
331        }
332    }
333    // The address in front of the arguments, because a call through one is written with the
334    // register it goes through and nothing in the operand vector is at a place a table could name.
335    // First read is a place that does not depend on the signature, which is what
336    // [`rucc_target::x86_64::Arg::Through`] is written against.
337    if let Callee::Through(reg) = callee {
338        operands.push(mir::Operand::read(reg, conv.int_class));
339    }
340    for (reg, at, class) in passed {
341        operands.push(mir::Operand::read(reg, class).with(Constraint::Fixed(at)));
342    }
343    if let Some(at) = counted {
344        let count = out.new_vreg(conv.int_class);
345        let zero = mir::Opcode::new(names.intern("x64.mov_ri_32"));
346        out.build(block, zero).def(count, conv.int_class).imm(i64::from(vectors)).finish();
347        operands.push(mir::Operand::read(count, conv.int_class).with(Constraint::Fixed(at)));
348    }
349
350    let opcode = mir::Opcode::new(names.intern(match callee {
351        Callee::Named(_) => CALL,
352        Callee::Through(_) => CALL_REG,
353    }));
354    let mut build = out.build(block, opcode);
355    if let Callee::Named(symbol) = callee {
356        build = build.symbol(symbol);
357    }
358    for operand in operands {
359        build = build.operand(operand);
360    }
361    build.finish();
362    Ok(Made { result, outgoing: places.size() })
363}
364
365/// What the pseudo for an argument of that type is called.
366///
367/// The width is in the name for the same reason it is in every other opcode here: it is what the
368/// instruction is about. Nothing encodes it, so nothing depends on it being right, but a listing
369/// that says an argument arrived and does not say how much of it did is a listing worth less.
370///
371/// Which widths there are is the question the rule set asks of a type, and not a list of its own,
372/// because it has to be the same list. An argument brought in at a width the rules have no name
373/// for is a register
374/// nothing downstream could then read, and a width the rules cover that this refuses is a
375/// function turned away for no reason. Asking one question in one place is what keeps the two
376/// answers from drifting, and an address is what they used to disagree about.
377#[must_use]
378pub fn head_of(ty: Type) -> Option<&'static str> {
379    if let Some(at) = crate::term::float_slot(ty) {
380        return Some(["x64.arg_val_f32", "x64.arg_val_f64"][at]);
381    }
382    let names = ["x64.arg_val_8", "x64.arg_val_16", "x64.arg_val_32", "x64.arg_val_64"];
383    Some(names[crate::term::slot(ty)?])
384}
385
386#[cfg(test)]
387mod tests {
388    use rucc_target::x86_64::{REGS, SYSV, WIN64};
389
390    use super::*;
391
392    /// The parameters of a function under a convention, as machine IR text.
393    fn bind(params: &[Type], conv: &CallRegs) -> String {
394        let mut names = Interner::new();
395        let mut out = mir::Func::new(names.intern("f"));
396        let block = out.create_block();
397        entry(&mut out, block, params, conv, &mut names).expect("every parameter arrives");
398        mir::print_func(&out, &names, &REGS)
399    }
400
401    #[test]
402    fn the_first_arguments_arrive_where_the_convention_puts_them() {
403        let i32 = Type::int(32);
404        assert_eq!(
405            bind(&[i32, i32, Type::int(64)], &SYSV),
406            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_32\n    \
407             %1:gpr($rsi) = x64.arg_val_32\n    %2:gpr($rdx) = x64.arg_val_64\n}\n"
408        );
409    }
410
411    #[test]
412    fn the_other_convention_puts_the_same_arguments_somewhere_else() {
413        // The first argument is in `rcx` here and in `rdi` above, which is the difference that
414        // makes a SysV binary calling a Windows one read the wrong value rather than fail.
415        let i64 = Type::int(64);
416        assert_eq!(
417            bind(&[i64, i64], &WIN64),
418            "mfunc @f {\nblock0:\n    %0:gpr($rcx) = x64.arg_val_64\n    \
419             %1:gpr($rdx) = x64.arg_val_64\n}\n"
420        );
421    }
422
423    #[test]
424    fn an_argument_past_the_last_register_is_reported_rather_than_read_from_nowhere() {
425        let i64 = Type::int(64);
426        let mut names = Interner::new();
427        let mut out = mir::Func::new(names.intern("f"));
428        let block = out.create_block();
429        let seven = vec![i64; 7];
430        assert_eq!(entry(&mut out, block, &seven, &SYSV, &mut names), Err((6, Missing::OnStack)));
431        // Six of them still got registers, and the seventh is what stopped it. Windows runs out
432        // three arguments earlier, which is the same answer at a different position.
433        assert_eq!(entry(&mut out, block, &seven, &WIN64, &mut names), Err((4, Missing::OnStack)));
434    }
435
436    /// A float arrives in the other file, and the two files are counted apart on SysV: the
437    /// integer here is the first integer argument and the float is the first float one, so they
438    /// are in `rdi` and `xmm0` rather than in the first and second of anything.
439    #[test]
440    fn a_float_arrives_in_a_vector_register_and_is_counted_apart_from_the_integers() {
441        let f32 = Type::float(rucc_ir::Float::F32);
442        let f64 = Type::float(rucc_ir::Float::F64);
443        assert_eq!(
444            bind(&[Type::int(32), f64, f32], &SYSV),
445            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_32\n    \
446             %1:xmm($xmm0) = x64.arg_val_f64\n    %2:xmm($xmm1) = x64.arg_val_f32\n}\n"
447        );
448    }
449
450    /// Windows counts the two files together, so the same three arguments land in different
451    /// registers: the float is the second argument and takes the second vector register rather
452    /// than the first, which is the difference that makes a mismatched call read the wrong value.
453    #[test]
454    fn the_other_convention_counts_the_two_files_as_one_run_of_positions() {
455        let f64 = Type::float(rucc_ir::Float::F64);
456        assert_eq!(
457            bind(&[Type::int(32), f64, Type::int(64)], &WIN64),
458            "mfunc @f {\nblock0:\n    %0:gpr($rcx) = x64.arg_val_32\n    \
459             %1:xmm($xmm1) = x64.arg_val_f64\n    %2:gpr($r8) = x64.arg_val_64\n}\n"
460        );
461    }
462
463    /// A `long double` is in neither file, and what it is turned away for says so rather than
464    /// calling eighty bits a width no register holds. The x87 stack is a register file this
465    /// compiler does not allocate in and has no instruction for.
466    #[test]
467    fn a_long_double_is_reported_as_the_x87_stack_it_travels_on() {
468        let mut names = Interner::new();
469        let mut out = mir::Func::new(names.intern("f"));
470        let block = out.create_block();
471        let params = [Type::int(32), Type::float(rucc_ir::Float::F80)];
472        assert_eq!(entry(&mut out, block, &params, &SYSV, &mut names), Err((1, Missing::OnX87)));
473        assert_eq!(
474            make(&[], Some(Type::float(rucc_ir::Float::F80)), false, &SYSV).2,
475            Err(Refused { argument: None, missing: Missing::OnX87 })
476        );
477    }
478
479    /// One call to `g`, with a register for each argument arriving in the block that makes it.
480    fn make(
481        args: &[Type],
482        returns: Option<Type>,
483        variadic: bool,
484        conv: &CallRegs,
485    ) -> (Interner, mir::Func, Result<Made, Refused>) {
486        let mut names = Interner::new();
487        let mut out = mir::Func::new(names.intern("f"));
488        let block = out.create_block();
489        let passed: Vec<(Type, mir::Reg)> =
490            args.iter().map(|&ty| (ty, out.append_param(block, class_of(ty, conv)))).collect();
491        let callee = Callee::Named(names.intern("g"));
492        let what = Calling { callee, args: &passed, returns, variadic };
493        let made = call(&mut out, block, &what, conv, &mut names);
494        (names, out, made)
495    }
496
497    /// What the call in that function reads and writes, by register name, in the order the
498    /// operands are in.
499    fn operands(func: &mir::Func) -> (Vec<String>, Vec<String>) {
500        let block = func.entry().expect("a function with a block in it");
501        let call = func.terminator(block).expect("the call is the last thing in the block");
502        let name = |operand: &mir::Operand| match (operand.reg.phys(), operand.constraint) {
503            (Some(reg), _) | (None, Constraint::Fixed(reg)) => {
504                REGS.name(operand.class, reg).expect("a register the file describes").to_string()
505            }
506            _ => format!("{:?}", operand.reg),
507        };
508        let mut written = Vec::new();
509        let mut read = Vec::new();
510        for operand in &func[func[call].operands] {
511            let into = if operand.role == mir::Role::Use { &mut read } else { &mut written };
512            into.push(name(operand));
513        }
514        (written, read)
515    }
516
517    #[test]
518    fn a_call_passes_its_arguments_where_the_convention_puts_them() {
519        let i32 = Type::int(32);
520        let (_, func, made) = make(&[i32, i32, i32], None, false, &SYSV);
521        assert_eq!(made.expect("three integers all fit in registers").result, None);
522        assert_eq!(operands(&func).1, ["rdi", "rsi", "rdx"]);
523    }
524
525    #[test]
526    fn the_other_convention_passes_the_same_arguments_somewhere_else() {
527        let i64 = Type::int(64);
528        let (_, func, made) = make(&[i64, i64], None, false, &WIN64);
529        // Thirty two bytes of stack for a call that passes nothing on the stack, which is what
530        // Windows asks a caller to leave the callee whether the callee uses it or not.
531        assert_eq!(made.expect("two integers fit in registers").outgoing, 32);
532        assert_eq!(operands(&func).1, ["rcx", "rdx"]);
533    }
534
535    #[test]
536    fn what_a_call_gives_back_comes_out_of_the_register_the_convention_returns_in() {
537        let (names, func, made) = make(&[], Some(Type::int(32)), false, &SYSV);
538        let result = made.expect("an integer comes back").result.expect("in a register");
539        // The first thing written is the result, and it is the only thing written that is a value
540        // rather than a register the callee destroyed.
541        assert_eq!(operands(&func).0.first().map(String::as_str), Some("rax"));
542        assert_eq!(func.class_of(result), Some(SYSV.int_class));
543        assert!(mir::print_func(&func, &names, &REGS).contains("x64.call"));
544    }
545
546    #[test]
547    fn every_register_the_callee_may_destroy_is_written_by_the_call() {
548        let (_, func, _) = make(&[Type::int(64)], Some(Type::int(64)), false, &SYSV);
549        let (written, read) = operands(&func);
550        // The callee saved registers are not here, because a value in one of those survives a
551        // call and that is the whole difference between the two halves of the convention.
552        for saved in ["rbx", "rbp", "r12", "r13", "r14", "r15"] {
553            assert!(!written.contains(&saved.to_string()), "{saved} survives a call");
554        }
555        // Every other integer register is, once. The two named ones are named by the result and
556        // by the argument instead, and naming one twice would be blocking it twice.
557        for destroyed in ["rcx", "rdx", "rsi", "r8", "r9", "r10", "r11"] {
558            let count = written.iter().filter(|name| *name == destroyed).count();
559            assert_eq!(count, 1, "{destroyed} is destroyed by a call and is written {count} times");
560        }
561        assert_eq!(written.iter().filter(|name| *name == "rax").count(), 1);
562        assert_eq!(read, ["rdi"]);
563        // The vector registers are all destroyed on SysV, and they are in the other class.
564        assert!(written.contains(&"xmm0".to_string()));
565    }
566
567    #[test]
568    fn a_variadic_call_says_how_many_vector_registers_it_passed_arguments_in() {
569        let (names, func, made) = make(&[Type::int(64)], None, true, &SYSV);
570        made.expect("an integer argument to a variadic callee");
571        let (_, read) = operands(&func);
572        // Zero of them here, and `al` is where a SysV callee looks for it. Leaving whatever was in
573        // the register there would make a callee that saves its vector registers save ones it was
574        // never given.
575        assert_eq!(read, ["rdi", "rax"]);
576        assert_eq!(
577            mir::print_func(&func, &names, &REGS).lines().nth(2),
578            Some("    %1:gpr = x64.mov_ri_32 0")
579        );
580
581        // Two of them here, which is the number that decides how much of the register save area a
582        // callee like `printf` fills in. A count of zero with a float in `xmm0` would be a callee
583        // reading its first `%f` out of a register nothing wrote.
584        let f64 = Type::float(rucc_ir::Float::F64);
585        let (names, func, made) = make(&[Type::int(64), f64, f64], None, true, &SYSV);
586        made.expect("one integer and two floats all fit in registers");
587        assert_eq!(operands(&func).1, ["rdi", "xmm0", "xmm1", "rax"]);
588        assert!(mir::print_func(&func, &names, &REGS).contains("x64.mov_ri_32 2"));
589    }
590
591    /// Windows passes a float to a variadic callee in both files at once, and which arguments are
592    /// the ones the signature does not name is not something a call carries, so it is turned down
593    /// rather than passed in one file and read from the other.
594    #[test]
595    fn a_float_passed_to_a_variadic_callee_on_windows_is_reported() {
596        let f64 = Type::float(rucc_ir::Float::F64);
597        assert_eq!(
598            make(&[Type::int(32), f64], None, true, &WIN64).2,
599            Err(Refused { argument: Some(1), missing: Missing::InBothFiles })
600        );
601        // The same call to a callee whose signature names both arguments is fine, because there is
602        // no second copy to make.
603        assert!(make(&[Type::int(32), f64], None, false, &WIN64).2.is_ok());
604    }
605
606    #[test]
607    fn a_call_through_an_address_reads_it_in_front_of_the_arguments() {
608        let i32 = Type::int(32);
609        let mut names = Interner::new();
610        let mut out = mir::Func::new(names.intern("f"));
611        let block = out.create_block();
612        let address = out.append_param(block, SYSV.int_class);
613        let passed = vec![(i32, out.append_param(block, SYSV.int_class))];
614        let what = Calling {
615            callee: Callee::Through(address),
616            args: &passed,
617            returns: Some(i32),
618            variadic: false,
619        };
620        call(&mut out, block, &what, &SYSV, &mut names).expect("one integer fits in a register");
621
622        // The address is the first thing read and the arguments follow it, which is the order the
623        // assembler counts on, and it is in no particular register because every register a call
624        // could insist on is one the call has already spoken for.
625        let text = mir::print_func(&out, &names, &REGS);
626        assert!(text.contains("= x64.call_reg %0, %1($rdi)\n"), "{text}");
627        assert!(!text.contains("@g"), "a call through an address names nobody: {text}");
628    }
629
630    #[test]
631    fn a_call_that_would_pass_an_argument_on_the_stack_is_reported() {
632        let i64 = Type::int(64);
633        let seven = vec![i64; 7];
634        let (_, func, made) = make(&seven, None, false, &SYSV);
635        assert_eq!(made, Err(Refused { argument: Some(6), missing: Missing::OnStack }));
636        // Nothing was written, so a call this cannot make leaves no half of one behind.
637        let block = func.entry().expect("a function with a block in it");
638        assert_eq!(func.insts(block).count(), 0);
639        // Windows runs out three arguments earlier, which is the same answer at a different
640        // position and the reason this is a fact about the convention rather than about the call.
641        assert_eq!(
642            make(&seven, None, false, &WIN64).2,
643            Err(Refused { argument: Some(4), missing: Missing::OnStack })
644        );
645    }
646
647    /// A float travels in the other file at both ends of a call, and the register it comes back in
648    /// is the first of that file rather than the first of the other one.
649    #[test]
650    fn a_call_passes_and_returns_a_float_in_a_vector_register() {
651        let f64 = Type::float(rucc_ir::Float::F64);
652        let (_, func, made) = make(&[Type::int(32), f64], Some(f64), false, &SYSV);
653        let result = made.expect("an integer and a float both fit in registers");
654        let (written, read) = operands(&func);
655        assert_eq!(read, ["rdi", "xmm0"]);
656        assert_eq!(written.first().map(String::as_str), Some("xmm0"));
657        assert_eq!(func.class_of(result.result.expect("a float comes back")), Some(SYSV.sse_class));
658        // Written once, because the register the result comes back in is already blocked by being
659        // named and a clobber that repeated it would be blocking it twice. `rax` is a clobber here
660        // rather than the result, which is the same register number in the other file and is the
661        // whole reason the two lists are counted apart.
662        assert_eq!(written.iter().filter(|name| *name == "xmm0").count(), 1);
663        assert!(written.contains(&"rax".to_string()));
664    }
665
666    #[test]
667    fn a_call_at_a_width_no_register_holds_is_reported_on_either_side() {
668        let i128 = Type::int(128);
669        assert_eq!(
670            make(&[i128], None, false, &SYSV).2,
671            Err(Refused { argument: Some(0), missing: Missing::Width })
672        );
673        assert_eq!(
674            make(&[], Some(i128), false, &SYSV).2,
675            Err(Refused { argument: None, missing: Missing::Width })
676        );
677    }
678
679    #[test]
680    fn an_argument_wider_than_a_register_has_no_name() {
681        assert_eq!(head_of(Type::int(128)), None);
682        assert_eq!(head_of(Type::int(8)), Some("x64.arg_val_8"));
683        assert_eq!(head_of(Type::int(64)), Some("x64.arg_val_64"));
684    }
685
686    /// An address arrives in a general purpose register like any other integer of its width, and
687    /// used to be turned away here as a width no register holds, which is what issue 274 is.
688    /// `int g(char *s)` is the smallest program that was.
689    #[test]
690    fn an_address_arrives_in_a_register_like_the_integer_it_is() {
691        assert_eq!(head_of(Type::PTR), Some("x64.arg_val_64"));
692        assert_eq!(
693            bind(&[Type::PTR], &SYSV),
694            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_64\n}\n"
695        );
696        // And it travels the same way at a call, on both sides of one.
697        assert!(make(&[Type::PTR], Some(Type::PTR), false, &SYSV).2.is_ok());
698    }
699}