Skip to main content

rucc_codegen/
fold.rs

1//! Folding an address computation into the memory operand of whatever reads it.
2//!
3//! Design: `spec/10-backend.md` section 10.9, and `spec/optimizer/37-machine-level-optimization.md`
4//! section 37.4.
5//!
6//! The selector matches one instruction at a time and offers it its operands' operands, which is
7//! two levels of term and is exactly what an address needs to become a `lea`: `a + i * 4` is an
8//! add at the root with a multiply under it. Put that same address under a load and everything
9//! moves down a level, the multiply is at level two, and no plan the selector has reaches it. So
10//! an array read comes out of selection as two instructions, the `lea` that works the address out
11//! and the `mov` that reads through it, and the second one's addressing mode holds nothing but a
12//! base.
13//!
14//! Which is a pair a peephole can see. When the register a `lea` writes is read by exactly one
15//! instruction, and that instruction reads it as the base of its memory operand, the two addresses
16//! compose: the reader's displacement is a constant added to an address the `lea` already worked
17//! out, so adding the two displacements together gives the address the reader wanted in the mode
18//! the `lea` was using. The `lea` then has no reader at all and goes.
19//!
20//! # What it will not do
21//!
22//! Two indexes. The reader having an index of its own means the composed address wants two scaled
23//! registers and this machine, like every machine, has one. Nothing looks for a way to put them
24//! together because there is not one.
25//!
26//! A displacement that does not fit. The two are added as `i64` and the answer has to be an `i32`,
27//! which is what the field holds. It is not a case that comes up in a program anybody wrote, and
28//! the check is there because the alternative to checking is wrapping.
29//!
30//! A reader in another block. Folding moves the work from where the `lea` is to where the reader
31//! is, and across a block boundary that can mean moving it into a loop. The same rule and the same
32//! reason as `crate::lower::Lowering::foldable`, which is the selector's version of this question.
33//!
34//! A register that something writes in between. Machine IR is in SSA form until the allocator has
35//! run, so a virtual register cannot be, but a physical one can: the frame pointer and the stack
36//! pointer are already physical here, and a call in between writes every register it is allowed
37//! to. Rather than ask which registers are the exceptions, the walk below drops a candidate the
38//! moment anything writes a register its address reads.
39//!
40//! An address whose displacement is not settled. A local's place in the frame and an argument's
41//! place in the caller's is a distance from the stack pointer, and there is no frame until the
42//! allocator has finished, so [`crate::lower`] leaves those instructions with a zero in the
43//! displacement and [`crate::finish`] writes the number in later against a list of which
44//! instruction is which. Folding one of them away would leave that number being written into an
45//! instruction nothing runs, and the fold itself would have composed a displacement that was not
46//! there yet. So the caller says which instructions those are and this leaves them alone. What it
47//! costs is the fold on a local whose address is taken, which is worth having and is not worth
48//! having at the price of `finish` and this pass sharing a secret.
49//!
50//! # Where it runs
51//!
52//! After selection and before the allocator, which is the one window where both instructions
53//! exist and the registers are still virtual. Running it after allocation would work on the
54//! arithmetic and would be reading a register file where the reader's base may have been reused
55//! for something else in between.
56
57use std::collections::{HashMap, HashSet};
58
59use rucc_base::Interner;
60use rucc_mir as mir;
61use rucc_target::{FrameInsts, Role};
62
63/// Folds every address computation that one memory operand reads, and gives back how many.
64///
65/// `waiting` is the instructions whose displacement [`crate::finish`] has still to write, which
66/// are the ones this must not touch.
67///
68/// Run after lowering and before allocation. Running it twice can find more than running it once,
69/// because folding a `lea` into a second `lea` leaves that second one foldable in turn, and the
70/// walk below takes those in the one pass since it goes forwards.
71pub fn addresses(
72    func: &mut mir::Func,
73    insts: &FrameInsts,
74    names: &mut Interner,
75    waiting: &HashSet<mir::Inst>,
76) -> usize {
77    let lea = mir::Opcode::new(names.intern(&format!("{}{}", insts.prefix, insts.lea)));
78    let reads = reads(func);
79    let mut folded = 0;
80    for block in func.blocks().collect::<Vec<_>>() {
81        // One `lea` per register it wrote, dropped again as soon as anything the address reads is
82        // written or the register is read by somebody who is not folding it.
83        let mut open: HashMap<mir::Reg, mir::Inst> = HashMap::new();
84        for inst in func.insts(block).collect::<Vec<_>>() {
85            if let Some(folding) = candidate(func, &open, inst) {
86                let operands = func.push_operands(&folding.operands);
87                let mem = func.add_amode(folding.amode);
88                func[inst].operands = operands;
89                func[inst].mem = Some(mem);
90                open.remove(&folding.base);
91                func.remove_inst(folding.from);
92                folded += 1;
93            }
94            for written in written(func, inst) {
95                open.retain(|reg, &mut held| *reg != written && !touches(func, held, written));
96            }
97            if func[inst].opcode == lea && !waiting.contains(&inst) {
98                if let Some(reg) = written_once(func, &reads, inst) {
99                    open.insert(reg, inst);
100                }
101            }
102        }
103    }
104    folded
105}
106
107/// How many times each virtual register is read, counting the arguments an edge carries.
108///
109/// A `lea` is only worth folding when the instruction folding it is the whole of what reads the
110/// register, since folding does not delete the `lea` for anybody else and doing the address twice
111/// is not a saving. An argument on an edge is a read like any other and is not in any operand
112/// vector, which is the one place this is easy to get wrong.
113fn reads(func: &mir::Func) -> HashMap<mir::Reg, usize> {
114    let mut counts = HashMap::new();
115    for block in func.blocks() {
116        for inst in func.insts(block) {
117            for operand in &func[func[inst].operands] {
118                if operand.role == Role::Use {
119                    *counts.entry(operand.reg).or_insert(0) += 1;
120                }
121            }
122        }
123        for call in &func[block].succs {
124            for &arg in &call.args {
125                *counts.entry(arg).or_insert(0) += 1;
126            }
127        }
128    }
129    counts
130}
131
132/// The one virtual register an instruction writes, when it writes exactly one and exactly one
133/// thing reads it.
134fn written_once(
135    func: &mir::Func,
136    reads: &HashMap<mir::Reg, usize>,
137    inst: mir::Inst,
138) -> Option<mir::Reg> {
139    let operands = &func[func[inst].operands];
140    let mut defs = operands.iter().filter(|operand| operand.role != Role::Use);
141    let def = defs.next()?;
142    if defs.next().is_some() || !def.reg.is_virtual() || reads.get(&def.reg) != Some(&1) {
143        return None;
144    }
145    Some(def.reg)
146}
147
148/// The registers an instruction writes.
149fn written(func: &mir::Func, inst: mir::Inst) -> Vec<mir::Reg> {
150    func[func[inst].operands]
151        .iter()
152        .filter(|operand| operand.role != Role::Use)
153        .map(|operand| operand.reg)
154        .collect()
155}
156
157/// Whether an address computation reads that register, which is what makes writing it the end of
158/// the chance to fold it.
159fn touches(func: &mir::Func, inst: mir::Inst, reg: mir::Reg) -> bool {
160    let Some(mem) = func[inst].mem else { return false };
161    let amode = func[mem];
162    let operands = &func[func[inst].operands];
163    [amode.base, amode.index]
164        .into_iter()
165        .flatten()
166        .filter_map(|at| operands.get(usize::from(at)))
167        .any(|operand| operand.reg == reg)
168}
169
170/// The register an instruction's memory operand reads as its base, when that is the whole of what
171/// its memory operand is.
172///
173/// A symbol or an index means the two addresses do not compose, and this is where both are turned
174/// down, because the reader is the half of the pair with no room left in it.
175fn base_reg(func: &mir::Func, inst: mir::Inst) -> Option<mir::Reg> {
176    let amode = func[func[inst].mem?];
177    if amode.index.is_some() || amode.symbol.is_some() || amode.got {
178        return None;
179    }
180    Some(func[func[inst].operands].get(usize::from(amode.base?))?.reg)
181}
182
183/// A fold that has been checked and not yet done.
184///
185/// Everything the rewrite needs is worked out here rather than after the decision, so that the
186/// decision is the last thing that can go either way and the rewrite itself is three assignments
187/// that cannot fail.
188struct Folding {
189    /// The address instruction that goes, because nothing reads what it wrote any more.
190    from: mir::Inst,
191    /// The register it wrote, which stops being open the moment this is done.
192    base: mir::Reg,
193    /// What the reader's operands become.
194    operands: Vec<mir::Operand>,
195    /// What the reader's addressing mode becomes.
196    amode: mir::Amode,
197}
198
199/// The `lea` whose address this instruction should read directly, and what reading it directly
200/// makes of the instruction.
201///
202/// The operand vector is rebuilt rather than edited because the registers a memory operand names
203/// come last in it, which is the invariant [`mir::InstBuilder::mem`] keeps and the printer and the
204/// allocator both read. Dropping the base the reader had and putting the `lea`'s base and index on
205/// the end keeps it, and the indices in the new addressing mode are worked out from the length
206/// rather than carried over.
207fn candidate(
208    func: &mir::Func,
209    open: &HashMap<mir::Reg, mir::Inst>,
210    inst: mir::Inst,
211) -> Option<Folding> {
212    let base = base_reg(func, inst)?;
213    let from = *open.get(&base)?;
214    let address = func[func[from].mem?];
215    // The reader holds the base in its last operand and nothing else names it, since the register
216    // has one read in the whole function and this is it. So the composed address is the `lea`'s
217    // with the reader's displacement added, and the only thing that can go wrong is the width of
218    // the field it goes in.
219    let disp = i64::from(address.disp) + i64::from(func[func[inst].mem?].disp);
220    let mut amode = mir::Amode { disp: i32::try_from(disp).ok()?, ..address };
221
222    let taken = &func[func[from].operands];
223    let reader = &func[func[inst].operands];
224    let mut operands = reader.get(..reader.len().checked_sub(1)?)?.to_vec();
225    for (at, into) in [(address.base, &mut amode.base), (address.index, &mut amode.index)] {
226        let Some(at) = at else { continue };
227        operands.push(*taken.get(usize::from(at))?);
228        *into = Some(u8::try_from(operands.len() - 1).ok()?);
229    }
230    Some(Folding { from, base, operands, amode })
231}
232
233#[cfg(test)]
234mod tests {
235    use rucc_target::x86_64::{FRAME, GPR, RDI};
236
237    use super::*;
238
239    /// A function with one block, and the names it was built with.
240    fn empty() -> (Interner, mir::Func, mir::Block) {
241        let mut names = Interner::new();
242        let mut func = mir::Func::new(names.intern("f"));
243        let block = func.create_block();
244        (names, func, block)
245    }
246
247    /// The opcode of that name on this target.
248    fn op(names: &mut Interner, name: &str) -> mir::Opcode {
249        mir::Opcode::new(names.intern(&format!("{}{name}", FRAME.prefix)))
250    }
251
252    /// What every instruction in a block came to, as opcodes and addressing modes.
253    fn shape(func: &mir::Func, names: &Interner, block: mir::Block) -> Vec<(String, mir::Amode)> {
254        func.insts(block)
255            .map(|inst| {
256                let amode = func[inst].mem.map_or(mir::Amode::NOTHING, |mem| func[mem]);
257                (names.resolve(func[inst].opcode.name()).to_owned(), amode)
258            })
259            .collect()
260    }
261
262    /// The registers a memory operand names, in the order the addressing mode names them.
263    fn address_regs(func: &mir::Func, inst: mir::Inst) -> Vec<mir::Reg> {
264        let amode = func[func[inst].mem.expect("a memory operand")];
265        let operands = &func[func[inst].operands];
266        [amode.base, amode.index]
267            .into_iter()
268            .flatten()
269            .map(|at| operands[usize::from(at)].reg)
270            .collect()
271    }
272
273    /// An array read as selection leaves it: a `lea` that scales the index and adds the base, and
274    /// a `mov` that reads through the register it wrote.
275    #[test]
276    fn an_address_a_load_reads_once_becomes_the_load_s_own_addressing_mode() {
277        let (mut names, mut func, block) = empty();
278        let array = func.new_vreg(GPR);
279        let index = func.new_vreg(GPR);
280        let address = func.new_vreg(GPR);
281        let value = func.new_vreg(GPR);
282        let lea = op(&mut names, FRAME.lea);
283        let load = op(&mut names, "mov_rm_32");
284        func.build(block, lea)
285            .def(address, GPR)
286            .mem(
287                mir::Mem::at(mir::Operand::read(array, GPR))
288                    .indexed(mir::Operand::read(index, GPR), 4),
289            )
290            .finish();
291        func.build(block, load)
292            .def(value, GPR)
293            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
294            .finish();
295
296        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
297
298        let left = shape(&func, &names, block);
299        assert_eq!(left.len(), 1, "the address is worked out twice: {left:?}");
300        assert_eq!(left[0].0, format!("{}mov_rm_32", FRAME.prefix));
301        assert_eq!(left[0].1.scale, 4);
302        assert_eq!(left[0].1.disp, 0);
303        let inst = func.insts(block).next().expect("the load is still there");
304        assert_eq!(address_regs(&func, inst), vec![array, index], "the load reads the wrong pair");
305    }
306
307    /// The two displacements are added, which is the whole of what composing them takes when one
308    /// of the two addresses has room for an index and the other has none.
309    #[test]
310    fn the_displacements_of_the_two_addresses_are_added() {
311        let (mut names, mut func, block) = empty();
312        let array = func.new_vreg(GPR);
313        let address = func.new_vreg(GPR);
314        let value = func.new_vreg(GPR);
315        let lea = op(&mut names, FRAME.lea);
316        let load = op(&mut names, "mov_rm_32");
317        func.build(block, lea)
318            .def(address, GPR)
319            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
320            .finish();
321        func.build(block, load)
322            .def(value, GPR)
323            .mem(mir::Mem::at(mir::Operand::read(address, GPR)).plus(8))
324            .finish();
325
326        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
327
328        let left = shape(&func, &names, block);
329        assert_eq!(left.len(), 1);
330        assert_eq!(left[0].1.disp, 24, "the field is at the sum of the two offsets or nowhere");
331    }
332
333    /// A store keeps the value it writes, which is the operand the address does not name, and the
334    /// rebuilt operand vector has to hold on to it.
335    #[test]
336    fn a_store_keeps_the_value_it_is_storing() {
337        let (mut names, mut func, block) = empty();
338        let array = func.new_vreg(GPR);
339        let index = func.new_vreg(GPR);
340        let address = func.new_vreg(GPR);
341        let value = func.new_vreg(GPR);
342        let lea = op(&mut names, FRAME.lea);
343        let store = op(&mut names, "mov_mr_32");
344        func.build(block, lea)
345            .def(address, GPR)
346            .mem(
347                mir::Mem::at(mir::Operand::read(array, GPR))
348                    .indexed(mir::Operand::read(index, GPR), 8),
349            )
350            .finish();
351        func.build(block, store)
352            .uses(value, GPR)
353            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
354            .finish();
355
356        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
357
358        let inst = func.insts(block).next().expect("the store is still there");
359        let regs: Vec<mir::Reg> = func[func[inst].operands].iter().map(|op| op.reg).collect();
360        assert_eq!(regs, vec![value, array, index], "the value the store writes went missing");
361        assert_eq!(func[func[inst].mem.expect("a memory operand")].scale, 8);
362    }
363
364    /// Two readers is not a saving. Folding into either of them leaves the `lea` where it is for
365    /// the other, and the address is then worked out twice rather than once.
366    #[test]
367    fn an_address_two_instructions_read_is_left_where_it_is() {
368        let (mut names, mut func, block) = empty();
369        let array = func.new_vreg(GPR);
370        let address = func.new_vreg(GPR);
371        let lea = op(&mut names, FRAME.lea);
372        let load = op(&mut names, "mov_rm_32");
373        func.build(block, lea)
374            .def(address, GPR)
375            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
376            .finish();
377        for _ in 0..2 {
378            let value = func.new_vreg(GPR);
379            func.build(block, load)
380                .def(value, GPR)
381                .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
382                .finish();
383        }
384
385        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
386        assert_eq!(shape(&func, &names, block).len(), 3);
387    }
388
389    /// The reader having an index of its own is the one shape that does not compose, since the
390    /// answer would want two scaled registers.
391    #[test]
392    fn a_reader_that_already_has_an_index_is_left_alone() {
393        let (mut names, mut func, block) = empty();
394        let array = func.new_vreg(GPR);
395        let index = func.new_vreg(GPR);
396        let address = func.new_vreg(GPR);
397        let value = func.new_vreg(GPR);
398        let lea = op(&mut names, FRAME.lea);
399        let load = op(&mut names, "mov_rm_32");
400        func.build(block, lea)
401            .def(address, GPR)
402            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
403            .finish();
404        func.build(block, load)
405            .def(value, GPR)
406            .mem(
407                mir::Mem::at(mir::Operand::read(address, GPR))
408                    .indexed(mir::Operand::read(index, GPR), 4),
409            )
410            .finish();
411
412        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
413        assert_eq!(shape(&func, &names, block).len(), 2);
414    }
415
416    /// The two displacements add up to more than the field holds, so the pair stays a pair. The
417    /// program that does this is one nobody wrote, and the point of the test is that the answer is
418    /// a refusal rather than a wrap.
419    #[test]
420    fn two_displacements_that_do_not_fit_together_are_not_put_together() {
421        let (mut names, mut func, block) = empty();
422        let array = func.new_vreg(GPR);
423        let address = func.new_vreg(GPR);
424        let value = func.new_vreg(GPR);
425        let lea = op(&mut names, FRAME.lea);
426        let load = op(&mut names, "mov_rm_32");
427        func.build(block, lea)
428            .def(address, GPR)
429            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(i32::MAX))
430            .finish();
431        func.build(block, load)
432            .def(value, GPR)
433            .mem(mir::Mem::at(mir::Operand::read(address, GPR)).plus(1))
434            .finish();
435
436        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
437        assert_eq!(shape(&func, &names, block).len(), 2);
438    }
439
440    /// A physical register the address reads, written between the two. Machine IR is in SSA form
441    /// here so a virtual register cannot be, and this is why the walk asks anyway.
442    #[test]
443    fn a_register_the_address_reads_being_written_in_between_ends_the_chance() {
444        let (mut names, mut func, block) = empty();
445        let array = mir::Reg::physical(RDI);
446        let address = func.new_vreg(GPR);
447        let value = func.new_vreg(GPR);
448        let lea = op(&mut names, FRAME.lea);
449        let load = op(&mut names, "mov_rm_32");
450        let put = op(&mut names, "mov_ri_64");
451        func.build(block, lea)
452            .def(address, GPR)
453            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
454            .finish();
455        func.build(block, put).def(array, GPR).imm(7).finish();
456        func.build(block, load)
457            .def(value, GPR)
458            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
459            .finish();
460
461        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
462        assert_eq!(shape(&func, &names, block).len(), 3);
463    }
464
465    /// A reader in another block. Folding would move the address to wherever that block is, and
466    /// this pass has no way to know whether that is somewhere it runs more often.
467    #[test]
468    fn a_reader_in_another_block_is_not_one_this_folds_into() {
469        let (mut names, mut func, block) = empty();
470        let next = func.create_block();
471        let array = func.new_vreg(GPR);
472        let address = func.new_vreg(GPR);
473        let value = func.new_vreg(GPR);
474        let lea = op(&mut names, FRAME.lea);
475        let load = op(&mut names, "mov_rm_32");
476        func.build(block, lea)
477            .def(address, GPR)
478            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
479            .finish();
480        *func.succs_mut(block) = vec![mir::BlockCall::to(next)];
481        func.build(next, load)
482            .def(value, GPR)
483            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
484            .finish();
485
486        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
487    }
488
489    /// A chain of two, which is what an address of a field of an element of an array comes out as.
490    /// The walk goes forwards, so the second `lea` is folded into the load and then the first is
491    /// folded into what is left of the second, both in the one pass.
492    #[test]
493    fn a_chain_of_two_addresses_is_folded_the_whole_way_in_one_pass() {
494        let (mut names, mut func, block) = empty();
495        let array = func.new_vreg(GPR);
496        let index = func.new_vreg(GPR);
497        let element = func.new_vreg(GPR);
498        let field = func.new_vreg(GPR);
499        let value = func.new_vreg(GPR);
500        let lea = op(&mut names, FRAME.lea);
501        let load = op(&mut names, "mov_rm_32");
502        func.build(block, lea)
503            .def(element, GPR)
504            .mem(
505                mir::Mem::at(mir::Operand::read(array, GPR))
506                    .indexed(mir::Operand::read(index, GPR), 8),
507            )
508            .finish();
509        func.build(block, lea)
510            .def(field, GPR)
511            .mem(mir::Mem::at(mir::Operand::read(element, GPR)).plus(4))
512            .finish();
513        func.build(block, load)
514            .def(value, GPR)
515            .mem(mir::Mem::at(mir::Operand::read(field, GPR)))
516            .finish();
517
518        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 2);
519
520        let left = shape(&func, &names, block);
521        assert_eq!(left.len(), 1, "one of the two addresses is still its own instruction");
522        assert_eq!(left[0].1.scale, 8);
523        assert_eq!(left[0].1.disp, 4);
524        let inst = func.insts(block).next().expect("the load is still there");
525        assert_eq!(address_regs(&func, inst), vec![array, index]);
526    }
527
528    /// An address of a global, which the `lea` holds as a symbol rather than as a register. It
529    /// composes the same way and the reader ends up naming the symbol itself, which is one
530    /// instruction rather than two for every read of a global with a constant subscript.
531    #[test]
532    fn an_address_of_a_global_folds_into_the_reader_symbol_and_all() {
533        let (mut names, mut func, block) = empty();
534        let global = names.intern("counters");
535        let address = func.new_vreg(GPR);
536        let value = func.new_vreg(GPR);
537        let lea = op(&mut names, FRAME.lea);
538        let load = op(&mut names, "mov_rm_32");
539        func.build(block, lea).def(address, GPR).mem(mir::Mem::of(global)).finish();
540        func.build(block, load)
541            .def(value, GPR)
542            .mem(mir::Mem::at(mir::Operand::read(address, GPR)).plus(12))
543            .finish();
544
545        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
546
547        let left = shape(&func, &names, block);
548        assert_eq!(left.len(), 1);
549        assert_eq!(left[0].1.symbol, Some(global));
550        assert_eq!(left[0].1.disp, 12);
551    }
552
553    /// An address into the frame, which reads as an address of nothing until `finish` writes the
554    /// distance in. Folding it would compose a displacement that is not there yet and would leave
555    /// `finish` writing the real one into an instruction nothing runs.
556    #[test]
557    fn an_address_whose_displacement_is_still_to_be_written_is_left_where_it_is() {
558        let (mut names, mut func, block) = empty();
559        let sp = mir::Reg::physical(RDI);
560        let address = func.new_vreg(GPR);
561        let value = func.new_vreg(GPR);
562        let lea = op(&mut names, FRAME.lea);
563        let load = op(&mut names, "mov_rm_32");
564        let local = func
565            .build(block, lea)
566            .def(address, GPR)
567            .mem(mir::Mem::at(mir::Operand::read(sp, GPR)))
568            .finish();
569        func.build(block, load)
570            .def(value, GPR)
571            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
572            .finish();
573
574        let waiting = HashSet::from([local]);
575        assert_eq!(addresses(&mut func, &FRAME, &mut names, &waiting), 0);
576        assert_eq!(shape(&func, &names, block).len(), 2);
577
578        // And the same function with nothing waiting, so that what the test pins is the list and
579        // not some other thing about the pair.
580        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
581    }
582
583    /// An instruction that is not the target's address instruction, writing a register a load
584    /// reads. A load through the result of a load is two loads and folding one into the other
585    /// would read the wrong memory, so the opcode is checked rather than the shape.
586    #[test]
587    fn only_the_target_s_address_instruction_is_one_this_folds() {
588        let (mut names, mut func, block) = empty();
589        let array = func.new_vreg(GPR);
590        let address = func.new_vreg(GPR);
591        let value = func.new_vreg(GPR);
592        let load = op(&mut names, "mov_rm_64");
593        let read = op(&mut names, "mov_rm_32");
594        func.build(block, load)
595            .def(address, GPR)
596            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
597            .finish();
598        func.build(block, read)
599            .def(value, GPR)
600            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
601            .finish();
602
603        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
604        assert_eq!(shape(&func, &names, block).len(), 2);
605    }
606}