rucc_codegen/varargs.rs
1//! What a function does to read the arguments its own signature does not name.
2//!
3//! Design: `spec/12-abi-and-runtime.md`, which is where the layout below comes from.
4//!
5//! A variadic callee has a problem an ordinary one does not. Six of its arguments arrived in
6//! general purpose registers and eight more in vector ones, and it cannot know which of those hold
7//! anything, because what it was passed is a thing only the caller knew. Registers are also not
8//! addressable, and `va_arg` walks arguments one after another at run time, which is walking
9//! addresses. So the convention says the callee spills all fourteen of them into a block of its own
10//! frame on the way in, and from then on every argument it was passed is somewhere in memory: the
11//! ones that came in registers are in that block, and the ones that did not are in the caller's
12//! argument area where they were left.
13//!
14//! That block is the register save area, and a `va_list` is four fields saying how far into the
15//! arguments the walk has got:
16//!
17//! ```text
18//! offset 0 gp_offset bytes into the save area of the next argument from a gpr
19//! offset 4 fp_offset bytes into the save area of the next argument from an xmm
20//! offset 8 overflow_arg_area the next argument that came in the caller's memory
21//! offset 16 reg_save_area the bottom of the save area
22//! ```
23//!
24//! `va_start` fills all four in. The two offsets do not start at zero: the arguments the signature
25//! does name took registers too, and they took the first ones, so each offset starts past them.
26//! `va_arg` is then one question asked at run time, which is whether the offset for its file has run
27//! off the end of the save area. If it has not, the argument is in the save area and the offset
28//! steps on by a slot. If it has, the argument is in the caller's memory and the overflow pointer
29//! steps on by a word instead.
30//!
31//! # Why the layout is exactly the psABI's and not a convenient one
32//!
33//! Nothing outside the function can see the save area, so its shape looks like a private decision.
34//! It is not one, because a `va_list` is a thing a program hands to another function, and the
35//! function it usually hands it to is `vfprintf` in the C library, which somebody else compiled and
36//! which walks the list by the rules in the psABI document. So the offsets are the document's
37//! offsets, the area is the document's one hundred and seventy six bytes, and the eight bytes
38//! between two general purpose slots and the sixteen between two vector ones are the document's too.
39//!
40//! The upper half of a vector slot is the document's too, and what is in it is the top of a
41//! `_Float128`. A slot is sixteen bytes wide because the register is, and a quad is the one type
42//! here that fills one, so the spill writes all sixteen bytes of every vector register and a
43//! `va_arg` of a quad reads all sixteen back. gcc writes the same sixteen with the same instruction,
44//! which is what makes a list built here readable by a walk somebody else compiled. Anything wider
45//! than a register would be a vector type, which is issue #200 and is not a thing yet.
46//!
47//! # What is here and what is next door
48//!
49//! `va_arg` becomes a compare and a branch, and this is where, because a rewrite that needs new
50//! blocks has to happen before selection for the reason [`crate::expand`] gives. Everything it needs
51//! is in the list it was handed, so it needs nothing from the frame and can run here.
52//!
53//! An aggregate read off a list is the same instruction under another name, because an aggregate is
54//! not a value and there is nothing for one result to be, so that one answers where the object is
55//! instead. Over two eightbytes it is class MEMORY whatever its members are, which means it is in
56//! the caller's argument area and there is no question to ask about which half of the walk it is
57//! in: the overflow pointer says where it is and steps on past it. Sixteen bytes and under arrived
58//! in registers, and then the question is the one a scalar asks, with two differences. The object
59//! takes a register of each file for each of its eightbytes, so the room in the save area has to be
60//! there for all of them at once and the offsets step on by all of them at once. And the halves of
61//! it in the save area are not next to each other, so the answer cannot be an address in the area:
62//! the eightbytes are copied out into a buffer of the function's own and the answer is that.
63//!
64//! Which file each eightbyte came from is the classification, which is an answer about a C type and
65//! not one the size and the alignment give. It arrives on the instruction, worked out by the front
66//! end, which is the last thing to hold a type. An object with no slots on it is one the
67//! classification sent to the argument area, and that is what tells the two halves below apart.
68//!
69//! `va_start` is the other way round. Three of the four fields it writes are distances into a frame
70//! that does not exist yet, so it stays an instruction as far as [`crate::lower`], which builds it
71//! out of the frame the way it builds an `alloca`. The spill that fills the save area is written
72//! there for the same reason.
73
74use rucc_base::float::Format;
75use rucc_ir::{
76 Block, Builder, Extra, Flags, Float, Func, Imm, Inst, InstData, IntPred, MemInfo, MemOrder,
77 Opcode, Restrict, Type, Value,
78};
79use rucc_target::{CallRegs, Slot};
80
81/// Where the count of general purpose register bytes already walked is.
82pub const GP_OFFSET: i64 = 0;
83/// Where the count of vector register bytes already walked is.
84pub const FP_OFFSET: i64 = 4;
85/// Where the pointer to the next argument in the caller's memory is.
86pub const OVERFLOW: i64 = 8;
87/// Where the pointer to the bottom of the register save area is.
88pub const SAVE_AREA: i64 = 16;
89/// How many bytes one `va_list` is, which is what a `va_copy` moves.
90pub const SIZE: u64 = 24;
91/// How wide the slot one vector register is saved in is, which is how wide the register is whatever
92/// this actually writes into it.
93pub const VECTOR_SLOT: u32 = 16;
94
95/// How big a callee's register save area is and where its two halves are.
96///
97/// Worked out from the convention rather than written down, so that a convention with a different
98/// number of argument registers gets an area the right size for it without anything here changing.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub struct Area {
101 /// How many bytes of it the general purpose registers take, which is also where the vector half
102 /// begins, since the general purpose half is first and starts at nothing.
103 pub floats_at: u32,
104 /// How many bytes the whole of it is.
105 pub size: u32,
106 /// How many registers of each file it holds, general purpose first.
107 counts: (u32, u32),
108 /// How far apart two general purpose slots are, which is a word.
109 word: u32,
110}
111
112impl Area {
113 /// The save area a variadic callee under that convention needs.
114 #[must_use]
115 pub fn of(conv: &CallRegs) -> Self {
116 let ints = u32::try_from(conv.int_args.len()).unwrap_or(0);
117 let floats = u32::try_from(conv.sse_args.len()).unwrap_or(0);
118 let floats_at = conv.word * ints;
119 Self {
120 floats_at,
121 size: floats_at + VECTOR_SLOT * floats,
122 counts: (ints, floats),
123 word: conv.word,
124 }
125 }
126
127 /// How far apart two of a file's slots are.
128 #[must_use]
129 pub fn stride(self, float: bool) -> u32 {
130 if float { VECTOR_SLOT } else { self.word }
131 }
132
133 /// Where a file's first slot is, which is what `va_start` writes into that file's field when
134 /// the signature named no argument that file carried.
135 #[must_use]
136 pub fn starts_at(self, float: bool) -> u32 {
137 if float { self.floats_at } else { 0 }
138 }
139
140 /// Where a file's slots end, which is where the vector half begins for the general purpose
141 /// file and the end of the whole area for the vector one.
142 ///
143 /// This is what an object taking more than one register of a file is measured against: the
144 /// psABI asks whether the offset is at or below the end less a slot for each register the
145 /// object wants, and one register of it is the same question [`Area::last`] asks.
146 #[must_use]
147 pub fn ends_at(self, float: bool) -> u32 {
148 if float { self.size } else { self.floats_at }
149 }
150
151 /// How many registers of a file the area holds.
152 #[must_use]
153 fn holds(self, float: bool) -> u32 {
154 if float { self.counts.1 } else { self.counts.0 }
155 }
156
157 /// The offset of a file's last slot, which is the threshold `va_arg` compares against.
158 ///
159 /// The last slot's own offset and not the end of the area, because an offset equal to the end
160 /// is one slot past the last argument while an offset a slot below the end is the last argument
161 /// itself. An empty file has no such offset and nothing here has one.
162 #[must_use]
163 pub fn last(self, float: bool) -> Option<u32> {
164 let last = self.holds(float).checked_sub(1)?;
165 Some(self.starts_at(float) + self.stride(float) * last)
166 }
167}
168
169/// Rewrites every `va_arg`, `va_copy` and `va_end` in the function, and leaves `va_start` alone.
170///
171/// Those three are the ones made only of reads and writes of a list some pointer already reaches,
172/// so none of them needs to know anything about the frame and all three can be done here.
173/// `va_start` is the one that does need the frame, and [`crate::lower`] has it.
174///
175/// A convention whose list is not the four field one is left alone entirely, and a function using
176/// one is then refused further down with the message about a rule that does not exist. Windows is
177/// the one such convention here: its list is a plain pointer, its callee spills its four register
178/// arguments into the shadow space the caller already reserved rather than into an area of its own,
179/// and none of the three rewrites below is right for any of that.
180pub fn lists(func: &mut Func, conv: &CallRegs) {
181 if conv.shared_positions {
182 return;
183 }
184 let area = Area::of(conv);
185 let found: Vec<Inst> =
186 func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
187 for inst in found {
188 match func[inst].opcode {
189 Opcode::VaArg => next(func, inst, area),
190 Opcode::VaObject => object(func, inst, area),
191 Opcode::VaCopy => copy(func, inst),
192 // Nothing at all, which is what the psABI says it is. The instruction was still worth
193 // emitting, because it says the list stops being read here, and here is where that
194 // stops being worth saying.
195 Opcode::VaEnd => func.remove_inst(inst),
196 _ => {}
197 }
198 }
199}
200
201/// One `va_arg`, as the branch on whether the argument it wants is still in the save area.
202///
203/// The block the instruction was in is cut in two at the instruction. What was above it stays where
204/// it is and gets the compare and the branch, what was below it moves into a new block that takes
205/// the address as a parameter, and the `va_arg` itself becomes the load at the top of that block.
206/// Turning it into the load rather than replacing it keeps the value the rest of the function reads
207/// the value it already read, so nothing has to be substituted anywhere, and the two paths meet at a
208/// block parameter because the IR has no variables for them to meet at.
209fn next(func: &mut Func, inst: Inst, area: Area) {
210 let Some(result) = func[inst].first_result else { return };
211 let Some(&list) = func[func[inst].args].first() else { return };
212 let ty = func[result].ty;
213 let Some(block) = func.block_of(inst) else { return };
214 let span = func.span(inst);
215 // A `long double` is class X87, which is a class with no register in the save area, so it is
216 // always in the caller's argument area and there is no question to ask about it. That is this
217 // walk with the register half deleted, which is little enough to be written out separately
218 // rather than folded in as a special case of a branch that is never taken.
219 if ty.is_float() && ty.bits() == 80 {
220 x87(func, inst, area);
221 return;
222 }
223 // A `_Float128` is the one value wider than a general purpose register that a single register
224 // still holds. It is class SSE followed by SSEUP, which name one vector register between them,
225 // so it walks the vector half the way a `double` does and takes the whole of a slot instead of
226 // the low half of one. Where it stops being a wider `double` is the caller's argument area,
227 // which gives it two words aligned to two rather than the one word every value the machine
228 // computes in gets.
229 let quad = ty.is_float() && ty.bits() == 128;
230 // A scalar of a width a register holds, which is every type the algorithm below is right about.
231 // An `__int128` takes two slots with an alignment rule of its own, which is a second algorithm
232 // rather than a wider reading of this one, so it is left alone here and refused by name further
233 // down.
234 if !quad
235 && (!ty.is_scalar() || ty.bits() > 64 || !(ty.is_int() || ty.is_float() || ty.is_ptr()))
236 {
237 return;
238 }
239 let float = ty.is_float();
240 let Some(last) = area.last(float) else { return };
241 let field = if float { FP_OFFSET } else { GP_OFFSET };
242
243 // Everything below the instruction, taken out before anything is built, because the builder
244 // appends to a block and this block has to end at the branch.
245 let rest: Vec<Inst> = func.insts(block).skip_while(|&at| at != inst).skip(1).collect();
246 let taken = func.create_block();
247 let overflowed = func.create_block();
248 let join = func.create_block();
249 let addr = func.append_param(join, Type::PTR);
250 func.remove_inst(inst);
251 for &at in &rest {
252 func.remove_inst(at);
253 }
254
255 // The question, in the block the `va_arg` used to be in. Unsigned, because an offset into the
256 // save area counts bytes and is never negative, and because what the field holds once the
257 // register arguments have all been walked is a number past the end rather than a small one.
258 let mut build = Builder::new(func, block).at(span);
259 let counter = offset(&mut build, list, field);
260 let walked = build.load(Type::int(32), counter, info(4, 4), Flags::default());
261 let end = build.iconst(Type::int(32), i128::from(last));
262 let inside = build.icmp(IntPred::Ule, walked, end);
263 build.br_if(inside, taken, &[], overflowed, &[]);
264
265 // The register path: the argument is in the save area at the offset the field holds, and the
266 // field steps on by one slot of its file.
267 let mut build = Builder::new(func, taken).at(span);
268 let base = offset(&mut build, list, SAVE_AREA);
269 let save = build.load(Type::PTR, base, info(8, 8), Flags::default());
270 let wide = build.unary(Opcode::ZExt, walked, Type::int(64));
271 let found = added(&mut build, save, wide);
272 let stride = build.iconst(Type::int(32), i128::from(area.stride(float)));
273 let stepped = build.binary(Opcode::Add, walked, stride, Flags::default());
274 let counter = offset(&mut build, list, field);
275 build.store(stepped, counter, info(4, 4), Flags::default());
276 build.jump(join, &[found]);
277
278 // The memory path: the argument is where the caller left it, and the pointer steps on past it.
279 // By a word for everything the machine computes in, because the caller's argument area is a run
280 // of whole words whatever is in them, and by two words rounded up to two for a quad, which is
281 // the slot the class gets from a function that names it as well as from one that does not.
282 let mut build = Builder::new(func, overflowed).at(span);
283 let (slot, want) = if quad {
284 (u64::from(VECTOR_SLOT), VECTOR_SLOT)
285 } else {
286 (u64::from(area.word), area.word)
287 };
288 let at = overflow(&mut build, list, area, slot, want);
289 let here = build.unary(Opcode::IntToPtr, at, Type::PTR);
290 build.jump(join, &[here]);
291
292 // And the load the program actually wrote, over the address the two paths agreed on, with
293 // everything that used to follow it behind it in the order it was written.
294 let bytes = ty.bits() / 8;
295 let mem = func.add_mem(info(u64::from(bytes), bytes));
296 let args = func.push_values(&[addr]);
297 let data = &mut func[inst];
298 data.opcode = Opcode::Load;
299 data.args = args;
300 data.extra = Extra::Mem(mem);
301 data.flags = data.flags.intersection(Flags::legal_on(Opcode::Load));
302 func.append_inst(join, inst);
303 for at in rest {
304 func.append_inst(join, at);
305 }
306}
307
308/// One `va_arg` of a `long double`, which is the memory half of the walk and nothing else.
309///
310/// The psABI gives a `long double` class X87 and there is no x87 register among the ones a variadic
311/// callee spills, so a `long double` passed to one is in the caller's argument area whatever else
312/// the call passed and however few arguments came before it. Nothing is asked, no block is made,
313/// and the overflow pointer is rounded up, read and stepped on.
314///
315/// Sixteen bytes and sixteen byte alignment are the psABI's numbers for the class rather than the
316/// type's own: the value is ten bytes of x87 and the argument slot it sits in is padded out to two
317/// words, which is why the load below is ten bytes wide and the step is sixteen.
318fn x87(func: &mut Func, inst: Inst, area: Area) {
319 /// What one of these takes in the argument area.
320 const SLOT: u64 = 16;
321 /// What the argument area aligns one to.
322 const ALIGN: u32 = 16;
323
324 let Some(result) = func[inst].first_result else { return };
325 let Some(&list) = func[func[inst].args].first() else { return };
326 let Some(block) = func.block_of(inst) else { return };
327 let bytes = func[result].ty.bits() / 8;
328 let span = func.span(inst);
329
330 // Everything below the instruction, taken out before anything is built, for the reason the
331 // branching walk takes it out: a builder appends to a block, and the instruction has to end up
332 // behind what is built and in front of what followed it.
333 let rest: Vec<Inst> = func.insts(block).skip_while(|&at| at != inst).skip(1).collect();
334 func.remove_inst(inst);
335 for &at in &rest {
336 func.remove_inst(at);
337 }
338
339 let mut build = Builder::new(func, block).at(span);
340 let at = overflow(&mut build, list, area, SLOT, ALIGN);
341 let addr = build.unary(Opcode::IntToPtr, at, Type::PTR);
342
343 let mem = func.add_mem(info(u64::from(bytes), ALIGN));
344 let args = func.push_values(&[addr]);
345 let data = &mut func[inst];
346 data.opcode = Opcode::Load;
347 data.args = args;
348 data.extra = Extra::Mem(mem);
349 data.flags = data.flags.intersection(Flags::legal_on(Opcode::Load));
350 func.append_inst(block, inst);
351 for at in rest {
352 func.append_inst(block, at);
353 }
354}
355
356/// One `va_object`, as the address the object can be read from.
357///
358/// Two shapes, and the slots on the instruction are what say which. An object with none is one the
359/// classification sent to the caller's argument area, which is what everything over two eightbytes
360/// is whatever its members are. There is no question to ask about that one: the overflow pointer
361/// says where it is and steps on past it, and no block is needed.
362///
363/// An object with slots arrived in registers, and that is the branch [`next`] builds for a scalar
364/// with the object's own two differences: the room has to be there for every one of its slots at
365/// once, and what is answered is a buffer the slots were copied into rather than an address in the
366/// save area, because two eightbytes of one object are not next to each other in there.
367///
368/// The address is answered rather than a copy of the object, which is what the instruction is for
369/// and what gcc does with the same argument. An object in the caller's memory is already somewhere
370/// addressable, and the copy the C standard describes is the assignment the caller of `va_arg`
371/// wrote, which the front end has already built around this.
372fn object(func: &mut Func, inst: Inst, area: Area) {
373 let Extra::VaObject(at) = func[inst].extra else { return };
374 let object = func[at];
375 let MemInfo { size, align, .. } = func[object.mem];
376 let slots: Vec<Slot> = func[object.slots].to_vec();
377 let Some(&list) = func[func[inst].args].first() else { return };
378 let Some(block) = func.block_of(inst) else { return };
379 if func[inst].first_result.is_none() || !fits(&slots, area) {
380 return;
381 }
382 let span = func.span(inst);
383
384 // The buffer the register form copies into, made before anything else, because an alloca of
385 // a fixed size belongs in the entry block and the walk below is built where the instruction
386 // is. It is as big as the slots reach rather than as big as the object, which is more for an
387 // object whose last eightbyte is a part of one: five bytes travel in a whole register and
388 // come out of the area as a whole register, so the buffer has eight bytes for them to land
389 // in and the three past the object are never read.
390 // It is also aligned to whatever the widest slot has to be stored at rather than to whatever
391 // the object asked for, which is the same number for every object a C program can write and is
392 // not the same statement. A slot holding a whole vector register moves as a `movaps`, and a
393 // `movaps` faults on an address that is not a multiple of sixteen, so the buffer says sixteen
394 // because the copy needs it and not because the type happened to ask.
395 let reach = slots.iter().map(|&slot| slot.offset() + width(slot)).max().unwrap_or(0);
396 let wants = slots
397 .iter()
398 .map(|&slot| slot_align(area, is_float(slot), width(slot)))
399 .max()
400 .unwrap_or(1)
401 .max(align);
402 let room = buffer(func, inst, reach.max(size), wants);
403
404 // Everything below the instruction, taken out before anything is built, because a builder
405 // appends to a block and the register form ends this one at a branch.
406 let rest: Vec<Inst> = func.insts(block).skip_while(|&at| at != inst).skip(1).collect();
407 func.remove_inst(inst);
408 for &at in &rest {
409 func.remove_inst(at);
410 }
411
412 let (ends, address) = match room {
413 Some(room) if !slots.is_empty() => {
414 let read = Read { list, area, slots: &slots, size, align, room, wants };
415 registers(func, block, inst, read)
416 }
417 _ => {
418 let mut build = Builder::new(func, block).at(span);
419 (block, overflow(&mut build, list, area, size, align))
420 }
421 };
422
423 // And the instruction itself is that address, so that everything reading it goes on reading
424 // the value it already read and nothing has to be substituted anywhere.
425 let args = func.push_values(&[address]);
426 let data = &mut func[inst];
427 data.opcode = Opcode::IntToPtr;
428 data.args = args;
429 data.extra = Extra::None;
430 data.flags = data.flags.intersection(Flags::legal_on(Opcode::IntToPtr));
431 func.append_inst(ends, inst);
432 for at in rest {
433 func.append_inst(ends, at);
434 }
435}
436
437/// One object read off one list, which is what both halves of the walk are about.
438#[derive(Clone, Copy)]
439struct Read<'a> {
440 /// The list it is read from.
441 list: Value,
442 /// The save area of the function doing the reading.
443 area: Area,
444 /// Which register each of the object's eightbytes arrived in, and empty for an object that
445 /// arrived in the caller's memory.
446 slots: &'a [Slot],
447 /// How many bytes the object is.
448 size: u64,
449 /// What it is aligned to, which is what the caller's argument area put it at.
450 align: u32,
451 /// The buffer of the function's own the register form copies the object into.
452 room: Value,
453 /// What that buffer is aligned to, which is the object's alignment or what the widest slot
454 /// needs, whichever is the larger.
455 wants: u32,
456}
457
458/// Whether the classification is one this knows how to read out of the save area.
459///
460/// A slot wider than a register or more of them than the area holds is a classification from some
461/// other machine or from a rule this has not been taught. Turning it down here leaves the
462/// instruction alone, and an instruction left alone is refused by name further down, which is a
463/// message about `va_arg` rather than whatever a half built walk would do at run time.
464fn fits(slots: &[Slot], area: Area) -> bool {
465 let mut counts = [0, 0];
466 for &slot in slots {
467 let float = is_float(slot);
468 if !in_a_register(slot) || width(slot) > u64::from(area.stride(float)) {
469 return false;
470 }
471 counts[usize::from(float)] += 1;
472 }
473 counts[0] <= area.holds(false) && counts[1] <= area.holds(true)
474}
475
476/// Whether a slot is one of the registers a variadic callee spills.
477///
478/// The width does not say on its own. A `long double` is ten bytes and would sit inside a vector
479/// slot with room to spare, and it is class X87, which has no register among the fourteen, so a
480/// classification carrying one is a classification this walk cannot read. The formats a vector
481/// register does hold are named rather than the ones it does not, so a format added later is one
482/// this leaves alone until somebody says where it travels.
483fn in_a_register(slot: Slot) -> bool {
484 match slot {
485 Slot::Integer { .. } => true,
486 Slot::Float { format, .. } => matches!(
487 format,
488 Format::Half | Format::BFloat16 | Format::Single | Format::Double | Format::Quad
489 ),
490 }
491}
492
493/// The register form: the room in the save area is asked about once per file, and the object is
494/// copied out of the area into a buffer when it is there and read from the caller's memory when it
495/// is not.
496///
497/// The question is asked once per file the object takes a register of, and both have to say yes,
498/// because the psABI puts the whole object in the caller's memory when there is not room in the
499/// area for all of it. A file the object takes nothing of has room by definition and is not asked
500/// about, which is every object of one class and is most of them.
501///
502/// Gives back the block the walk ends in and the address, as an integer, that the two paths agreed
503/// on.
504fn registers(func: &mut Func, block: Block, inst: Inst, read: Read<'_>) -> (Block, Value) {
505 let span = func.span(inst);
506 let area = read.area;
507 let counts = [taken_of(read.slots, false), taken_of(read.slots, true)];
508 let saved = func.create_block();
509 let overflowed = func.create_block();
510 let join = func.create_block();
511 let address = func.append_param(join, Type::int(64));
512
513 // The questions, each in its own block, because two of them are two branches and the second
514 // is only asked when the first said yes.
515 let asked: Vec<bool> =
516 [false, true].into_iter().filter(|&float| counts[usize::from(float)] > 0).collect();
517 let mut at = block;
518 for (index, &float) in asked.iter().enumerate() {
519 let next = if index + 1 == asked.len() { saved } else { func.create_block() };
520 // The psABI's own threshold: the end of the file's half of the area, less a slot for each
521 // register the object wants, so that an offset at it leaves room for all of them.
522 let room =
523 area.ends_at(float).saturating_sub(area.stride(float) * counts[usize::from(float)]);
524 let mut build = Builder::new(func, at).at(span);
525 let counter = offset(&mut build, read.list, field_of(float));
526 let walked = build.load(Type::int(32), counter, info(4, 4), Flags::default());
527 let end = build.iconst(Type::int(32), i128::from(room));
528 let inside = build.icmp(IntPred::Ule, walked, end);
529 build.br_if(inside, next, &[], overflowed, &[]);
530 at = next;
531 }
532
533 let mut build = Builder::new(func, saved).at(span);
534 let found = copied(&mut build, read, counts);
535 build.jump(join, &[found]);
536
537 let mut build = Builder::new(func, overflowed).at(span);
538 let here = overflow(&mut build, read.list, area, read.size, read.align);
539 build.jump(join, &[here]);
540
541 (join, address)
542}
543
544/// The object copied out of the save area into the buffer, as the address of the buffer.
545///
546/// A buffer and not an address in the area because the eightbytes of one object are not next to
547/// each other in there: two integer eightbytes are eight bytes apart and two vector ones are
548/// sixteen, and an object of one of each has them in different halves of the area entirely. So
549/// there is nowhere in the area the object is, and the one place it can be made to be is somewhere
550/// else.
551fn copied(build: &mut Builder<'_>, read: Read<'_>, counts: [u32; 2]) -> Value {
552 let area = read.area;
553 let base = offset(build, read.list, SAVE_AREA);
554 let save = build.load(Type::PTR, base, info(8, 8), Flags::default());
555
556 // Where each file's next slot is, which is the one thing the offsets in the list say, and the
557 // counter itself, which is what steps on by every slot the object took of that file.
558 let mut walked = [None, None];
559 let mut nexts = [None, None];
560 for float in [false, true] {
561 let file = usize::from(float);
562 if counts[file] == 0 {
563 continue;
564 }
565 let counter = offset(build, read.list, field_of(float));
566 let read = build.load(Type::int(32), counter, info(4, 4), Flags::default());
567 let wide = build.unary(Opcode::ZExt, read, Type::int(64));
568 walked[file] = Some(read);
569 nexts[file] = Some(added(build, save, wide));
570 }
571
572 let mut seen = [0, 0];
573 for &slot in read.slots {
574 let float = is_float(slot);
575 let file = usize::from(float);
576 let Some(from) = nexts[file] else { continue };
577 let step = i64::from(area.stride(float) * seen[file]);
578 seen[file] += 1;
579 let bytes = width(slot);
580 let ty = moved_as(area, float, bytes);
581 let aligned = slot_align(area, float, bytes);
582 let at = offset(build, from, step);
583 let value = build.load(ty, at, info(bytes, aligned), Flags::default());
584 let into = offset(build, read.room, i64::try_from(slot.offset()).unwrap_or(0));
585 let holds = info(bytes, part(read.wants, slot.offset()));
586 build.store(value, into, holds, Flags::default());
587 }
588
589 // And the counters step on by every slot the object took, since the whole of it came out of
590 // the area and the argument behind it starts past all of it.
591 for float in [false, true] {
592 let file = usize::from(float);
593 let Some(counter) = walked[file] else { continue };
594 let by = build.iconst(Type::int(32), i128::from(area.stride(float) * counts[file]));
595 let stepped = build.binary(Opcode::Add, counter, by, Flags::default());
596 let at = offset(build, read.list, field_of(float));
597 build.store(stepped, at, info(4, 4), Flags::default());
598 }
599 build.unary(Opcode::PtrToInt, read.room, Type::int(64))
600}
601
602/// A buffer at the front of the entry block, which is where an alloca of a fixed size belongs.
603///
604/// Not where the walk is, because a walk inside a loop would then be an alloca inside a loop,
605/// which is a frame that grows every time round. One buffer per `va_arg` of an object, made once
606/// and written every time the object is read, which is what the front end would have written if
607/// the temporary had a name.
608fn buffer(func: &mut Func, inst: Inst, size: u64, align: u32) -> Option<Value> {
609 let entry = func.entry()?;
610 let span = func.span(inst);
611 let mem = func.add_mem(info(size, align.max(1)));
612 let data = InstData { extra: Extra::Mem(mem), ..InstData::new(Opcode::Alloca) };
613 let made = func.create_inst(data, &[Type::PTR], span);
614 let first = func.insts(entry).next();
615 match first {
616 Some(first) => func.insert_before(made, first),
617 None => func.append_inst(entry, made),
618 }
619 func[made].first_result
620}
621
622/// Where the argument the caller left in memory is, with the overflow pointer stepped on past it,
623/// as an integer address.
624///
625/// The pointer is rounded up first for an object that wants more alignment than a word. The
626/// argument area is a run of words, so anything asking for eight or less is where it is already,
627/// and anything asking for more was put at the next multiple of what it asked for by whoever
628/// passed it.
629fn overflow(build: &mut Builder<'_>, list: Value, area: Area, size: u64, align: u32) -> Value {
630 let word = u64::from(area.word);
631 let wide = Type::int(64);
632 let pointer = offset(build, list, OVERFLOW);
633 let here = build.load(Type::PTR, pointer, info(word, area.word), Flags::default());
634
635 // As an integer, because rounding up is an add and a mask and neither is a thing to do to a
636 // pointer. Both casts are free: the two are the same bits on this machine and nothing is
637 // written for either.
638 let mut at = build.unary(Opcode::PtrToInt, here, wide);
639 if u64::from(align) > word {
640 // Up to the next multiple of a power of two, which is the round up every alignment is.
641 // The mask is the negative of the alignment because that is what the complement of one
642 // less than it comes to, and writing it that way keeps it inside a signed sixty four bit
643 // constant.
644 let bump = build.iconst(wide, i128::from(align) - 1);
645 at = build.binary(Opcode::Add, at, bump, Flags::default());
646 let mask = build.iconst(wide, -i128::from(align));
647 at = build.binary(Opcode::And, at, mask, Flags::default());
648 }
649
650 // Past it, rounded up to a whole number of words, because the argument area holds words and
651 // the argument behind this one starts at one of them.
652 let by = build.iconst(wide, i128::from(size.next_multiple_of(word)));
653 let onward = build.binary(Opcode::Add, at, by, Flags::default());
654 let onward = build.unary(Opcode::IntToPtr, onward, Type::PTR);
655 build.store(onward, pointer, info(word, area.word), Flags::default());
656 at
657}
658
659/// Which of the two counters a file's slots are walked with.
660fn field_of(float: bool) -> i64 {
661 if float { FP_OFFSET } else { GP_OFFSET }
662}
663
664/// Whether a slot is one of the vector file's.
665fn is_float(slot: Slot) -> bool {
666 matches!(slot, Slot::Float { .. })
667}
668
669/// How many registers of a file an object takes.
670fn taken_of(slots: &[Slot], float: bool) -> u32 {
671 u32::try_from(slots.iter().filter(|&&slot| is_float(slot) == float).count()).unwrap_or(0)
672}
673
674/// What one slot's bytes are moved as.
675///
676/// An integer of the slot's width whatever file it came from, because what this is is a copy of the
677/// object's bytes and nothing here reads them as anything. A slot the whole width of a vector
678/// register is the exception and has to be: there is no integer that wide on this machine, and the
679/// file the bytes are already in is the one that moves all sixteen of them at once.
680fn moved_as(area: Area, float: bool, bytes: u64) -> Type {
681 if float && bytes > u64::from(area.word) {
682 return Type::float(Float::F128);
683 }
684 Type::int(u32::try_from(bytes).unwrap_or(1) * 8)
685}
686
687/// What the address of a slot in the save area is known to be aligned to.
688///
689/// The area begins on a vector slot boundary and every slot in it is a whole number of its file's
690/// strides along from there, so a value as wide as its file's stride sits at a multiple of the
691/// stride and everything narrower sits at a multiple of a word. The wide case is the one that has
692/// to be right, since what moves a whole vector register is a `movaps` and a `movaps` faults on an
693/// address that is not a multiple of sixteen rather than being slow about it.
694fn slot_align(area: Area, float: bool, bytes: u64) -> u32 {
695 if bytes > u64::from(area.word) { area.stride(float) } else { area.word }
696}
697
698/// How many bytes one slot moves, which is its own width rounded up to one the machine has a load
699/// for.
700fn width(slot: Slot) -> u64 {
701 match slot {
702 Slot::Integer { size, .. } => u64::from(size.next_power_of_two().clamp(1, 8)),
703 Slot::Float { format, .. } => u64::from(format.width()).div_ceil(8),
704 }
705}
706
707/// What a part of an object at that offset is aligned to, which is what the object is aligned to
708/// for the part at the front of it and how far into the object the part sits for every other.
709fn part(align: u32, offset: u64) -> u32 {
710 let align = align.max(1);
711 if offset == 0 {
712 return align;
713 }
714 u32::try_from(1_u64 << offset.trailing_zeros()).unwrap_or(align).min(align)
715}
716
717/// One `va_copy`, as the fields of one list moved into another.
718///
719/// A list is those fields and holds nothing anywhere else, so copying it is copying them, and three
720/// words move as three words rather than as a call to `memcpy`, which is a name this compiler
721/// cannot emit yet and would be the wrong answer for three words in any case.
722///
723/// Every read is built before any write, so that a list copied onto itself, which is legal and
724/// useless, moves what it held rather than what it has just been given.
725fn copy(func: &mut Func, inst: Inst) {
726 let [into, from] = func[func[inst].args] else { return };
727 let mut moved = Vec::new();
728 for word in 0..SIZE / 8 {
729 let step = i64::try_from(word * 8).unwrap_or(0);
730 let there = field(func, inst, from, step);
731 let mem = func.add_mem(info(8, 8));
732 let args = func.push_values(&[there]);
733 let data = InstData { args, extra: Extra::Mem(mem), ..InstData::new(Opcode::Load) };
734 moved.push((ahead(func, inst, data, Type::int(64)), step));
735 }
736 for (read, step) in moved {
737 let here = field(func, inst, into, step);
738 let mem = func.add_mem(info(8, 8));
739 let args = func.push_values(&[read, here]);
740 let data = InstData { args, extra: Extra::Mem(mem), ..InstData::new(Opcode::Store) };
741 let span = func.span(inst);
742 let made = func.create_inst(data, &[], span);
743 func.insert_before(made, inst);
744 }
745 func.remove_inst(inst);
746}
747
748/// The address of a field of a list, written in front of an instruction, or the list itself for the
749/// field at the front of it.
750fn field(func: &mut Func, inst: Inst, list: Value, at: i64) -> Value {
751 if at == 0 {
752 return list;
753 }
754 let extra = Extra::Imm(func.add_imm(Imm::int(i128::from(at), Type::int(64))));
755 let step =
756 ahead(func, inst, InstData { extra, ..InstData::new(Opcode::IConst) }, Type::int(64));
757 let args = func.push_values(&[list, step]);
758 ahead(func, inst, InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR)
759}
760
761/// Puts an instruction in front of another one and gives back the value it produces.
762fn ahead(func: &mut Func, inst: Inst, data: InstData, ty: Type) -> Value {
763 let span = func.span(inst);
764 let made = func.create_inst(data, &[ty], span);
765 func.insert_before(made, inst);
766 func[made].first_result.expect("an instruction created with one result has one")
767}
768
769/// The address of a field of a list in a block being filled, or the list itself for the field at
770/// the front of it.
771fn offset(build: &mut Builder<'_>, list: Value, at: i64) -> Value {
772 if at == 0 {
773 return list;
774 }
775 let step = build.iconst(Type::int(64), i128::from(at));
776 added(build, list, step)
777}
778
779/// A pointer with an integer added to it.
780fn added(build: &mut Builder<'_>, pointer: Value, by: Value) -> Value {
781 let args = build.func().push_values(&[pointer, by]);
782 build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR)
783}
784
785/// An ordinary read or write of that many bytes, aligned that far.
786///
787/// Every access this pass makes is to a field of a list or to an argument, and none of them is
788/// atomic or has anything to say about aliasing.
789fn info(size: u64, align: u32) -> MemInfo {
790 MemInfo {
791 size,
792 align,
793 order: MemOrder::NotAtomic,
794 tbaa: None,
795 owns: 0,
796 restrict: Restrict::NONE,
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use rucc_base::Interner;
803 use rucc_base::float::Format;
804 use rucc_ir::{Builder, Extra, Func, InstData, Module, Opcode, Signature, Type, VaInfo};
805 use rucc_target::x86_64::{SYSV, WIN64};
806 use rucc_target::{Arch, Env, Os, Slot, TargetInfo, Triple};
807
808 use super::{Area, FP_OFFSET, GP_OFFSET, OVERFLOW, SAVE_AREA, SIZE, VECTOR_SLOT, lists};
809
810 fn target() -> TargetInfo {
811 TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
812 }
813
814 /// `T f(va_list *ap) { return va_arg(*ap, T); }`, or the same shape over whichever of the
815 /// family is asked for, with the list arriving as the pointer it has decayed to by the time
816 /// anything reads it.
817 fn built(opcode: Opcode, ty: Type, lists: usize) -> (Interner, Func) {
818 let mut names = Interner::new();
819 let params = vec![Type::PTR; lists];
820 let mut signature = Signature::new().with_params(¶ms);
821 if !ty.is_void() {
822 signature = signature.with_returns(&[ty]);
823 }
824 let mut func = Func::new(names.intern("f"), signature);
825 let entry = func.create_block();
826 let args: Vec<_> = params.iter().map(|&ty| func.append_param(entry, ty)).collect();
827
828 let mut build = Builder::new(&mut func, entry);
829 let list = build.func().push_values(&args);
830 if ty.is_void() {
831 build.inst(InstData { args: list, ..InstData::new(opcode) }, &[]);
832 build.ret(&[]);
833 } else {
834 let got = build.value(InstData { args: list, ..InstData::new(opcode) }, ty);
835 build.ret(&[got]);
836 }
837 (names, func)
838 }
839
840 fn printed(func: &Func, names: &mut Interner) -> String {
841 let module = Module::new(names.intern("va.c"), &target());
842 rucc_ir::print_func(&module, func, names)
843 }
844
845 fn valid(func: &Func, names: &mut Interner) {
846 let module = Module::new(names.intern("va.c"), &target());
847 rucc_ir::verify_func(&module, func, names).expect("the rewrite builds valid IR");
848 }
849
850 /// The numbers in this test are the psABI's own, written out rather than computed, because the
851 /// whole point of the layout is that it is the document's and not a convenient one. A version
852 /// of [`Area`] that worked them out differently would agree with itself and disagree with the C
853 /// library, and this is what would notice.
854 #[test]
855 fn the_save_area_is_the_one_the_document_describes() {
856 let area = Area::of(&SYSV);
857 assert_eq!(area.floats_at, 48, "six general purpose registers of eight bytes");
858 assert_eq!(area.size, 176, "and eight vector ones of sixteen");
859 assert_eq!(area.stride(false), 8);
860 assert_eq!(area.stride(true), VECTOR_SLOT);
861 assert_eq!(area.starts_at(false), 0);
862 assert_eq!(area.starts_at(true), 48);
863 // The last slot's own offset and not the end of the area, which is what `va_arg` compares
864 // against: an offset equal to the end is one slot past the last argument.
865 assert_eq!(area.last(false), Some(40));
866 assert_eq!(area.last(true), Some(160));
867 }
868
869 /// And the four fields, for the same reason.
870 #[test]
871 fn a_list_is_the_four_fields_the_document_describes() {
872 assert_eq!((GP_OFFSET, FP_OFFSET, OVERFLOW, SAVE_AREA), (0, 4, 8, 16));
873 assert_eq!(SIZE, 24);
874 }
875
876 #[test]
877 fn a_va_arg_becomes_the_branch_on_whether_the_argument_is_still_in_the_save_area() {
878 let (mut names, mut func) = built(Opcode::VaArg, Type::int(32), 1);
879 let before = func.blocks().count();
880 lists(&mut func, &SYSV);
881 assert_eq!(func.blocks().count(), before + 3, "one for each path and one they meet at");
882
883 let text = printed(&func, &mut names);
884 assert!(!text.contains("va_arg"), "the va_arg is gone: {text}");
885 assert!(text.contains("icmp ule"), "the threshold is a comparison: {text}");
886 assert!(text.contains("br_if"), "and it is branched on: {text}");
887 valid(&func, &mut names);
888 }
889
890 /// Which field it walks is the whole of the difference between the two files, and getting it
891 /// backwards is a program that reads its integers out of the vector half.
892 #[test]
893 fn which_half_of_the_area_is_walked_is_the_type_s_answer() {
894 for (ty, last, stride) in
895 [(Type::int(64), 40, 8), (Type::float(rucc_ir::Float::F64), 160, 16)]
896 {
897 let (mut names, mut func) = built(Opcode::VaArg, ty, 1);
898 lists(&mut func, &SYSV);
899 let text = printed(&func, &mut names);
900 assert!(text.contains(&format!("iconst.i32 {last}")), "{ty:?} stops at {last}: {text}");
901 assert!(text.contains(&format!("iconst.i32 {stride}")), "and steps by it: {text}");
902 }
903 }
904
905 /// The value the rest of the function reads has to stay the value it already read, since the
906 /// rewrite substitutes nothing anywhere. It stays it by the `va_arg` becoming the load rather
907 /// than being replaced by one, so the instruction is the same instruction under a new opcode
908 /// and in a new block.
909 #[test]
910 fn what_reads_the_argument_reads_the_same_value_it_did_before() {
911 let (mut names, mut func) = built(Opcode::VaArg, Type::int(32), 1);
912 let entry = func.entry().expect("an entry block");
913 let inst = func.insts(entry).next().expect("the va_arg is first");
914 let read = func[inst].first_result.expect("it produces the argument");
915
916 lists(&mut func, &SYSV);
917 assert_eq!(func[inst].opcode, Opcode::Load, "the same instruction, lowered");
918 assert_eq!(func[inst].first_result, Some(read), "producing the same value");
919 assert_ne!(func.block_of(inst), Some(entry), "in the block the two paths meet at");
920 valid(&func, &mut names);
921 }
922
923 #[test]
924 fn a_va_end_is_nothing_at_all() {
925 let (mut names, mut func) = built(Opcode::VaEnd, Type::VOID, 1);
926 lists(&mut func, &SYSV);
927 let text = printed(&func, &mut names);
928 assert!(!text.contains("va_end"), "{text}");
929 assert_eq!(func.blocks().count(), 1, "and needs no block: {text}");
930 valid(&func, &mut names);
931 }
932
933 /// Three words and no branch, because a list is three words and holds nothing anywhere else.
934 #[test]
935 fn a_va_copy_is_the_list_moved_a_word_at_a_time() {
936 let (mut names, mut func) = built(Opcode::VaCopy, Type::VOID, 2);
937 lists(&mut func, &SYSV);
938 let text = printed(&func, &mut names);
939 assert!(!text.contains("va_copy"), "{text}");
940 assert_eq!(text.matches("load.i64").count(), 3, "{text}");
941 assert_eq!(text.matches("store").count(), 3, "{text}");
942 assert_eq!(func.blocks().count(), 1, "and needs no block: {text}");
943 valid(&func, &mut names);
944 }
945
946 /// Every read before every write, so that `va_copy(ap, ap)` moves what the list held rather
947 /// than what it has just been given. Useless and legal, which is exactly the combination that
948 /// gets written once and never tested anywhere else.
949 #[test]
950 fn a_list_copied_onto_itself_moves_what_it_held() {
951 let (mut names, mut func) = built(Opcode::VaCopy, Type::VOID, 1);
952 // One parameter, so both operands of the copy are the same list. The builder above pushes
953 // as many operands as there are parameters, so the second is added here.
954 let entry = func.entry().expect("an entry block");
955 let inst = func.insts(entry).next().expect("the copy is first");
956 let list = func[func[inst].args][0];
957 let args = func.push_values(&[list, list]);
958 func[inst].args = args;
959
960 lists(&mut func, &SYSV);
961 let text = printed(&func, &mut names);
962 let first = text.find("store").expect("a write");
963 let last = text.rfind("load.i64").expect("a read");
964 assert!(last < first, "every read is above every write: {text}");
965 valid(&func, &mut names);
966 }
967
968 /// `struct s f(va_list *ap) { return va_arg(*ap, struct s); }`, where the structure is that
969 /// many bytes wanting that much alignment and arrived in those registers. The object form of
970 /// the instruction rather than the value one, because an aggregate is not a value and answers
971 /// where it is instead.
972 ///
973 /// No slots is the object the classification sent to the caller's argument area, which is what
974 /// everything over two eightbytes is.
975 fn object(size: u64, align: u32, slots: &[Slot]) -> (Interner, Func) {
976 let mut names = Interner::new();
977 let signature = Signature::new().with_params(&[Type::PTR]).with_returns(&[Type::PTR]);
978 let mut func = Func::new(names.intern("f"), signature);
979 let entry = func.create_block();
980 let list = func.append_param(entry, Type::PTR);
981 let mem = func.add_mem(super::info(size, align));
982 let slots = func.push_slots(slots);
983 let at = func.add_va_object(VaInfo { mem, slots });
984 let mut build = Builder::new(&mut func, entry);
985 let args = build.func().push_values(&[list]);
986 let data = InstData { args, extra: Extra::VaObject(at), ..InstData::new(Opcode::VaObject) };
987 let got = build.value(data, Type::PTR);
988 build.ret(&[got]);
989 (names, func)
990 }
991
992 /// One eightbyte of an object in the general purpose file, at that offset.
993 fn gpr(offset: u64, size: u32) -> Slot {
994 Slot::Integer { offset, size }
995 }
996
997 /// One in the vector file, holding a `double`, which is what a whole eightbyte of floating
998 /// point data is read as whichever way the members divide it up.
999 fn sse(offset: u64) -> Slot {
1000 Slot::Float { offset, format: Format::Double }
1001 }
1002
1003 /// Over two eightbytes is class MEMORY whatever the members are, so there is one place it can
1004 /// be and no question to ask about which.
1005 #[test]
1006 fn an_object_too_big_for_the_registers_is_read_out_of_the_caller_s_memory() {
1007 let (mut names, mut func) = object(24, 8, &[]);
1008 lists(&mut func, &SYSV);
1009 let text = printed(&func, &mut names);
1010 assert!(!text.contains("va_object"), "{text}");
1011 assert_eq!(func.blocks().count(), 1, "no branch, so no new block: {text}");
1012 assert!(text.contains("iconst.i64 8"), "the overflow field is at eight: {text}");
1013 assert!(text.contains("iconst.i64 24"), "and the pointer steps past the object: {text}");
1014 assert!(!text.contains("gp_offset"), "{text}");
1015 valid(&func, &mut names);
1016 }
1017
1018 /// The size the pointer steps on by is the size rounded up to a word, because the argument
1019 /// area holds words and the argument behind this one starts at one of them.
1020 #[test]
1021 fn a_size_that_is_not_a_whole_number_of_words_steps_on_by_the_next_one() {
1022 let (mut names, mut func) = object(28, 4, &[]);
1023 lists(&mut func, &SYSV);
1024 let text = printed(&func, &mut names);
1025 assert!(text.contains("iconst.i64 32"), "twenty eight bytes step on by thirty two: {text}");
1026 valid(&func, &mut names);
1027 }
1028
1029 /// An object wanting more than a word is at the next multiple of what it wants, and one
1030 /// wanting a word or less is where the pointer already is, since the area is a run of words.
1031 #[test]
1032 fn an_object_wanting_more_alignment_than_a_word_is_rounded_up_to_it() {
1033 let (mut names, mut func) = object(32, 16, &[]);
1034 lists(&mut func, &SYSV);
1035 let text = printed(&func, &mut names);
1036 assert!(text.contains("iconst.i64 15"), "up to the next sixteen: {text}");
1037 assert!(text.contains("iconst.i64 -16"), "and down to a multiple of it: {text}");
1038 assert!(text.contains(" = and "), "which is an add and a mask: {text}");
1039 valid(&func, &mut names);
1040
1041 let (mut names, mut func) = object(24, 8, &[]);
1042 lists(&mut func, &SYSV);
1043 assert!(!printed(&func, &mut names).contains(" = and "), "a word wants no rounding");
1044 }
1045
1046 /// An object that arrived in registers is in the save area, and reading it is the branch a
1047 /// scalar asks with the object's own threshold: two eightbytes want two slots, so an offset
1048 /// that leaves room for one is not room enough.
1049 #[test]
1050 fn an_object_that_arrived_in_registers_is_copied_out_of_the_save_area() {
1051 let (mut names, mut func) = object(16, 8, &[gpr(0, 8), gpr(8, 8)]);
1052 let before = func.blocks().count();
1053 lists(&mut func, &SYSV);
1054 assert_eq!(func.blocks().count(), before + 3, "one for each path and one they meet at");
1055
1056 let text = printed(&func, &mut names);
1057 assert!(!text.contains("va_object"), "{text}");
1058 assert!(text.contains("iconst.i32 32"), "forty eight less two slots: {text}");
1059 assert!(text.contains("icmp ule"), "which is the threshold: {text}");
1060 assert!(text.contains("alloca, size 16"), "the object lands in a buffer: {text}");
1061 assert!(text.contains("iconst.i32 16"), "and the counter steps by both slots: {text}");
1062 valid(&func, &mut names);
1063 }
1064
1065 /// An object of one eightbyte of each file has to have room in both halves of the area, and
1066 /// the psABI puts the whole of it in the caller's memory when either of them is out. So there
1067 /// are two questions, and the second is only asked when the first said yes.
1068 #[test]
1069 fn an_object_in_both_files_asks_about_both_of_them() {
1070 let (mut names, mut func) = object(16, 8, &[gpr(0, 8), sse(8)]);
1071 lists(&mut func, &SYSV);
1072 let text = printed(&func, &mut names);
1073 assert_eq!(text.matches("br_if").count(), 2, "one question per file: {text}");
1074 assert!(text.contains("iconst.i32 40"), "forty eight less one slot: {text}");
1075 assert!(text.contains("iconst.i32 160"), "and a hundred and seventy six less one: {text}");
1076 assert!(text.contains("iconst.i32 8"), "each counter steps by its own slot: {text}");
1077 valid(&func, &mut names);
1078 }
1079
1080 /// An object whose last eightbyte is a part of one still comes out of the area as a whole
1081 /// register, so the buffer has room for the whole register and the bytes past the object are
1082 /// never read.
1083 #[test]
1084 fn the_buffer_is_as_big_as_the_registers_reach() {
1085 let (mut names, mut func) = object(5, 1, &[gpr(0, 5)]);
1086 lists(&mut func, &SYSV);
1087 let text = printed(&func, &mut names);
1088 assert!(text.contains("alloca, size 8"), "five bytes travel in a whole register: {text}");
1089 valid(&func, &mut names);
1090 }
1091
1092 /// A classification this cannot read out of the area is left alone, which is what makes the
1093 /// function refused by name further down rather than compiled into half a walk.
1094 ///
1095 /// Class X87 is the one to ask about, because the width alone would say yes: ten bytes sit
1096 /// inside a vector slot with room to spare, and there is no x87 register among the fourteen a
1097 /// variadic callee spills, so there is nothing in the area for this to read.
1098 #[test]
1099 fn a_classification_that_does_not_fit_the_area_is_left_alone() {
1100 let x87 = [Slot::Float { offset: 0, format: Format::X87Extended }];
1101 let (mut names, mut func) = object(16, 16, &x87);
1102 let before = printed(&func, &mut names);
1103 lists(&mut func, &SYSV);
1104 assert_eq!(printed(&func, &mut names), before);
1105 }
1106
1107 /// A `_Float128` walks the vector half with a slot of the whole register.
1108 ///
1109 /// One register and not two, which is the same answer the classification gives a quad passed to
1110 /// a function that names it: the offset stops at the last slot rather than the last but one,
1111 /// and it steps on by sixteen. What is read is sixteen bytes of float, which is a `movaps`
1112 /// further down and is the instruction gcc reads the same slot with.
1113 #[test]
1114 fn a_quad_takes_a_whole_vector_slot_of_the_save_area() {
1115 let (mut names, mut func) = built(Opcode::VaArg, Type::float(rucc_ir::Float::F128), 1);
1116 lists(&mut func, &SYSV);
1117 valid(&func, &mut names);
1118 let text = printed(&func, &mut names);
1119 assert!(!text.contains("va_arg"), "the va_arg is gone: {text}");
1120 assert!(text.contains("iconst.i32 160"), "a hundred and seventy six less one slot: {text}");
1121 assert!(text.contains("iconst.i32 16"), "and the counter steps by a whole one: {text}");
1122 assert!(text.contains("load.f128"), "read as the sixteen bytes it is: {text}");
1123 }
1124
1125 /// And the argument area gives it two words aligned to two, which is where it stops being a
1126 /// wider `double`. Every other value the machine computes in is where the pointer already is
1127 /// and steps it on by a word.
1128 #[test]
1129 fn a_quad_the_registers_ran_out_before_is_rounded_up_to_sixteen() {
1130 let (mut names, mut func) = built(Opcode::VaArg, Type::float(rucc_ir::Float::F128), 1);
1131 lists(&mut func, &SYSV);
1132 let text = printed(&func, &mut names);
1133 assert!(text.contains("iconst.i64 15"), "up to the next sixteen: {text}");
1134 assert!(text.contains("iconst.i64 -16"), "and down to a multiple of it: {text}");
1135 assert!(text.contains(" = and "), "which is an add and a mask: {text}");
1136
1137 let (mut names, mut func) = built(Opcode::VaArg, Type::float(rucc_ir::Float::F64), 1);
1138 lists(&mut func, &SYSV);
1139 let text = printed(&func, &mut names);
1140 assert!(!text.contains(" = and "), "a double is where the pointer already is: {text}");
1141 assert!(text.contains("iconst.i64 8"), "and steps it on by a word: {text}");
1142 }
1143
1144 /// An object holding a quad is one slot of the vector file, so the question is a single one
1145 /// and the copy moves all sixteen bytes at once.
1146 ///
1147 /// The buffer it lands in is sixteen byte aligned, which is what the store needs rather than
1148 /// what the object asked for, although for this object the two are the same number.
1149 #[test]
1150 fn an_object_holding_a_quad_is_copied_out_as_one_whole_register() {
1151 let quad = [Slot::Float { offset: 0, format: Format::Quad }];
1152 let (mut names, mut func) = object(16, 16, &quad);
1153 lists(&mut func, &SYSV);
1154 valid(&func, &mut names);
1155 let text = printed(&func, &mut names);
1156 assert!(!text.contains("va_object"), "{text}");
1157 assert_eq!(text.matches("br_if").count(), 1, "one file, so one question: {text}");
1158 assert!(text.contains("iconst.i32 160"), "a hundred and seventy six less one slot: {text}");
1159 assert!(text.contains("load.f128"), "moved as the register it is in: {text}");
1160 assert!(text.contains("alloca, size 16, align 16"), "a buffer a movaps accepts: {text}");
1161 }
1162
1163 /// What reads the object goes on reading the value it already read, the same way it does for a
1164 /// value, and for the same reason: the instruction becomes the address rather than being
1165 /// replaced by one, so nothing has to be substituted anywhere.
1166 #[test]
1167 fn what_reads_the_object_reads_the_same_value_it_did_before() {
1168 for slots in [&[][..], &[gpr(0, 8), gpr(8, 8)][..]] {
1169 let (mut names, mut func) = object(if slots.is_empty() { 24 } else { 16 }, 8, slots);
1170 let entry = func.entry().expect("an entry block");
1171 let inst = func.insts(entry).next().expect("the va_object is first");
1172 let read = func[inst].first_result.expect("it answers an address");
1173
1174 lists(&mut func, &SYSV);
1175 assert_eq!(func[inst].opcode, Opcode::IntToPtr, "the same instruction, lowered");
1176 assert_eq!(func[inst].first_result, Some(read), "producing the same value");
1177 valid(&func, &mut names);
1178 }
1179 }
1180
1181 /// Windows has a list of a different shape and an algorithm to match, and none of the rewrites
1182 /// here is right for any of it. Leaving it alone is what makes the function refused by name
1183 /// further down rather than compiled into a walk over an area that was never filled in.
1184 #[test]
1185 fn a_convention_whose_list_is_not_this_one_is_left_alone() {
1186 let (mut names, mut func) = built(Opcode::VaArg, Type::int(32), 1);
1187 let before = printed(&func, &mut names);
1188 lists(&mut func, &WIN64);
1189 assert_eq!(printed(&func, &mut names), before);
1190 }
1191
1192 /// A width the algorithm is not right about is left alone for the same reason. An `__int128`
1193 /// takes two slots under an alignment rule of its own, which is a second algorithm and not a
1194 /// wider reading of this one, so it stays exactly as it was and is refused by name later.
1195 #[test]
1196 fn a_type_that_does_not_travel_in_one_slot_is_left_alone() {
1197 let ty = Type::int(128);
1198 let (mut names, mut func) = built(Opcode::VaArg, ty, 1);
1199 let before = printed(&func, &mut names);
1200 lists(&mut func, &SYSV);
1201 assert_eq!(printed(&func, &mut names), before, "{ty:?}");
1202 }
1203
1204 /// A `long double` is class X87, and the class has no register among the fourteen a variadic
1205 /// callee spills, so one is in the caller's argument area whether or not anything came before
1206 /// it. What that means for the rewrite is that the question `va_arg` usually asks has a known
1207 /// answer, so there is no compare, no branch and no join: one block, the overflow pointer
1208 /// rounded up to sixteen and stepped on by sixteen, and the load.
1209 #[test]
1210 fn a_long_double_is_read_straight_out_of_the_callers_argument_area() {
1211 let (mut names, mut func) = built(Opcode::VaArg, Type::float(rucc_ir::Float::F80), 1);
1212 lists(&mut func, &SYSV);
1213 valid(&func, &mut names);
1214 let text = printed(&func, &mut names);
1215 assert!(!text.contains("va_arg"), "the va_arg is gone: {text}");
1216 assert!(!text.contains("br_if"), "and nothing was asked: {text}");
1217 assert_eq!(func.blocks().count(), 1, "so no block was made: {text}");
1218 // The two numbers the psABI gives the class, in the rounding up and in the step.
1219 assert!(text.contains(" 15"), "rounded up to sixteen: {text}");
1220 assert!(text.contains(" 16"), "and stepped on by sixteen: {text}");
1221 }
1222
1223 /// Nothing else is touched, which matters because this runs over every function whether or not
1224 /// one reads a variable argument.
1225 #[test]
1226 fn a_function_with_no_list_in_it_is_left_exactly_as_it_was() {
1227 let mut names = Interner::new();
1228 let int = Type::int(32);
1229 let mut func =
1230 Func::new(names.intern("f"), Signature::new().with_params(&[int]).with_returns(&[int]));
1231 let entry = func.create_block();
1232 let x = func.append_param(entry, int);
1233 Builder::new(&mut func, entry).ret(&[x]);
1234
1235 let before = printed(&func, &mut names);
1236 lists(&mut func, &SYSV);
1237 assert_eq!(printed(&func, &mut names), before);
1238 }
1239}