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(¶ms);
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}