Skip to main content

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