rucc-codegen 0.3.9

Instruction selection, scheduling, block layout, frames and prologue emission.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
//! Where a function's arguments already are when it starts running, and where a call puts its own.
//!
//! Design: `spec/12-abi-and-runtime.md`.
//!
//! This is the one part of the calling convention that is not a lowering rule, and it is worth
//! saying why, because everything else in this crate is. A rule matches a term and rewrites it,
//! and which register the third argument arrives in is not a fact about any term: it depends on
//! the argument's position and on the classification of every argument before it. A pattern has
//! nowhere to put that. So the arguments are built here, by hand, out of what the convention
//! says, the same way [`crate::finish`] builds a prologue.
//!
//! The classification itself is not here either. `rucc-lower` has already run it by the time a
//! function reaches this crate, which is why the parameters read here are plain scalars: an
//! aggregate has been split into the pieces it travels in, and a return through memory is an
//! ordinary pointer parameter in front of the rest. What is left for this is the step after
//! classification, from how a value travels to which register it is actually in, which is
//! [`rucc_target::Places`].
//!
//! # What it writes
//!
//! One `x64.arg_val_*` per parameter, at the top of the entry block, each defining a fresh
//! register constrained to the one the argument arrived in. They encode to nothing. The point of
//! them is that a parameter has to be defined somewhere for the allocator to have anything to
//! move, and the entry block cannot define it as a block parameter: there is no edge into the
//! entry block for the move to go on, which is what `rucc_regalloc::rewrite` asserts.
//!
//! What the allocator does with them is the whole of the argument sequence. A parameter that is
//! read where it arrived costs nothing, and one that is not gets a copy, which is the same
//! bargain the return already makes and is decided by the same code.
//!
//! # A call
//!
//! The same reasoning the other way round, and one instruction rather than several. `x64.call`
//! and `x64.call_reg` are the only opcodes in the description whose operand vector is empty
//! there, because nothing about a call's operands is the same from one call to the next, so they
//! are built here: one read per argument constrained to the register the convention passes it in,
//! one definition for the value that comes back constrained to the register it comes back in, and
//! one definition per register the convention does not preserve.
//!
//! A call through an address has one operand more, which is the address, and it is the one
//! operand of a call that is a fact about the instruction rather than about the signature. It
//! goes in front of the arguments, because the assembler has to find it and an index into a
//! vector whose length depends on the convention is not a way of finding anything.
//!
//! Those last ones are the clobbers, and they are the whole of what the allocator has to know
//! about a call besides where the values go. Each is a definition of the physical register itself
//! rather than of a value, since there is no value: it says the register is written here, which
//! is exactly what stops the allocator from leaving something in one across the call. A register
//! an argument or the result already names is not repeated, because naming it once already blocks
//! it for the length of the instruction, which is all a clobber does.
//!
//! What is not here is the bytes an argument past the last register goes in. That is a place in
//! the frame and no frame exists yet, the same reason a parameter arriving there is reported
//! rather than read, so a call is asked how many bytes it would need and reports it, and a call
//! that would need any is turned down for now.

use rucc_base::{Interner, Symbol};
use rucc_ir::Type;
use rucc_mir as mir;
use rucc_target::{CallRegs, Constraint, PhysReg, Places, RegClass, Where};

/// Why a parameter could not be brought in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Missing {
    /// It arrives on the stack, which is somewhere nothing reads from yet: the offset is a
    /// distance into a frame, and a frame is not worked out until after allocation.
    OnStack,
    /// It travels on the x87 stack, which is a `long double` and nothing else. That stack is a
    /// third register file, it is not one the allocator has, and no instruction in the
    /// description touches it.
    OnX87,
    /// It is a float passed to a callee that takes arguments beyond the ones its signature names,
    /// on a convention that puts such a float in a vector register and in the general purpose
    /// register at the same position at once. Which arguments are the ones beyond the signature is
    /// what decides whether the second copy is needed, and a call does not carry that yet.
    InBothFiles,
    /// It is a width no pseudo covers, which is anything a machine register does not hold.
    Width,
}

impl Missing {
    /// What it says when a function could not be compiled because of it.
    ///
    /// Worded so that it reads the same about a value arriving and a value being passed, since
    /// the two are the same fact seen from the two ends of one call.
    #[must_use]
    pub fn why(self) -> &'static str {
        match self {
            Missing::OnStack => "is passed on the stack",
            Missing::OnX87 => "is on the x87 stack",
            Missing::InBothFiles => "is a float passed to a variadic callee on this convention",
            Missing::Width => "is a width no argument register holds",
        }
    }
}

/// Which register file a value of that type travels in.
///
/// The whole of what the two files mean to this module. A float is in the vector one and
/// everything else is in the general purpose one, which is what both of this machine's conventions
/// say, and the `long double` that is in neither is turned away by [`refuses`] before this is
/// asked.
fn class_of(ty: Type, conv: &CallRegs) -> RegClass {
    if ty.is_float() { conv.sse_class } else { conv.int_class }
}

/// Why a value of that type cannot travel at all, or nothing if it can.
///
/// The width question and the file question in one place, so that the two ends of a call give the
/// same answer about the same type.
fn refuses(ty: Type) -> Option<Missing> {
    if head_of(ty).is_some() {
        return None;
    }
    // A `long double` is the one type here that is in neither of the two files. Saying so is worth
    // more than calling it a width, because eighty bits is a width this machine computes in and
    // the file it computes in is what actually stands in the way.
    if ty.is_float() && ty.bits() == 80 {
        return Some(Missing::OnX87);
    }
    Some(Missing::Width)
}

/// Binds a function's parameters to the registers the convention says they arrive in.
///
/// The registers come back in the order the parameters were given, so the caller can bind each
/// IR parameter to the one at its position.
///
/// # Errors
///
/// The first parameter this cannot bring in, and why. A function with one is reported rather
/// than compiled, because the alternative is a function that reads an argument from wherever the
/// last one happened to leave a register.
pub fn entry(
    out: &mut mir::Func,
    block: mir::Block,
    params: &[Type],
    conv: &CallRegs,
    names: &mut Interner,
) -> Result<Vec<mir::Reg>, (usize, Missing)> {
    let mut places = Places::new(conv);
    let mut regs = Vec::with_capacity(params.len());
    for (index, &ty) in params.iter().enumerate() {
        // Asking for the place of a parameter that cannot be brought in is still worth doing
        // before giving up, and it costs nothing, because every place after it depends on it and
        // a reader stepping through this in a debugger should see the same numbers a working
        // version would.
        let at = if ty.is_float() { places.float() } else { places.integer() };
        if let Some(missing) = refuses(ty) {
            return Err((index, missing));
        }
        let head = head_of(ty).ok_or((index, Missing::Width))?;
        let Where::Reg(arrived) = at else { return Err((index, Missing::OnStack)) };

        let class = class_of(ty, conv);
        let reg = out.new_vreg(class);
        let opcode = mir::Opcode::new(names.intern(head));
        let operand = mir::Operand::write(reg, class).with(Constraint::Fixed(arrived));
        out.build(block, opcode).operand(operand).finish();
        regs.push(reg);
    }
    Ok(regs)
}

/// What the instruction that calls a name is called.
///
/// Here rather than in a rule for the same reason the arguments are: a rule pattern sees one term
/// and a call's operands are whatever the signature made them, so no pattern could name them.
pub const CALL: &str = "x64.call";

/// What the instruction that calls an address in a register is called.
///
/// A different instruction rather than the same one with a different operand, which is what the
/// machine says too: one carries the distance to somewhere in the program and takes a relocation,
/// and the other carries the register the address is in and takes none. Sharing an opcode would
/// mean an instruction whose bytes depend on whether a field beside it happens to be set.
pub const CALL_REG: &str = "x64.call_reg";

/// What one call came to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Made {
    /// The register the value came back in, or `None` for a call that gives nothing back.
    pub result: Option<mir::Reg>,
    /// How many bytes below the stack pointer this call needs for the arguments it passes there.
    ///
    /// Not always zero for a call that passes everything in registers: a Windows caller reserves
    /// thirty two bytes for the callee to spill its register arguments into whether it uses them
    /// or not, and that reservation is this.
    pub outgoing: u32,
}

/// Which of a call's values could not be passed, and why.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Refused {
    /// Its position among the arguments, or `None` for the value that comes back.
    pub argument: Option<usize>,
    /// What is wrong with where it travels.
    pub missing: Missing,
}

/// What a call goes to.
///
/// The whole of the difference between the two calls. Everything else about them, which is what
/// they pass and what comes back and which registers they destroy, is the signature's answer and
/// is the same answer either way.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Callee {
    /// A name, which the linker resolves.
    Named(Symbol),
    /// An address in a register, which nothing resolves because there is nothing to resolve: the
    /// value is not known until the program runs.
    ///
    /// The register is unconstrained, and it has to be, because every register the convention
    /// does not preserve is one this instruction writes and every register an argument travels in
    /// is spoken for. What is left is the registers the callee has to put back, which is where
    /// the allocator will put the address, and it is the right answer for the same reason it is
    /// the only one.
    Through(mir::Reg),
}

/// One call, as everything about it that is not the function it is being built into.
#[derive(Debug, Clone, Copy)]
pub struct Calling<'a> {
    /// What it calls.
    pub callee: Callee,
    /// What it passes, as the type each value travels as and the register it is in, in the order
    /// the signature holds them, which is the order the convention places them in.
    pub args: &'a [(Type, mir::Reg)],
    /// What comes back, or `None` for a call that gives nothing back.
    pub returns: Option<Type>,
    /// Whether the callee takes arguments beyond the ones its signature names, which is what says
    /// whether it reads the count of vector registers the call passed arguments in.
    pub variadic: bool,
}

/// Builds one call: what it passes, what comes back, and what it destroys.
///
/// # Errors
///
/// The first value this cannot pass, and why, before anything is written. A call with one is
/// reported rather than compiled, because the alternative is a call that leaves an argument
/// wherever the last one happened to put a register.
pub fn call(
    out: &mut mir::Func,
    block: mir::Block,
    made: &Calling<'_>,
    conv: &CallRegs,
    names: &mut Interner,
) -> Result<Made, Refused> {
    let &Calling { callee, args, returns, variadic } = made;
    // Where everything goes, worked out before anything is built, so that a call this cannot make
    // leaves no half of one behind.
    let mut places = Places::new(conv);
    let mut passed = Vec::with_capacity(args.len());
    // How many of them went in vector registers, which is what a SysV variadic callee is told.
    let mut vectors = 0u32;
    for (index, &(ty, reg)) in args.iter().enumerate() {
        let refused = |missing| Refused { argument: Some(index), missing };
        let at = if ty.is_float() { places.float() } else { places.integer() };
        if let Some(missing) = refuses(ty) {
            return Err(refused(missing));
        }
        // Windows passes a float to a variadic callee in the vector register and in the general
        // purpose register at the same position, both at once, because the callee has no
        // prototype to tell it which file to look in. Doing that needs to know which arguments are
        // the ones the signature does not name, and a call carries whether the callee is variadic
        // rather than how many arguments it names, so this is turned down rather than passed in
        // one file and read from the other.
        if ty.is_float() && variadic && conv.shared_positions {
            return Err(refused(Missing::InBothFiles));
        }
        let Where::Reg(at) = at else { return Err(refused(Missing::OnStack)) };
        let class = class_of(ty, conv);
        if class == conv.sse_class {
            vectors += 1;
        }
        passed.push((reg, at, class));
    }
    let comes_back = match returns {
        None => None,
        Some(ty) if refuses(ty).is_some() => {
            return Err(Refused { argument: None, missing: refuses(ty).unwrap_or(Missing::Width) });
        }
        // Which register a value comes back in depends on nothing but the value, which is why the
        // return side of the convention is a rule and this side is not. There is no rule here
        // because the arguments are in the same instruction.
        Some(ty) => {
            let class = class_of(ty, conv);
            let file = if class == conv.sse_class { conv.sse_returns } else { conv.int_returns };
            let at = *file.first().ok_or(Refused { argument: None, missing: Missing::Width })?;
            Some((at, class))
        }
    };

    // A variadic callee on SysV reads how many vector registers the call passed arguments in and
    // skips saving them when the answer is none, which is what makes `printf` with no floating
    // point argument cheap. It is an obligation rather than an optimization: leaving whatever was
    // in the register there makes the callee save a register file it was not given, and a count
    // that is too low makes it read an argument out of a register nothing put one in.
    let counted = if variadic { conv.vector_count } else { None };

    // The definitions first and the reads after, which is the order every operand vector in the
    // machine IR is in and the order `rucc_mir::defs` counts.
    let mut operands = Vec::with_capacity(args.len() + conv.int_order.len() + 2);
    let result = comes_back.map(|(at, class)| {
        let reg = out.new_vreg(class);
        operands.push(mir::Operand::write(reg, class).with(Constraint::Fixed(at)));
        reg
    });
    // One list per file, because a physical register is a number and the class is what says which
    // file it is a number in. One list would have `xmm0` blocking `rax`.
    let spoken_for = |class: RegClass| -> Vec<PhysReg> {
        comes_back
            .filter(|&(_, at)| at == class)
            .map(|(reg, _)| reg)
            .into_iter()
            .chain(counted.filter(|_| class == conv.int_class))
            .chain(passed.iter().filter(|&&(_, _, at)| at == class).map(|&(_, reg, _)| reg))
            .collect()
    };
    let named = spoken_for(conv.int_class);
    for &reg in conv.int_order {
        if !conv.preserves_int(reg) && !named.contains(&reg) {
            operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.int_class));
        }
    }
    let named = spoken_for(conv.sse_class);
    for &reg in conv.sse_order {
        if !conv.preserves_sse(reg) && !named.contains(&reg) {
            operands.push(mir::Operand::write(mir::Reg::physical(reg), conv.sse_class));
        }
    }
    // The address in front of the arguments, because a call through one is written with the
    // register it goes through and nothing in the operand vector is at a place a table could name.
    // First read is a place that does not depend on the signature, which is what
    // [`rucc_target::x86_64::Arg::Through`] is written against.
    if let Callee::Through(reg) = callee {
        operands.push(mir::Operand::read(reg, conv.int_class));
    }
    for (reg, at, class) in passed {
        operands.push(mir::Operand::read(reg, class).with(Constraint::Fixed(at)));
    }
    if let Some(at) = counted {
        let count = out.new_vreg(conv.int_class);
        let zero = mir::Opcode::new(names.intern("x64.mov_ri_32"));
        out.build(block, zero).def(count, conv.int_class).imm(i64::from(vectors)).finish();
        operands.push(mir::Operand::read(count, conv.int_class).with(Constraint::Fixed(at)));
    }

    let opcode = mir::Opcode::new(names.intern(match callee {
        Callee::Named(_) => CALL,
        Callee::Through(_) => CALL_REG,
    }));
    let mut build = out.build(block, opcode);
    if let Callee::Named(symbol) = callee {
        build = build.symbol(symbol);
    }
    for operand in operands {
        build = build.operand(operand);
    }
    build.finish();
    Ok(Made { result, outgoing: places.size() })
}

/// What the pseudo for an argument of that type is called.
///
/// The width is in the name for the same reason it is in every other opcode here: it is what the
/// instruction is about. Nothing encodes it, so nothing depends on it being right, but a listing
/// that says an argument arrived and does not say how much of it did is a listing worth less.
///
/// Which widths there are is the question the rule set asks of a type, and not a list of its own,
/// because it has to be the same list. An argument brought in at a width the rules have no name
/// for is a register
/// nothing downstream could then read, and a width the rules cover that this refuses is a
/// function turned away for no reason. Asking one question in one place is what keeps the two
/// answers from drifting, and an address is what they used to disagree about.
#[must_use]
pub fn head_of(ty: Type) -> Option<&'static str> {
    if let Some(at) = crate::term::float_slot(ty) {
        return Some(["x64.arg_val_f32", "x64.arg_val_f64"][at]);
    }
    let names = ["x64.arg_val_8", "x64.arg_val_16", "x64.arg_val_32", "x64.arg_val_64"];
    Some(names[crate::term::slot(ty)?])
}

#[cfg(test)]
mod tests {
    use rucc_target::x86_64::{REGS, SYSV, WIN64};

    use super::*;

    /// The parameters of a function under a convention, as machine IR text.
    fn bind(params: &[Type], conv: &CallRegs) -> String {
        let mut names = Interner::new();
        let mut out = mir::Func::new(names.intern("f"));
        let block = out.create_block();
        entry(&mut out, block, params, conv, &mut names).expect("every parameter arrives");
        mir::print_func(&out, &names, &REGS)
    }

    #[test]
    fn the_first_arguments_arrive_where_the_convention_puts_them() {
        let i32 = Type::int(32);
        assert_eq!(
            bind(&[i32, i32, Type::int(64)], &SYSV),
            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_32\n    \
             %1:gpr($rsi) = x64.arg_val_32\n    %2:gpr($rdx) = x64.arg_val_64\n}\n"
        );
    }

    #[test]
    fn the_other_convention_puts_the_same_arguments_somewhere_else() {
        // The first argument is in `rcx` here and in `rdi` above, which is the difference that
        // makes a SysV binary calling a Windows one read the wrong value rather than fail.
        let i64 = Type::int(64);
        assert_eq!(
            bind(&[i64, i64], &WIN64),
            "mfunc @f {\nblock0:\n    %0:gpr($rcx) = x64.arg_val_64\n    \
             %1:gpr($rdx) = x64.arg_val_64\n}\n"
        );
    }

    #[test]
    fn an_argument_past_the_last_register_is_reported_rather_than_read_from_nowhere() {
        let i64 = Type::int(64);
        let mut names = Interner::new();
        let mut out = mir::Func::new(names.intern("f"));
        let block = out.create_block();
        let seven = vec![i64; 7];
        assert_eq!(entry(&mut out, block, &seven, &SYSV, &mut names), Err((6, Missing::OnStack)));
        // Six of them still got registers, and the seventh is what stopped it. Windows runs out
        // three arguments earlier, which is the same answer at a different position.
        assert_eq!(entry(&mut out, block, &seven, &WIN64, &mut names), Err((4, Missing::OnStack)));
    }

    /// A float arrives in the other file, and the two files are counted apart on SysV: the
    /// integer here is the first integer argument and the float is the first float one, so they
    /// are in `rdi` and `xmm0` rather than in the first and second of anything.
    #[test]
    fn a_float_arrives_in_a_vector_register_and_is_counted_apart_from_the_integers() {
        let f32 = Type::float(rucc_ir::Float::F32);
        let f64 = Type::float(rucc_ir::Float::F64);
        assert_eq!(
            bind(&[Type::int(32), f64, f32], &SYSV),
            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_32\n    \
             %1:xmm($xmm0) = x64.arg_val_f64\n    %2:xmm($xmm1) = x64.arg_val_f32\n}\n"
        );
    }

    /// Windows counts the two files together, so the same three arguments land in different
    /// registers: the float is the second argument and takes the second vector register rather
    /// than the first, which is the difference that makes a mismatched call read the wrong value.
    #[test]
    fn the_other_convention_counts_the_two_files_as_one_run_of_positions() {
        let f64 = Type::float(rucc_ir::Float::F64);
        assert_eq!(
            bind(&[Type::int(32), f64, Type::int(64)], &WIN64),
            "mfunc @f {\nblock0:\n    %0:gpr($rcx) = x64.arg_val_32\n    \
             %1:xmm($xmm1) = x64.arg_val_f64\n    %2:gpr($r8) = x64.arg_val_64\n}\n"
        );
    }

    /// A `long double` is in neither file, and what it is turned away for says so rather than
    /// calling eighty bits a width no register holds. The x87 stack is a register file this
    /// compiler does not allocate in and has no instruction for.
    #[test]
    fn a_long_double_is_reported_as_the_x87_stack_it_travels_on() {
        let mut names = Interner::new();
        let mut out = mir::Func::new(names.intern("f"));
        let block = out.create_block();
        let params = [Type::int(32), Type::float(rucc_ir::Float::F80)];
        assert_eq!(entry(&mut out, block, &params, &SYSV, &mut names), Err((1, Missing::OnX87)));
        assert_eq!(
            make(&[], Some(Type::float(rucc_ir::Float::F80)), false, &SYSV).2,
            Err(Refused { argument: None, missing: Missing::OnX87 })
        );
    }

    /// One call to `g`, with a register for each argument arriving in the block that makes it.
    fn make(
        args: &[Type],
        returns: Option<Type>,
        variadic: bool,
        conv: &CallRegs,
    ) -> (Interner, mir::Func, Result<Made, Refused>) {
        let mut names = Interner::new();
        let mut out = mir::Func::new(names.intern("f"));
        let block = out.create_block();
        let passed: Vec<(Type, mir::Reg)> =
            args.iter().map(|&ty| (ty, out.append_param(block, class_of(ty, conv)))).collect();
        let callee = Callee::Named(names.intern("g"));
        let what = Calling { callee, args: &passed, returns, variadic };
        let made = call(&mut out, block, &what, conv, &mut names);
        (names, out, made)
    }

    /// What the call in that function reads and writes, by register name, in the order the
    /// operands are in.
    fn operands(func: &mir::Func) -> (Vec<String>, Vec<String>) {
        let block = func.entry().expect("a function with a block in it");
        let call = func.terminator(block).expect("the call is the last thing in the block");
        let name = |operand: &mir::Operand| match (operand.reg.phys(), operand.constraint) {
            (Some(reg), _) | (None, Constraint::Fixed(reg)) => {
                REGS.name(operand.class, reg).expect("a register the file describes").to_string()
            }
            _ => format!("{:?}", operand.reg),
        };
        let mut written = Vec::new();
        let mut read = Vec::new();
        for operand in &func[func[call].operands] {
            let into = if operand.role == mir::Role::Use { &mut read } else { &mut written };
            into.push(name(operand));
        }
        (written, read)
    }

    #[test]
    fn a_call_passes_its_arguments_where_the_convention_puts_them() {
        let i32 = Type::int(32);
        let (_, func, made) = make(&[i32, i32, i32], None, false, &SYSV);
        assert_eq!(made.expect("three integers all fit in registers").result, None);
        assert_eq!(operands(&func).1, ["rdi", "rsi", "rdx"]);
    }

    #[test]
    fn the_other_convention_passes_the_same_arguments_somewhere_else() {
        let i64 = Type::int(64);
        let (_, func, made) = make(&[i64, i64], None, false, &WIN64);
        // Thirty two bytes of stack for a call that passes nothing on the stack, which is what
        // Windows asks a caller to leave the callee whether the callee uses it or not.
        assert_eq!(made.expect("two integers fit in registers").outgoing, 32);
        assert_eq!(operands(&func).1, ["rcx", "rdx"]);
    }

    #[test]
    fn what_a_call_gives_back_comes_out_of_the_register_the_convention_returns_in() {
        let (names, func, made) = make(&[], Some(Type::int(32)), false, &SYSV);
        let result = made.expect("an integer comes back").result.expect("in a register");
        // The first thing written is the result, and it is the only thing written that is a value
        // rather than a register the callee destroyed.
        assert_eq!(operands(&func).0.first().map(String::as_str), Some("rax"));
        assert_eq!(func.class_of(result), Some(SYSV.int_class));
        assert!(mir::print_func(&func, &names, &REGS).contains("x64.call"));
    }

    #[test]
    fn every_register_the_callee_may_destroy_is_written_by_the_call() {
        let (_, func, _) = make(&[Type::int(64)], Some(Type::int(64)), false, &SYSV);
        let (written, read) = operands(&func);
        // The callee saved registers are not here, because a value in one of those survives a
        // call and that is the whole difference between the two halves of the convention.
        for saved in ["rbx", "rbp", "r12", "r13", "r14", "r15"] {
            assert!(!written.contains(&saved.to_string()), "{saved} survives a call");
        }
        // Every other integer register is, once. The two named ones are named by the result and
        // by the argument instead, and naming one twice would be blocking it twice.
        for destroyed in ["rcx", "rdx", "rsi", "r8", "r9", "r10", "r11"] {
            let count = written.iter().filter(|name| *name == destroyed).count();
            assert_eq!(count, 1, "{destroyed} is destroyed by a call and is written {count} times");
        }
        assert_eq!(written.iter().filter(|name| *name == "rax").count(), 1);
        assert_eq!(read, ["rdi"]);
        // The vector registers are all destroyed on SysV, and they are in the other class.
        assert!(written.contains(&"xmm0".to_string()));
    }

    #[test]
    fn a_variadic_call_says_how_many_vector_registers_it_passed_arguments_in() {
        let (names, func, made) = make(&[Type::int(64)], None, true, &SYSV);
        made.expect("an integer argument to a variadic callee");
        let (_, read) = operands(&func);
        // Zero of them here, and `al` is where a SysV callee looks for it. Leaving whatever was in
        // the register there would make a callee that saves its vector registers save ones it was
        // never given.
        assert_eq!(read, ["rdi", "rax"]);
        assert_eq!(
            mir::print_func(&func, &names, &REGS).lines().nth(2),
            Some("    %1:gpr = x64.mov_ri_32 0")
        );

        // Two of them here, which is the number that decides how much of the register save area a
        // callee like `printf` fills in. A count of zero with a float in `xmm0` would be a callee
        // reading its first `%f` out of a register nothing wrote.
        let f64 = Type::float(rucc_ir::Float::F64);
        let (names, func, made) = make(&[Type::int(64), f64, f64], None, true, &SYSV);
        made.expect("one integer and two floats all fit in registers");
        assert_eq!(operands(&func).1, ["rdi", "xmm0", "xmm1", "rax"]);
        assert!(mir::print_func(&func, &names, &REGS).contains("x64.mov_ri_32 2"));
    }

    /// Windows passes a float to a variadic callee in both files at once, and which arguments are
    /// the ones the signature does not name is not something a call carries, so it is turned down
    /// rather than passed in one file and read from the other.
    #[test]
    fn a_float_passed_to_a_variadic_callee_on_windows_is_reported() {
        let f64 = Type::float(rucc_ir::Float::F64);
        assert_eq!(
            make(&[Type::int(32), f64], None, true, &WIN64).2,
            Err(Refused { argument: Some(1), missing: Missing::InBothFiles })
        );
        // The same call to a callee whose signature names both arguments is fine, because there is
        // no second copy to make.
        assert!(make(&[Type::int(32), f64], None, false, &WIN64).2.is_ok());
    }

    #[test]
    fn a_call_through_an_address_reads_it_in_front_of_the_arguments() {
        let i32 = Type::int(32);
        let mut names = Interner::new();
        let mut out = mir::Func::new(names.intern("f"));
        let block = out.create_block();
        let address = out.append_param(block, SYSV.int_class);
        let passed = vec![(i32, out.append_param(block, SYSV.int_class))];
        let what = Calling {
            callee: Callee::Through(address),
            args: &passed,
            returns: Some(i32),
            variadic: false,
        };
        call(&mut out, block, &what, &SYSV, &mut names).expect("one integer fits in a register");

        // The address is the first thing read and the arguments follow it, which is the order the
        // assembler counts on, and it is in no particular register because every register a call
        // could insist on is one the call has already spoken for.
        let text = mir::print_func(&out, &names, &REGS);
        assert!(text.contains("= x64.call_reg %0, %1($rdi)\n"), "{text}");
        assert!(!text.contains("@g"), "a call through an address names nobody: {text}");
    }

    #[test]
    fn a_call_that_would_pass_an_argument_on_the_stack_is_reported() {
        let i64 = Type::int(64);
        let seven = vec![i64; 7];
        let (_, func, made) = make(&seven, None, false, &SYSV);
        assert_eq!(made, Err(Refused { argument: Some(6), missing: Missing::OnStack }));
        // Nothing was written, so a call this cannot make leaves no half of one behind.
        let block = func.entry().expect("a function with a block in it");
        assert_eq!(func.insts(block).count(), 0);
        // Windows runs out three arguments earlier, which is the same answer at a different
        // position and the reason this is a fact about the convention rather than about the call.
        assert_eq!(
            make(&seven, None, false, &WIN64).2,
            Err(Refused { argument: Some(4), missing: Missing::OnStack })
        );
    }

    /// A float travels in the other file at both ends of a call, and the register it comes back in
    /// is the first of that file rather than the first of the other one.
    #[test]
    fn a_call_passes_and_returns_a_float_in_a_vector_register() {
        let f64 = Type::float(rucc_ir::Float::F64);
        let (_, func, made) = make(&[Type::int(32), f64], Some(f64), false, &SYSV);
        let result = made.expect("an integer and a float both fit in registers");
        let (written, read) = operands(&func);
        assert_eq!(read, ["rdi", "xmm0"]);
        assert_eq!(written.first().map(String::as_str), Some("xmm0"));
        assert_eq!(func.class_of(result.result.expect("a float comes back")), Some(SYSV.sse_class));
        // Written once, because the register the result comes back in is already blocked by being
        // named and a clobber that repeated it would be blocking it twice. `rax` is a clobber here
        // rather than the result, which is the same register number in the other file and is the
        // whole reason the two lists are counted apart.
        assert_eq!(written.iter().filter(|name| *name == "xmm0").count(), 1);
        assert!(written.contains(&"rax".to_string()));
    }

    #[test]
    fn a_call_at_a_width_no_register_holds_is_reported_on_either_side() {
        let i128 = Type::int(128);
        assert_eq!(
            make(&[i128], None, false, &SYSV).2,
            Err(Refused { argument: Some(0), missing: Missing::Width })
        );
        assert_eq!(
            make(&[], Some(i128), false, &SYSV).2,
            Err(Refused { argument: None, missing: Missing::Width })
        );
    }

    #[test]
    fn an_argument_wider_than_a_register_has_no_name() {
        assert_eq!(head_of(Type::int(128)), None);
        assert_eq!(head_of(Type::int(8)), Some("x64.arg_val_8"));
        assert_eq!(head_of(Type::int(64)), Some("x64.arg_val_64"));
    }

    /// An address arrives in a general purpose register like any other integer of its width, and
    /// used to be turned away here as a width no register holds, which is what issue 274 is.
    /// `int g(char *s)` is the smallest program that was.
    #[test]
    fn an_address_arrives_in_a_register_like_the_integer_it_is() {
        assert_eq!(head_of(Type::PTR), Some("x64.arg_val_64"));
        assert_eq!(
            bind(&[Type::PTR], &SYSV),
            "mfunc @f {\nblock0:\n    %0:gpr($rdi) = x64.arg_val_64\n}\n"
        );
        // And it travels the same way at a call, on both sides of one.
        assert!(make(&[Type::PTR], Some(Type::PTR), false, &SYSV).2.is_ok());
    }
}