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 ® in conv.int_order {
234 if !conv.preserves_int(reg) && !named.contains(®) {
235 operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.int_class));
236 }
237 }
238 for ® 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, ®S)
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, ¶ms, &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, ®S).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, ®S).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}