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 ® 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/// 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, ®S)
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, ¶ms, &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, ®S).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, ®S).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}