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//! `va_start` is the other way round. Three of the four fields it writes are distances into a frame
54//! that does not exist yet, so it stays an instruction as far as [`crate::lower`], which builds it
55//! out of the frame the way it builds an `alloca`. The spill that fills the save area is written
56//! there for the same reason.
57
58use rucc_ir::{
59    Builder, Extra, Flags, Func, Imm, Inst, InstData, IntPred, MemInfo, MemOrder, Opcode, Type,
60    Value,
61};
62use rucc_target::CallRegs;
63
64/// Where the count of general purpose register bytes already walked is.
65pub const GP_OFFSET: i64 = 0;
66/// Where the count of vector register bytes already walked is.
67pub const FP_OFFSET: i64 = 4;
68/// Where the pointer to the next argument in the caller's memory is.
69pub const OVERFLOW: i64 = 8;
70/// Where the pointer to the bottom of the register save area is.
71pub const SAVE_AREA: i64 = 16;
72/// How many bytes one `va_list` is, which is what a `va_copy` moves.
73pub const SIZE: u64 = 24;
74/// How wide the slot one vector register is saved in is, which is how wide the register is whatever
75/// this actually writes into it.
76pub const VECTOR_SLOT: u32 = 16;
77
78/// How big a callee's register save area is and where its two halves are.
79///
80/// Worked out from the convention rather than written down, so that a convention with a different
81/// number of argument registers gets an area the right size for it without anything here changing.
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct Area {
84    /// How many bytes of it the general purpose registers take, which is also where the vector half
85    /// begins, since the general purpose half is first and starts at nothing.
86    pub floats_at: u32,
87    /// How many bytes the whole of it is.
88    pub size: u32,
89    /// How many registers of each file it holds, general purpose first.
90    counts: (u32, u32),
91    /// How far apart two general purpose slots are, which is a word.
92    word: u32,
93}
94
95impl Area {
96    /// The save area a variadic callee under that convention needs.
97    #[must_use]
98    pub fn of(conv: &CallRegs) -> Self {
99        let ints = u32::try_from(conv.int_args.len()).unwrap_or(0);
100        let floats = u32::try_from(conv.sse_args.len()).unwrap_or(0);
101        let floats_at = conv.word * ints;
102        Self {
103            floats_at,
104            size: floats_at + VECTOR_SLOT * floats,
105            counts: (ints, floats),
106            word: conv.word,
107        }
108    }
109
110    /// How far apart two of a file's slots are.
111    #[must_use]
112    pub fn stride(self, float: bool) -> u32 {
113        if float { VECTOR_SLOT } else { self.word }
114    }
115
116    /// Where a file's first slot is, which is what `va_start` writes into that file's field when
117    /// the signature named no argument that file carried.
118    #[must_use]
119    pub fn starts_at(self, float: bool) -> u32 {
120        if float { self.floats_at } else { 0 }
121    }
122
123    /// The offset of a file's last slot, which is the threshold `va_arg` compares against.
124    ///
125    /// The last slot's own offset and not the end of the area, because an offset equal to the end
126    /// is one slot past the last argument while an offset a slot below the end is the last argument
127    /// itself. An empty file has no such offset and nothing here has one.
128    #[must_use]
129    pub fn last(self, float: bool) -> Option<u32> {
130        let count = if float { self.counts.1 } else { self.counts.0 };
131        let last = count.checked_sub(1)?;
132        Some(self.starts_at(float) + self.stride(float) * last)
133    }
134}
135
136/// Rewrites every `va_arg`, `va_copy` and `va_end` in the function, and leaves `va_start` alone.
137///
138/// Those three are the ones made only of reads and writes of a list some pointer already reaches,
139/// so none of them needs to know anything about the frame and all three can be done here.
140/// `va_start` is the one that does need the frame, and [`crate::lower`] has it.
141///
142/// A convention whose list is not the four field one is left alone entirely, and a function using
143/// one is then refused further down with the message about a rule that does not exist. Windows is
144/// the one such convention here: its list is a plain pointer, its callee spills its four register
145/// arguments into the shadow space the caller already reserved rather than into an area of its own,
146/// and none of the three rewrites below is right for any of that.
147pub fn lists(func: &mut Func, conv: &CallRegs) {
148    if conv.shared_positions {
149        return;
150    }
151    let area = Area::of(conv);
152    let found: Vec<Inst> =
153        func.blocks().flat_map(|block| func.insts(block).collect::<Vec<_>>()).collect();
154    for inst in found {
155        match func[inst].opcode {
156            Opcode::VaArg => next(func, inst, area),
157            Opcode::VaCopy => copy(func, inst),
158            // Nothing at all, which is what the psABI says it is. The instruction was still worth
159            // emitting, because it says the list stops being read here, and here is where that
160            // stops being worth saying.
161            Opcode::VaEnd => func.remove_inst(inst),
162            _ => {}
163        }
164    }
165}
166
167/// One `va_arg`, as the branch on whether the argument it wants is still in the save area.
168///
169/// The block the instruction was in is cut in two at the instruction. What was above it stays where
170/// it is and gets the compare and the branch, what was below it moves into a new block that takes
171/// the address as a parameter, and the `va_arg` itself becomes the load at the top of that block.
172/// Turning it into the load rather than replacing it keeps the value the rest of the function reads
173/// the value it already read, so nothing has to be substituted anywhere, and the two paths meet at a
174/// block parameter because the IR has no variables for them to meet at.
175fn next(func: &mut Func, inst: Inst, area: Area) {
176    let Some(result) = func[inst].first_result else { return };
177    let Some(&list) = func[func[inst].args].first() else { return };
178    let ty = func[result].ty;
179    let Some(block) = func.block_of(inst) else { return };
180    let span = func.span(inst);
181    // A scalar of a width a register holds, which is every type the algorithm below is right about.
182    // A `long double` is on the x87 stack, and an `__int128` takes two slots with an alignment rule
183    // of its own. Both of those are a second algorithm rather than a wider reading of this one, so
184    // both are left alone here and refused by name further down.
185    if !ty.is_scalar() || ty.bits() > 64 || !(ty.is_int() || ty.is_float() || ty.is_ptr()) {
186        return;
187    }
188    let float = ty.is_float();
189    let Some(last) = area.last(float) else { return };
190    let field = if float { FP_OFFSET } else { GP_OFFSET };
191
192    // Everything below the instruction, taken out before anything is built, because the builder
193    // appends to a block and this block has to end at the branch.
194    let rest: Vec<Inst> = func.insts(block).skip_while(|&at| at != inst).skip(1).collect();
195    let taken = func.create_block();
196    let overflowed = func.create_block();
197    let join = func.create_block();
198    let addr = func.append_param(join, Type::PTR);
199    func.remove_inst(inst);
200    for &at in &rest {
201        func.remove_inst(at);
202    }
203
204    // The question, in the block the `va_arg` used to be in. Unsigned, because an offset into the
205    // save area counts bytes and is never negative, and because what the field holds once the
206    // register arguments have all been walked is a number past the end rather than a small one.
207    let mut build = Builder::new(func, block).at(span);
208    let counter = offset(&mut build, list, field);
209    let walked = build.load(Type::int(32), counter, info(4, 4), Flags::default());
210    let end = build.iconst(Type::int(32), i128::from(last));
211    let inside = build.icmp(IntPred::Ule, walked, end);
212    build.br_if(inside, taken, &[], overflowed, &[]);
213
214    // The register path: the argument is in the save area at the offset the field holds, and the
215    // field steps on by one slot of its file.
216    let mut build = Builder::new(func, taken).at(span);
217    let base = offset(&mut build, list, SAVE_AREA);
218    let save = build.load(Type::PTR, base, info(8, 8), Flags::default());
219    let wide = build.unary(Opcode::ZExt, walked, Type::int(64));
220    let found = added(&mut build, save, wide);
221    let stride = build.iconst(Type::int(32), i128::from(area.stride(float)));
222    let stepped = build.binary(Opcode::Add, walked, stride, Flags::default());
223    let counter = offset(&mut build, list, field);
224    build.store(stepped, counter, info(4, 4), Flags::default());
225    build.jump(join, &[found]);
226
227    // The memory path: the argument is where the caller left it, and the pointer steps on by a
228    // word, because the caller's argument area is a run of whole words whatever is in them.
229    let mut build = Builder::new(func, overflowed).at(span);
230    let pointer = offset(&mut build, list, OVERFLOW);
231    let here = build.load(Type::PTR, pointer, info(8, 8), Flags::default());
232    let word = build.iconst(Type::int(64), i128::from(area.word));
233    let onward = added(&mut build, here, word);
234    build.store(onward, pointer, info(8, 8), Flags::default());
235    build.jump(join, &[here]);
236
237    // And the load the program actually wrote, over the address the two paths agreed on, with
238    // everything that used to follow it behind it in the order it was written.
239    let bytes = ty.bits() / 8;
240    let mem = func.add_mem(info(u64::from(bytes), bytes));
241    let args = func.push_values(&[addr]);
242    let data = &mut func[inst];
243    data.opcode = Opcode::Load;
244    data.args = args;
245    data.extra = Extra::Mem(mem);
246    data.flags = data.flags.intersection(Flags::legal_on(Opcode::Load));
247    func.append_inst(join, inst);
248    for at in rest {
249        func.append_inst(join, at);
250    }
251}
252
253/// One `va_copy`, as the fields of one list moved into another.
254///
255/// A list is those fields and holds nothing anywhere else, so copying it is copying them, and three
256/// words move as three words rather than as a call to `memcpy`, which is a name this compiler
257/// cannot emit yet and would be the wrong answer for three words in any case.
258///
259/// Every read is built before any write, so that a list copied onto itself, which is legal and
260/// useless, moves what it held rather than what it has just been given.
261fn copy(func: &mut Func, inst: Inst) {
262    let [into, from] = func[func[inst].args] else { return };
263    let mut moved = Vec::new();
264    for word in 0..SIZE / 8 {
265        let step = i64::try_from(word * 8).unwrap_or(0);
266        let there = field(func, inst, from, step);
267        let mem = func.add_mem(info(8, 8));
268        let args = func.push_values(&[there]);
269        let data = InstData { args, extra: Extra::Mem(mem), ..InstData::new(Opcode::Load) };
270        moved.push((ahead(func, inst, data, Type::int(64)), step));
271    }
272    for (read, step) in moved {
273        let here = field(func, inst, into, step);
274        let mem = func.add_mem(info(8, 8));
275        let args = func.push_values(&[read, here]);
276        let data = InstData { args, extra: Extra::Mem(mem), ..InstData::new(Opcode::Store) };
277        let span = func.span(inst);
278        let made = func.create_inst(data, &[], span);
279        func.insert_before(made, inst);
280    }
281    func.remove_inst(inst);
282}
283
284/// The address of a field of a list, written in front of an instruction, or the list itself for the
285/// field at the front of it.
286fn field(func: &mut Func, inst: Inst, list: Value, at: i64) -> Value {
287    if at == 0 {
288        return list;
289    }
290    let extra = Extra::Imm(func.add_imm(Imm::int(i128::from(at), Type::int(64))));
291    let step =
292        ahead(func, inst, InstData { extra, ..InstData::new(Opcode::IConst) }, Type::int(64));
293    let args = func.push_values(&[list, step]);
294    ahead(func, inst, InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR)
295}
296
297/// Puts an instruction in front of another one and gives back the value it produces.
298fn ahead(func: &mut Func, inst: Inst, data: InstData, ty: Type) -> Value {
299    let span = func.span(inst);
300    let made = func.create_inst(data, &[ty], span);
301    func.insert_before(made, inst);
302    func[made].first_result.expect("an instruction created with one result has one")
303}
304
305/// The address of a field of a list in a block being filled, or the list itself for the field at
306/// the front of it.
307fn offset(build: &mut Builder<'_>, list: Value, at: i64) -> Value {
308    if at == 0 {
309        return list;
310    }
311    let step = build.iconst(Type::int(64), i128::from(at));
312    added(build, list, step)
313}
314
315/// A pointer with an integer added to it.
316fn added(build: &mut Builder<'_>, pointer: Value, by: Value) -> Value {
317    let args = build.func().push_values(&[pointer, by]);
318    build.value(InstData { args, ..InstData::new(Opcode::PtrAdd) }, Type::PTR)
319}
320
321/// An ordinary read or write of that many bytes, aligned that far.
322///
323/// Every access this pass makes is to a field of a list or to an argument, and none of them is
324/// atomic or has anything to say about aliasing.
325fn info(size: u64, align: u32) -> MemInfo {
326    MemInfo { size, align, order: MemOrder::NotAtomic, tbaa: None }
327}
328
329#[cfg(test)]
330mod tests {
331    use rucc_base::Interner;
332    use rucc_ir::{Builder, Func, InstData, Module, Opcode, Signature, Type};
333    use rucc_target::x86_64::{SYSV, WIN64};
334    use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
335
336    use super::{Area, FP_OFFSET, GP_OFFSET, OVERFLOW, SAVE_AREA, SIZE, VECTOR_SLOT, lists};
337
338    fn target() -> TargetInfo {
339        TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
340    }
341
342    /// `T f(va_list *ap) { return va_arg(*ap, T); }`, or the same shape over whichever of the
343    /// family is asked for, with the list arriving as the pointer it has decayed to by the time
344    /// anything reads it.
345    fn built(opcode: Opcode, ty: Type, lists: usize) -> (Interner, Func) {
346        let mut names = Interner::new();
347        let params = vec![Type::PTR; lists];
348        let mut signature = Signature::new().with_params(&params);
349        if !ty.is_void() {
350            signature = signature.with_returns(&[ty]);
351        }
352        let mut func = Func::new(names.intern("f"), signature);
353        let entry = func.create_block();
354        let args: Vec<_> = params.iter().map(|&ty| func.append_param(entry, ty)).collect();
355
356        let mut build = Builder::new(&mut func, entry);
357        let list = build.func().push_values(&args);
358        if ty.is_void() {
359            build.inst(InstData { args: list, ..InstData::new(opcode) }, &[]);
360            build.ret(&[]);
361        } else {
362            let got = build.value(InstData { args: list, ..InstData::new(opcode) }, ty);
363            build.ret(&[got]);
364        }
365        (names, func)
366    }
367
368    fn printed(func: &Func, names: &mut Interner) -> String {
369        let module = Module::new(names.intern("va.c"), &target());
370        rucc_ir::print_func(&module, func, names)
371    }
372
373    fn valid(func: &Func, names: &mut Interner) {
374        let module = Module::new(names.intern("va.c"), &target());
375        rucc_ir::verify_func(&module, func, names).expect("the rewrite builds valid IR");
376    }
377
378    /// The numbers in this test are the psABI's own, written out rather than computed, because the
379    /// whole point of the layout is that it is the document's and not a convenient one. A version
380    /// of [`Area`] that worked them out differently would agree with itself and disagree with the C
381    /// library, and this is what would notice.
382    #[test]
383    fn the_save_area_is_the_one_the_document_describes() {
384        let area = Area::of(&SYSV);
385        assert_eq!(area.floats_at, 48, "six general purpose registers of eight bytes");
386        assert_eq!(area.size, 176, "and eight vector ones of sixteen");
387        assert_eq!(area.stride(false), 8);
388        assert_eq!(area.stride(true), VECTOR_SLOT);
389        assert_eq!(area.starts_at(false), 0);
390        assert_eq!(area.starts_at(true), 48);
391        // The last slot's own offset and not the end of the area, which is what `va_arg` compares
392        // against: an offset equal to the end is one slot past the last argument.
393        assert_eq!(area.last(false), Some(40));
394        assert_eq!(area.last(true), Some(160));
395    }
396
397    /// And the four fields, for the same reason.
398    #[test]
399    fn a_list_is_the_four_fields_the_document_describes() {
400        assert_eq!((GP_OFFSET, FP_OFFSET, OVERFLOW, SAVE_AREA), (0, 4, 8, 16));
401        assert_eq!(SIZE, 24);
402    }
403
404    #[test]
405    fn a_va_arg_becomes_the_branch_on_whether_the_argument_is_still_in_the_save_area() {
406        let (mut names, mut func) = built(Opcode::VaArg, Type::int(32), 1);
407        let before = func.blocks().count();
408        lists(&mut func, &SYSV);
409        assert_eq!(func.blocks().count(), before + 3, "one for each path and one they meet at");
410
411        let text = printed(&func, &mut names);
412        assert!(!text.contains("va_arg"), "the va_arg is gone: {text}");
413        assert!(text.contains("icmp ule"), "the threshold is a comparison: {text}");
414        assert!(text.contains("br_if"), "and it is branched on: {text}");
415        valid(&func, &mut names);
416    }
417
418    /// Which field it walks is the whole of the difference between the two files, and getting it
419    /// backwards is a program that reads its integers out of the vector half.
420    #[test]
421    fn which_half_of_the_area_is_walked_is_the_type_s_answer() {
422        for (ty, last, stride) in
423            [(Type::int(64), 40, 8), (Type::float(rucc_ir::Float::F64), 160, 16)]
424        {
425            let (mut names, mut func) = built(Opcode::VaArg, ty, 1);
426            lists(&mut func, &SYSV);
427            let text = printed(&func, &mut names);
428            assert!(text.contains(&format!("iconst.i32 {last}")), "{ty:?} stops at {last}: {text}");
429            assert!(text.contains(&format!("iconst.i32 {stride}")), "and steps by it: {text}");
430        }
431    }
432
433    /// The value the rest of the function reads has to stay the value it already read, since the
434    /// rewrite substitutes nothing anywhere. It stays it by the `va_arg` becoming the load rather
435    /// than being replaced by one, so the instruction is the same instruction under a new opcode
436    /// and in a new block.
437    #[test]
438    fn what_reads_the_argument_reads_the_same_value_it_did_before() {
439        let (mut names, mut func) = built(Opcode::VaArg, Type::int(32), 1);
440        let entry = func.entry().expect("an entry block");
441        let inst = func.insts(entry).next().expect("the va_arg is first");
442        let read = func[inst].first_result.expect("it produces the argument");
443
444        lists(&mut func, &SYSV);
445        assert_eq!(func[inst].opcode, Opcode::Load, "the same instruction, lowered");
446        assert_eq!(func[inst].first_result, Some(read), "producing the same value");
447        assert_ne!(func.block_of(inst), Some(entry), "in the block the two paths meet at");
448        valid(&func, &mut names);
449    }
450
451    #[test]
452    fn a_va_end_is_nothing_at_all() {
453        let (mut names, mut func) = built(Opcode::VaEnd, Type::VOID, 1);
454        lists(&mut func, &SYSV);
455        let text = printed(&func, &mut names);
456        assert!(!text.contains("va_end"), "{text}");
457        assert_eq!(func.blocks().count(), 1, "and needs no block: {text}");
458        valid(&func, &mut names);
459    }
460
461    /// Three words and no branch, because a list is three words and holds nothing anywhere else.
462    #[test]
463    fn a_va_copy_is_the_list_moved_a_word_at_a_time() {
464        let (mut names, mut func) = built(Opcode::VaCopy, Type::VOID, 2);
465        lists(&mut func, &SYSV);
466        let text = printed(&func, &mut names);
467        assert!(!text.contains("va_copy"), "{text}");
468        assert_eq!(text.matches("load.i64").count(), 3, "{text}");
469        assert_eq!(text.matches("store").count(), 3, "{text}");
470        assert_eq!(func.blocks().count(), 1, "and needs no block: {text}");
471        valid(&func, &mut names);
472    }
473
474    /// Every read before every write, so that `va_copy(ap, ap)` moves what the list held rather
475    /// than what it has just been given. Useless and legal, which is exactly the combination that
476    /// gets written once and never tested anywhere else.
477    #[test]
478    fn a_list_copied_onto_itself_moves_what_it_held() {
479        let (mut names, mut func) = built(Opcode::VaCopy, Type::VOID, 1);
480        // One parameter, so both operands of the copy are the same list. The builder above pushes
481        // as many operands as there are parameters, so the second is added here.
482        let entry = func.entry().expect("an entry block");
483        let inst = func.insts(entry).next().expect("the copy is first");
484        let list = func[func[inst].args][0];
485        let args = func.push_values(&[list, list]);
486        func[inst].args = args;
487
488        lists(&mut func, &SYSV);
489        let text = printed(&func, &mut names);
490        let first = text.find("store").expect("a write");
491        let last = text.rfind("load.i64").expect("a read");
492        assert!(last < first, "every read is above every write: {text}");
493        valid(&func, &mut names);
494    }
495
496    /// Windows has a list of a different shape and an algorithm to match, and none of the rewrites
497    /// here is right for any of it. Leaving it alone is what makes the function refused by name
498    /// further down rather than compiled into a walk over an area that was never filled in.
499    #[test]
500    fn a_convention_whose_list_is_not_this_one_is_left_alone() {
501        let (mut names, mut func) = built(Opcode::VaArg, Type::int(32), 1);
502        let before = printed(&func, &mut names);
503        lists(&mut func, &WIN64);
504        assert_eq!(printed(&func, &mut names), before);
505    }
506
507    /// A width the algorithm is not right about is left alone for the same reason, and the two
508    /// here are the ones a program actually writes: a `long double` is on a register file this has
509    /// nothing to say about, and an `__int128` takes two slots under an alignment rule of its own.
510    #[test]
511    fn a_type_that_does_not_travel_in_one_slot_is_left_alone() {
512        for ty in [Type::float(rucc_ir::Float::F80), Type::int(128)] {
513            let (mut names, mut func) = built(Opcode::VaArg, ty, 1);
514            let before = printed(&func, &mut names);
515            lists(&mut func, &SYSV);
516            assert_eq!(printed(&func, &mut names), before, "{ty:?}");
517        }
518    }
519
520    /// Nothing else is touched, which matters because this runs over every function whether or not
521    /// one reads a variable argument.
522    #[test]
523    fn a_function_with_no_list_in_it_is_left_exactly_as_it_was() {
524        let mut names = Interner::new();
525        let int = Type::int(32);
526        let mut func =
527            Func::new(names.intern("f"), Signature::new().with_params(&[int]).with_returns(&[int]));
528        let entry = func.create_block();
529        let x = func.append_param(entry, int);
530        Builder::new(&mut func, entry).ret(&[x]);
531
532        let before = printed(&func, &mut names);
533        lists(&mut func, &SYSV);
534        assert_eq!(printed(&func, &mut names), before);
535    }
536}