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.
113///
114/// [`crate::layout`] asks the same question about the byte a comparison wrote, for the same
115/// reason and while the registers are still virtual for the same reason, so it reads this rather
116/// than counting again.
117pub(crate) fn reads(func: &mir::Func) -> HashMap<mir::Reg, usize> {
118    let mut counts = HashMap::new();
119    for block in func.blocks() {
120        for inst in func.insts(block) {
121            for operand in &func[func[inst].operands] {
122                if operand.role == Role::Use {
123                    *counts.entry(operand.reg).or_insert(0) += 1;
124                }
125            }
126        }
127        for call in &func[block].succs {
128            for &arg in &call.args {
129                *counts.entry(arg).or_insert(0) += 1;
130            }
131        }
132    }
133    counts
134}
135
136/// The one virtual register an instruction writes, when it writes exactly one and exactly one
137/// thing reads it.
138fn written_once(
139    func: &mir::Func,
140    reads: &HashMap<mir::Reg, usize>,
141    inst: mir::Inst,
142) -> Option<mir::Reg> {
143    let operands = &func[func[inst].operands];
144    let mut defs = operands.iter().filter(|operand| operand.role != Role::Use);
145    let def = defs.next()?;
146    if defs.next().is_some() || !def.reg.is_virtual() || reads.get(&def.reg) != Some(&1) {
147        return None;
148    }
149    Some(def.reg)
150}
151
152/// The registers an instruction writes.
153fn written(func: &mir::Func, inst: mir::Inst) -> Vec<mir::Reg> {
154    func[func[inst].operands]
155        .iter()
156        .filter(|operand| operand.role != Role::Use)
157        .map(|operand| operand.reg)
158        .collect()
159}
160
161/// Whether an address computation reads that register, which is what makes writing it the end of
162/// the chance to fold it.
163fn touches(func: &mir::Func, inst: mir::Inst, reg: mir::Reg) -> bool {
164    let Some(mem) = func[inst].mem else { return false };
165    let amode = func[mem];
166    let operands = &func[func[inst].operands];
167    [amode.base, amode.index]
168        .into_iter()
169        .flatten()
170        .filter_map(|at| operands.get(usize::from(at)))
171        .any(|operand| operand.reg == reg)
172}
173
174/// The register an instruction's memory operand reads as its base, when that is the whole of what
175/// its memory operand is.
176///
177/// A symbol or an index means the two addresses do not compose, and this is where both are turned
178/// down, because the reader is the half of the pair with no room left in it.
179fn base_reg(func: &mir::Func, inst: mir::Inst) -> Option<mir::Reg> {
180    let amode = func[func[inst].mem?];
181    if amode.index.is_some() || amode.symbol.is_some() || amode.got {
182        return None;
183    }
184    Some(func[func[inst].operands].get(usize::from(amode.base?))?.reg)
185}
186
187/// A fold that has been checked and not yet done.
188///
189/// Everything the rewrite needs is worked out here rather than after the decision, so that the
190/// decision is the last thing that can go either way and the rewrite itself is three assignments
191/// that cannot fail.
192struct Folding {
193    /// The address instruction that goes, because nothing reads what it wrote any more.
194    from: mir::Inst,
195    /// The register it wrote, which stops being open the moment this is done.
196    base: mir::Reg,
197    /// What the reader's operands become.
198    operands: Vec<mir::Operand>,
199    /// What the reader's addressing mode becomes.
200    amode: mir::Amode,
201}
202
203/// The `lea` whose address this instruction should read directly, and what reading it directly
204/// makes of the instruction.
205///
206/// The operand vector is rebuilt rather than edited because the registers a memory operand names
207/// come last in it, which is the invariant [`mir::InstBuilder::mem`] keeps and the printer and the
208/// allocator both read. Dropping the base the reader had and putting the `lea`'s base and index on
209/// the end keeps it, and the indices in the new addressing mode are worked out from the length
210/// rather than carried over.
211fn candidate(
212    func: &mir::Func,
213    open: &HashMap<mir::Reg, mir::Inst>,
214    inst: mir::Inst,
215) -> Option<Folding> {
216    let base = base_reg(func, inst)?;
217    let from = *open.get(&base)?;
218    let address = func[func[from].mem?];
219    // The reader holds the base in its last operand and nothing else names it, since the register
220    // has one read in the whole function and this is it. So the composed address is the `lea`'s
221    // with the reader's displacement added, and the only thing that can go wrong is the width of
222    // the field it goes in.
223    let disp = i64::from(address.disp) + i64::from(func[func[inst].mem?].disp);
224    let mut amode = mir::Amode { disp: i32::try_from(disp).ok()?, ..address };
225
226    let taken = &func[func[from].operands];
227    let reader = &func[func[inst].operands];
228    let mut operands = reader.get(..reader.len().checked_sub(1)?)?.to_vec();
229    for (at, into) in [(address.base, &mut amode.base), (address.index, &mut amode.index)] {
230        let Some(at) = at else { continue };
231        operands.push(*taken.get(usize::from(at))?);
232        *into = Some(u8::try_from(operands.len() - 1).ok()?);
233    }
234    Some(Folding { from, base, operands, amode })
235}
236
237#[cfg(test)]
238mod tests {
239    use rucc_target::x86_64::{FRAME, GPR, RDI};
240
241    use super::*;
242
243    /// A function with one block, and the names it was built with.
244    fn empty() -> (Interner, mir::Func, mir::Block) {
245        let mut names = Interner::new();
246        let mut func = mir::Func::new(names.intern("f"));
247        let block = func.create_block();
248        (names, func, block)
249    }
250
251    /// The opcode of that name on this target.
252    fn op(names: &mut Interner, name: &str) -> mir::Opcode {
253        mir::Opcode::new(names.intern(&format!("{}{name}", FRAME.prefix)))
254    }
255
256    /// What every instruction in a block came to, as opcodes and addressing modes.
257    fn shape(func: &mir::Func, names: &Interner, block: mir::Block) -> Vec<(String, mir::Amode)> {
258        func.insts(block)
259            .map(|inst| {
260                let amode = func[inst].mem.map_or(mir::Amode::NOTHING, |mem| func[mem]);
261                (names.resolve(func[inst].opcode.name()).to_owned(), amode)
262            })
263            .collect()
264    }
265
266    /// The registers a memory operand names, in the order the addressing mode names them.
267    fn address_regs(func: &mir::Func, inst: mir::Inst) -> Vec<mir::Reg> {
268        let amode = func[func[inst].mem.expect("a memory operand")];
269        let operands = &func[func[inst].operands];
270        [amode.base, amode.index]
271            .into_iter()
272            .flatten()
273            .map(|at| operands[usize::from(at)].reg)
274            .collect()
275    }
276
277    /// An array read as selection leaves it: a `lea` that scales the index and adds the base, and
278    /// a `mov` that reads through the register it wrote.
279    #[test]
280    fn an_address_a_load_reads_once_becomes_the_load_s_own_addressing_mode() {
281        let (mut names, mut func, block) = empty();
282        let array = func.new_vreg(GPR);
283        let index = func.new_vreg(GPR);
284        let address = func.new_vreg(GPR);
285        let value = func.new_vreg(GPR);
286        let lea = op(&mut names, FRAME.lea);
287        let load = op(&mut names, "mov_rm_32");
288        func.build(block, lea)
289            .def(address, GPR)
290            .mem(
291                mir::Mem::at(mir::Operand::read(array, GPR))
292                    .indexed(mir::Operand::read(index, GPR), 4),
293            )
294            .finish();
295        func.build(block, load)
296            .def(value, GPR)
297            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
298            .finish();
299
300        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
301
302        let left = shape(&func, &names, block);
303        assert_eq!(left.len(), 1, "the address is worked out twice: {left:?}");
304        assert_eq!(left[0].0, format!("{}mov_rm_32", FRAME.prefix));
305        assert_eq!(left[0].1.scale, 4);
306        assert_eq!(left[0].1.disp, 0);
307        let inst = func.insts(block).next().expect("the load is still there");
308        assert_eq!(address_regs(&func, inst), vec![array, index], "the load reads the wrong pair");
309    }
310
311    /// The two displacements are added, which is the whole of what composing them takes when one
312    /// of the two addresses has room for an index and the other has none.
313    #[test]
314    fn the_displacements_of_the_two_addresses_are_added() {
315        let (mut names, mut func, block) = empty();
316        let array = func.new_vreg(GPR);
317        let address = func.new_vreg(GPR);
318        let value = func.new_vreg(GPR);
319        let lea = op(&mut names, FRAME.lea);
320        let load = op(&mut names, "mov_rm_32");
321        func.build(block, lea)
322            .def(address, GPR)
323            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
324            .finish();
325        func.build(block, load)
326            .def(value, GPR)
327            .mem(mir::Mem::at(mir::Operand::read(address, GPR)).plus(8))
328            .finish();
329
330        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
331
332        let left = shape(&func, &names, block);
333        assert_eq!(left.len(), 1);
334        assert_eq!(left[0].1.disp, 24, "the field is at the sum of the two offsets or nowhere");
335    }
336
337    /// A store keeps the value it writes, which is the operand the address does not name, and the
338    /// rebuilt operand vector has to hold on to it.
339    #[test]
340    fn a_store_keeps_the_value_it_is_storing() {
341        let (mut names, mut func, block) = empty();
342        let array = func.new_vreg(GPR);
343        let index = func.new_vreg(GPR);
344        let address = func.new_vreg(GPR);
345        let value = func.new_vreg(GPR);
346        let lea = op(&mut names, FRAME.lea);
347        let store = op(&mut names, "mov_mr_32");
348        func.build(block, lea)
349            .def(address, GPR)
350            .mem(
351                mir::Mem::at(mir::Operand::read(array, GPR))
352                    .indexed(mir::Operand::read(index, GPR), 8),
353            )
354            .finish();
355        func.build(block, store)
356            .uses(value, GPR)
357            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
358            .finish();
359
360        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
361
362        let inst = func.insts(block).next().expect("the store is still there");
363        let regs: Vec<mir::Reg> = func[func[inst].operands].iter().map(|op| op.reg).collect();
364        assert_eq!(regs, vec![value, array, index], "the value the store writes went missing");
365        assert_eq!(func[func[inst].mem.expect("a memory operand")].scale, 8);
366    }
367
368    /// Two readers is not a saving. Folding into either of them leaves the `lea` where it is for
369    /// the other, and the address is then worked out twice rather than once.
370    #[test]
371    fn an_address_two_instructions_read_is_left_where_it_is() {
372        let (mut names, mut func, block) = empty();
373        let array = func.new_vreg(GPR);
374        let address = func.new_vreg(GPR);
375        let lea = op(&mut names, FRAME.lea);
376        let load = op(&mut names, "mov_rm_32");
377        func.build(block, lea)
378            .def(address, GPR)
379            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
380            .finish();
381        for _ in 0..2 {
382            let value = func.new_vreg(GPR);
383            func.build(block, load)
384                .def(value, GPR)
385                .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
386                .finish();
387        }
388
389        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
390        assert_eq!(shape(&func, &names, block).len(), 3);
391    }
392
393    /// The reader having an index of its own is the one shape that does not compose, since the
394    /// answer would want two scaled registers.
395    #[test]
396    fn a_reader_that_already_has_an_index_is_left_alone() {
397        let (mut names, mut func, block) = empty();
398        let array = func.new_vreg(GPR);
399        let index = func.new_vreg(GPR);
400        let address = func.new_vreg(GPR);
401        let value = func.new_vreg(GPR);
402        let lea = op(&mut names, FRAME.lea);
403        let load = op(&mut names, "mov_rm_32");
404        func.build(block, lea)
405            .def(address, GPR)
406            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
407            .finish();
408        func.build(block, load)
409            .def(value, GPR)
410            .mem(
411                mir::Mem::at(mir::Operand::read(address, GPR))
412                    .indexed(mir::Operand::read(index, GPR), 4),
413            )
414            .finish();
415
416        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
417        assert_eq!(shape(&func, &names, block).len(), 2);
418    }
419
420    /// The two displacements add up to more than the field holds, so the pair stays a pair. The
421    /// program that does this is one nobody wrote, and the point of the test is that the answer is
422    /// a refusal rather than a wrap.
423    #[test]
424    fn two_displacements_that_do_not_fit_together_are_not_put_together() {
425        let (mut names, mut func, block) = empty();
426        let array = func.new_vreg(GPR);
427        let address = func.new_vreg(GPR);
428        let value = func.new_vreg(GPR);
429        let lea = op(&mut names, FRAME.lea);
430        let load = op(&mut names, "mov_rm_32");
431        func.build(block, lea)
432            .def(address, GPR)
433            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(i32::MAX))
434            .finish();
435        func.build(block, load)
436            .def(value, GPR)
437            .mem(mir::Mem::at(mir::Operand::read(address, GPR)).plus(1))
438            .finish();
439
440        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
441        assert_eq!(shape(&func, &names, block).len(), 2);
442    }
443
444    /// A physical register the address reads, written between the two. Machine IR is in SSA form
445    /// here so a virtual register cannot be, and this is why the walk asks anyway.
446    #[test]
447    fn a_register_the_address_reads_being_written_in_between_ends_the_chance() {
448        let (mut names, mut func, block) = empty();
449        let array = mir::Reg::physical(RDI);
450        let address = func.new_vreg(GPR);
451        let value = func.new_vreg(GPR);
452        let lea = op(&mut names, FRAME.lea);
453        let load = op(&mut names, "mov_rm_32");
454        let put = op(&mut names, "mov_ri_64");
455        func.build(block, lea)
456            .def(address, GPR)
457            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
458            .finish();
459        func.build(block, put).def(array, GPR).imm(7).finish();
460        func.build(block, load)
461            .def(value, GPR)
462            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
463            .finish();
464
465        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
466        assert_eq!(shape(&func, &names, block).len(), 3);
467    }
468
469    /// A reader in another block. Folding would move the address to wherever that block is, and
470    /// this pass has no way to know whether that is somewhere it runs more often.
471    #[test]
472    fn a_reader_in_another_block_is_not_one_this_folds_into() {
473        let (mut names, mut func, block) = empty();
474        let next = func.create_block();
475        let array = func.new_vreg(GPR);
476        let address = func.new_vreg(GPR);
477        let value = func.new_vreg(GPR);
478        let lea = op(&mut names, FRAME.lea);
479        let load = op(&mut names, "mov_rm_32");
480        func.build(block, lea)
481            .def(address, GPR)
482            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
483            .finish();
484        *func.succs_mut(block) = vec![mir::BlockCall::to(next)];
485        func.build(next, load)
486            .def(value, GPR)
487            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
488            .finish();
489
490        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
491    }
492
493    /// A chain of two, which is what an address of a field of an element of an array comes out as.
494    /// The walk goes forwards, so the second `lea` is folded into the load and then the first is
495    /// folded into what is left of the second, both in the one pass.
496    #[test]
497    fn a_chain_of_two_addresses_is_folded_the_whole_way_in_one_pass() {
498        let (mut names, mut func, block) = empty();
499        let array = func.new_vreg(GPR);
500        let index = func.new_vreg(GPR);
501        let element = func.new_vreg(GPR);
502        let field = func.new_vreg(GPR);
503        let value = func.new_vreg(GPR);
504        let lea = op(&mut names, FRAME.lea);
505        let load = op(&mut names, "mov_rm_32");
506        func.build(block, lea)
507            .def(element, GPR)
508            .mem(
509                mir::Mem::at(mir::Operand::read(array, GPR))
510                    .indexed(mir::Operand::read(index, GPR), 8),
511            )
512            .finish();
513        func.build(block, lea)
514            .def(field, GPR)
515            .mem(mir::Mem::at(mir::Operand::read(element, GPR)).plus(4))
516            .finish();
517        func.build(block, load)
518            .def(value, GPR)
519            .mem(mir::Mem::at(mir::Operand::read(field, GPR)))
520            .finish();
521
522        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 2);
523
524        let left = shape(&func, &names, block);
525        assert_eq!(left.len(), 1, "one of the two addresses is still its own instruction");
526        assert_eq!(left[0].1.scale, 8);
527        assert_eq!(left[0].1.disp, 4);
528        let inst = func.insts(block).next().expect("the load is still there");
529        assert_eq!(address_regs(&func, inst), vec![array, index]);
530    }
531
532    /// An address of a global, which the `lea` holds as a symbol rather than as a register. It
533    /// composes the same way and the reader ends up naming the symbol itself, which is one
534    /// instruction rather than two for every read of a global with a constant subscript.
535    #[test]
536    fn an_address_of_a_global_folds_into_the_reader_symbol_and_all() {
537        let (mut names, mut func, block) = empty();
538        let global = names.intern("counters");
539        let address = func.new_vreg(GPR);
540        let value = func.new_vreg(GPR);
541        let lea = op(&mut names, FRAME.lea);
542        let load = op(&mut names, "mov_rm_32");
543        func.build(block, lea).def(address, GPR).mem(mir::Mem::of(global)).finish();
544        func.build(block, load)
545            .def(value, GPR)
546            .mem(mir::Mem::at(mir::Operand::read(address, GPR)).plus(12))
547            .finish();
548
549        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
550
551        let left = shape(&func, &names, block);
552        assert_eq!(left.len(), 1);
553        assert_eq!(left[0].1.symbol, Some(global));
554        assert_eq!(left[0].1.disp, 12);
555    }
556
557    /// An address into the frame, which reads as an address of nothing until `finish` writes the
558    /// distance in. Folding it would compose a displacement that is not there yet and would leave
559    /// `finish` writing the real one into an instruction nothing runs.
560    #[test]
561    fn an_address_whose_displacement_is_still_to_be_written_is_left_where_it_is() {
562        let (mut names, mut func, block) = empty();
563        let sp = mir::Reg::physical(RDI);
564        let address = func.new_vreg(GPR);
565        let value = func.new_vreg(GPR);
566        let lea = op(&mut names, FRAME.lea);
567        let load = op(&mut names, "mov_rm_32");
568        let local = func
569            .build(block, lea)
570            .def(address, GPR)
571            .mem(mir::Mem::at(mir::Operand::read(sp, GPR)))
572            .finish();
573        func.build(block, load)
574            .def(value, GPR)
575            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
576            .finish();
577
578        let waiting = HashSet::from([local]);
579        assert_eq!(addresses(&mut func, &FRAME, &mut names, &waiting), 0);
580        assert_eq!(shape(&func, &names, block).len(), 2);
581
582        // And the same function with nothing waiting, so that what the test pins is the list and
583        // not some other thing about the pair.
584        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 1);
585    }
586
587    /// An instruction that is not the target's address instruction, writing a register a load
588    /// reads. A load through the result of a load is two loads and folding one into the other
589    /// would read the wrong memory, so the opcode is checked rather than the shape.
590    #[test]
591    fn only_the_target_s_address_instruction_is_one_this_folds() {
592        let (mut names, mut func, block) = empty();
593        let array = func.new_vreg(GPR);
594        let address = func.new_vreg(GPR);
595        let value = func.new_vreg(GPR);
596        let load = op(&mut names, "mov_rm_64");
597        let read = op(&mut names, "mov_rm_32");
598        func.build(block, load)
599            .def(address, GPR)
600            .mem(mir::Mem::at(mir::Operand::read(array, GPR)).plus(16))
601            .finish();
602        func.build(block, read)
603            .def(value, GPR)
604            .mem(mir::Mem::at(mir::Operand::read(address, GPR)))
605            .finish();
606
607        assert_eq!(addresses(&mut func, &FRAME, &mut names, &HashSet::new()), 0);
608        assert_eq!(shape(&func, &names, block).len(), 2);
609    }
610}