Skip to main content

rucc_opt/
memssa.rs

1//! Memory SSA: the chain, and the budgeted walk back to the store a load sees.
2//!
3//! Design: `spec/optimizer/09-memory-ssa.md`. The representation is in `rucc-ir` and this is what
4//! builds it and what reads it.
5//!
6//! # One variable
7//!
8//! GCC has had this since 2004 and calls it virtual operands: a statement that reads memory
9//! carries a VUSE, one that writes memory carries a VDEF, and both are versions of one artificial
10//! variable called `.MEM`. LLVM calls the same three things `MemoryUse`, `MemoryDef` and
11//! `MemoryPhi`. The idea in both is to reuse the scalar SSA machinery for memory by pretending
12//! memory is one scalar, and it is the right idea, so this does the same.
13//!
14//! The consequence is that the def-use chain over memory is maximally conservative. Every store
15//! kills every load, structurally. All of the precision comes from walking it, which is what
16//! [`Walk::clobber`] does.
17//!
18//! [`build`] is the construction: place a memory parameter at every join the memory versions
19//! reach, which is the same iterated dominance frontier that SSA construction uses, and thread
20//! the operand through every instruction that touches memory. A memory phi is an ordinary block
21//! parameter, so nothing here is a side table and the CFG updates that keep memory SSA in step
22//! with the blocks are the ones every other value already needed.
23//!
24//! # The walk
25//!
26//! [`Walk::clobber`] is GCC's `walk_non_aliased_vuses` at `gcc/tree-ssa-alias.cc:3915`. Given the
27//! version of memory a load reads, it walks back through the defs, asks the alias analysis at each
28//! one whether that def could have written what the load reads, and stops at the first one that
29//! could. Two parts of GCC's interface are worth copying and both are here.
30//!
31//! **The budget.** `sccvn-max-alias-queries-per-access`, default 1000 at `gcc/params.opt:1020`,
32//! and it is [`MAX_ALIAS_QUERIES_PER_ACCESS`] here under the same name, because a user who knows
33//! to raise GCC's should not have to learn a second one. The walk is worst case quadratic: every
34//! load can walk back through every store and each step is an alias query, so a function with a
35//! thousand of each and no disambiguation is a million queries per pass that uses it, and there
36//! are four such passes. Exceeding the budget gives [`Clobber::Unknown`], which is not an answer
37//! and is not a no.
38//!
39//! **`translate`.** When the walk reaches a def it cannot see past, the caller may adjust the
40//! reference and carry on, which is [`Step::Retry`]. This is what lets value numbering follow a
41//! load through a `memcpy` by rewriting the reference to the copy's source, and section 9.2 says
42//! it is the mechanism behind a surprising fraction of GCC's memory optimization. Without it the
43//! walk is a stopping condition. With it, it is a way to rewrite the question.
44//!
45//! A rewrite is counted, as [`Counts::rewritten`], for the same reason the steps and the budget
46//! exhaustions are: it is the one thing in the walk that starts the walk again, so it is where the
47//! work goes when the work goes somewhere unexpected, and it is what says whether the callback is
48//! reaching anything at all on a build rather than only on the build somebody last looked at.
49//!
50//! # Five answers, not two
51//!
52//! [`Clobber`] has five variants and the shape of it is deliberate. Section 9.6 names two ways
53//! this goes wrong and the type is what rules both out.
54//!
55//! The first is a caller treating a budget exhaustion as a no. There is no `Option` anywhere in
56//! the return and there is no default arm to fall into, so [`Clobber::Unknown`] has to be handled
57//! by name.
58//!
59//! The second is partial overlap. A four byte store followed by a one byte load at offset one:
60//! the load sees the store, but it cannot be replaced by the stored value, because the byte it
61//! wants is somewhere inside that value and getting it out is a shift and a truncate. So a
62//! clobber that wrote exactly the bytes of the reference is [`Clobber::Exact`], one that wrote
63//! some of them is [`Clobber::Partial`], and one that may have written them is
64//! [`Clobber::Maybe`]. Section 9.5 says getting this down to two answers is a class of
65//! miscompilation.
66//!
67//! # What is conservative on purpose
68//!
69//! Every atomic and every fence is a full memory def and a full memory use. Section 9.5 says this
70//! is correct and it is what M4 should do, and that doing better means modelling the memory model
71//! rather than the memory, which is post-1.0. The failure mode it names is treating a relaxed
72//! atomic load as an ordinary load because it orders nothing: it orders nothing and it is still a
73//! load, and hoisting it out of a loop changes an observable. Atomics are never moved.
74//!
75//! `volatile` is checked before anything else and is never walked past. Alias analysis says
76//! nothing about how many times an access happens and `volatile` constrains that too, so it is a
77//! separate bit rather than a strong alias fact.
78//!
79//! # The cache
80//!
81//! There is not one. Section 9.3 is explicit: build the uncached walk, instrument how many alias
82//! queries a `-O2` compilation makes, and add caching only if that number is a measurable
83//! fraction of compile time. GCC has run without it for twenty years and LLVM's caching walker is
84//! a large part of its MemorySSA complexity and a known source of invalidation bugs. The
85//! instrumentation is the M4 deliverable and it is [`Counts`]. The number that decides it is the
86//! fraction of walks that end by exhausting the budget rather than by finding a clobber: above
87//! one percent and the budget is too small or the alias analysis is too weak, and both of those
88//! are better fixed than cached around.
89
90use std::collections::{HashMap, HashSet};
91use std::hash::{BuildHasherDefault, Hasher};
92
93use rucc_ir::{Block, BlockCall, Def, Flags, Func, Inst, InstData, MemOrder, Opcode, Type, Value};
94
95use crate::alias::{Access, Alias, Answer, Options};
96use crate::cfg::Cfg;
97use crate::dom::Dominators;
98use crate::outside::Outside;
99
100/// How many alias queries one walk may make before it gives up.
101///
102/// GCC's `sccvn-max-alias-queries-per-access`, default 1000 at `gcc/params.opt:1020`, under the
103/// same name on purpose. Exceeding it gives [`Clobber::Unknown`] rather than a wrong answer.
104pub const MAX_ALIAS_QUERIES_PER_ACCESS: u32 = 1000;
105
106/// What the walk found.
107///
108/// Five variants, and section 9.6 is why. Three of them are a clobber and they differ in how much
109/// of the reference the clobber covers, because a caller that cannot tell `Exact` from `Partial`
110/// replaces a one byte load with the wrong byte of a four byte store. The other two are the ways
111/// a walk ends without one, and `Unknown` is not a no.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum Clobber {
114    /// This instruction wrote exactly the bytes the reference covers.
115    ///
116    /// The only answer redundant load elimination may act on by taking the stored value, and
117    /// even then only after checking the two types are the same width.
118    Exact(Inst),
119    /// This instruction wrote some of the reference, or wrote all of it and more.
120    ///
121    /// The load sees it, and what it sees cannot be had without taking part of what was stored
122    /// or combining it with something else, which is document 16's decision rather than this
123    /// one's.
124    Partial(Inst),
125    /// This instruction may have written the reference, and there is no telling how much.
126    Maybe(Inst),
127    /// Nothing in this function wrote it. The walk reached the start of the chain.
128    NoClobber,
129    /// The walk ran out of budget, or the paths into a join disagreed. Nothing is known.
130    Unknown,
131}
132
133impl Clobber {
134    /// The instruction, for the three answers that name one.
135    #[must_use]
136    pub const fn inst(self) -> Option<Inst> {
137        match self {
138            Self::Exact(inst) | Self::Partial(inst) | Self::Maybe(inst) => Some(inst),
139            Self::NoClobber | Self::Unknown => None,
140        }
141    }
142}
143
144/// What a caller does when the walk reaches a def it cannot see past.
145///
146/// GCC's `translate` callback, section 9.2. A caller with no rewrite to offer says [`Step::Stop`]
147/// and gets the clobber. One that can see through the def rewrites the reference and the walk
148/// carries on with the new one.
149#[derive(Clone, Copy, Debug, PartialEq, Eq)]
150pub enum Step {
151    /// Stop here. This is the answer.
152    Stop,
153    /// Carry on past this def, asking about this reference instead.
154    Retry(Access),
155}
156
157/// What the walks have cost, which section 9.7 asks for as its own counter.
158///
159/// The walk is charged to whichever pass made it, so `-ftime-report` shows it under GVN and PRE
160/// and not under memory SSA. That is misleading, and the fix section 9.7 asks for is to report
161/// the step count separately from the wall time, because it is the thing to look at when a
162/// pathological input turns up.
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
164pub struct Counts {
165    walks: u64,
166    steps: u64,
167    exhausted: u64,
168    rewritten: u64,
169}
170
171impl Counts {
172    /// How many walks were made.
173    #[must_use]
174    pub const fn walks(&self) -> u64 {
175        self.walks
176    }
177
178    /// How many defs those walks looked at, which is one alias query each.
179    #[must_use]
180    pub const fn steps(&self) -> u64 {
181        self.steps
182    }
183
184    /// How many walks ended by running out of budget.
185    ///
186    /// This is the number section 9.3 says decides whether the cache gets built. Above one
187    /// percent of walks and the budget is too small or the alias analysis is too weak.
188    #[must_use]
189    pub const fn exhausted(&self) -> u64 {
190        self.exhausted
191    }
192
193    /// How many times a caller rewrote the reference and the walk carried on with the new one.
194    ///
195    /// A rewrite starts a walk of its own, so this is both how much work the `translate` callback
196    /// is asking for and how much it is getting, and it is the counter that says whether the
197    /// callback is doing anything at all on a given build.
198    #[must_use]
199    pub const fn rewritten(&self) -> u64 {
200        self.rewritten
201    }
202}
203
204/// Puts a function on the memory chain, and says whether it did.
205///
206/// Construction is the same iterated dominance frontier SSA construction uses, over one variable:
207/// the blocks that write memory are the definitions, the joins their versions reach get a memory
208/// parameter, and a walk of the dominator tree threads the operand through every instruction that
209/// touches memory. Linear with a dominance frontier factor, per section 9.7.
210///
211/// It gives back `false` and changes nothing for a function that has no memory operations at all,
212/// for a declaration, and for one that is already on the chain. The first of those is the reason
213/// the answer is a `bool` rather than nothing: a function with no memory in it must not get a
214/// `mem_entry`, because a chain that starts and reaches nothing is a chain the verifier turns
215/// down and a reader would have to interpret.
216pub fn build(func: &mut Func) -> bool {
217    let Some(entry) = func.entry() else {
218        return false;
219    };
220    let cfg = Cfg::new(func);
221    let doms = Dominators::new(&cfg);
222
223    // Where the writes are, which is where the versions of memory are defined.
224    let mut defs = vec![entry];
225    let mut any = false;
226    for block in func.blocks() {
227        // A block nothing reaches is one the verifier turns down on its own, and it is not on
228        // the dominator tree either, so threading would leave it off the chain and the chain
229        // would then be neither all of the function nor none of it. Running the cleanup that
230        // deletes it first is the caller's job.
231        if !cfg.reaches(block) {
232            return false;
233        }
234        let mut writes = false;
235        for inst in func.insts(block) {
236            if func.carries_mem(inst) {
237                return false;
238            }
239            let opcode = func[inst].opcode;
240            any |= opcode.touches_memory();
241            writes |= opcode.writes_memory();
242        }
243        if writes && block != entry {
244            defs.push(block);
245        }
246    }
247    // An entry block with nothing in it has no terminator either, so this is not a function the
248    // verifier would have let through and there is nothing sensible to build over it.
249    let Some(first) = func.insts(entry).next() else {
250        return false;
251    };
252    if !any {
253        return false;
254    }
255
256    let joins = iterated_frontier(&cfg, &doms, &defs);
257    let mut params = HashMap::new();
258    for block in func.blocks().collect::<Vec<_>>() {
259        if joins.contains(&block) {
260            params.insert(block, func.append_param(block, Type::MEM));
261        }
262    }
263
264    let start = start_of_chain(func, first);
265    let ends = thread(func, &doms, &params, entry, start);
266    pass_it_on(func, &params, &ends);
267    true
268}
269
270/// Takes the chain back off, and says whether it did.
271///
272/// The inverse of [`build`], and it is here because the back end has never seen memory SSA and is
273/// not going to: `rucc_codegen::capability` says outright that the chain comes off before it runs.
274/// Nothing was taking it off, so until this existed the only way to use the chain was to not use
275/// it. A pass that wants the walk builds the chain, does its work and strips it, which is a linear
276/// walk each way on top of whatever the pass itself costs.
277///
278/// Keeping the chain across passes instead would be cheaper and is a much bigger claim to make,
279/// since every edit to the control flow graph in the optimizer would have to keep the memory
280/// parameters in step with the blocks. That is worth wanting later and is not what this is.
281///
282/// Three things come off, in the order they have to. Every instruction on the chain loses its
283/// incoming version and its outgoing one, which is [`Func::without_mem`], and what it produced
284/// otherwise is forwarded to what the bare one produces. Every memory parameter comes off the
285/// block that has it and the matching argument comes off every branch to that block. The
286/// `mem_entry` at the top goes last, because until the rest is off it is a definition with
287/// readers.
288///
289/// It gives back `false` and changes nothing for a function that is not on the chain.
290pub fn strip(func: &mut Func) -> bool {
291    let mut forward: Vec<(Value, Value)> = Vec::new();
292    let mut gone: Vec<Inst> = Vec::new();
293    let mut entry = None;
294    for block in func.blocks().collect::<Vec<Block>>() {
295        for inst in func.insts(block).collect::<Vec<Inst>>() {
296            if func[inst].opcode == Opcode::MemEntry {
297                entry = Some(inst);
298                continue;
299            }
300            if !func.carries_mem(inst) {
301                continue;
302            }
303            let bare = func.without_mem(inst);
304            func.insert_before(bare, inst);
305            // The results the bare one kept are at the same positions, and the version of memory
306            // the old one produced is past the end of them, so zipping forwards exactly the ones
307            // that have somewhere to go.
308            for (old, new) in func[inst].results().zip(func[bare].results()) {
309                forward.push((old, new));
310            }
311            gone.push(inst);
312        }
313    }
314    if entry.is_none() && gone.is_empty() {
315        return false;
316    }
317    for inst in gone {
318        func.remove_inst(inst);
319    }
320    let forward: HashMap<Value, Value> = forward.into_iter().collect();
321    if !forward.is_empty() {
322        substitute(func, &forward);
323    }
324    drop_params(func);
325    if let Some(inst) = entry {
326        func.remove_inst(inst);
327    }
328    true
329}
330
331/// Takes the memory parameter off every block that has one, and the argument off every branch to
332/// it.
333///
334/// A parameter that goes has to take the argument in the same position out of every branch, and
335/// only the caller knows which branches there are, which is why [`Func::retain_params`] does not
336/// do it. The position is worked out before anything is removed, because renumbering the
337/// parameters and rewriting the arguments cannot both go first.
338fn drop_params(func: &mut Func) {
339    let mut at: HashMap<Block, Vec<usize>> = HashMap::new();
340    let mut going: HashSet<Value> = HashSet::new();
341    for block in func.blocks().collect::<Vec<Block>>() {
342        let mut keep = Vec::new();
343        for (index, &param) in func[block].params.iter().enumerate() {
344            if func[param].ty.is_mem() {
345                going.insert(param);
346            } else {
347                keep.push(index);
348            }
349        }
350        if keep.len() != func[block].params.len() {
351            at.insert(block, keep);
352        }
353    }
354    if at.is_empty() {
355        return;
356    }
357    for block in func.blocks().collect::<Vec<Block>>() {
358        let Some(terminator) = func.terminator(block) else {
359            continue;
360        };
361        for target in func.target_list(terminator).iter() {
362            let call = func[target];
363            let Some(keep) = at.get(&call.block) else {
364                continue;
365            };
366            let args: Vec<Value> = keep.iter().map(|&index| func[call.args][index]).collect();
367            let args = func.push_values(&args);
368            func.set_block_call(target, BlockCall { args, ..call });
369        }
370    }
371    for block in at.keys().copied().collect::<Vec<Block>>() {
372        func.retain_params(block, |param| !going.contains(&param));
373    }
374}
375
376/// The `mem_entry` above that instruction, which is where every chain starts.
377///
378/// It goes at the very top of the entry block, and the verifier insists on that: a start to the
379/// chain anywhere else would have instructions above it that are on the chain and reach a version
380/// of memory defined below them.
381fn start_of_chain(func: &mut Func, first: Inst) -> Value {
382    let span = func.span(first);
383    let inst = func.create_inst(InstData::new(Opcode::MemEntry), &[Type::MEM], span);
384    func.insert_before(inst, first);
385    func[inst].results().next().expect("mem_entry produces one value")
386}
387
388/// Threads the operand through every instruction that touches memory, and says which version of
389/// memory each block ends with.
390///
391/// The walk is over the dominator tree rather than the CFG, because the version reaching the top
392/// of a block is the one its immediate dominator ended with unless the block has a parameter of
393/// its own. That is the ordinary SSA renaming and memory is an ordinary variable here.
394fn thread(
395    func: &mut Func,
396    doms: &Dominators,
397    params: &HashMap<Block, Value>,
398    entry: Block,
399    start: Value,
400) -> HashMap<Block, Value> {
401    // An instruction cannot grow a result, so threading one makes a new instruction beside it and
402    // the old one goes away. What the old one produced is forwarded to what the new one produces,
403    // at the same positions, in one substitution at the end rather than as each is replaced,
404    // because an instruction threaded early can be an operand of one threaded late.
405    let mut forward: Vec<(Value, Value)> = Vec::new();
406    let mut ends = HashMap::new();
407    let mut stack = vec![(entry, start)];
408    while let Some((block, incoming)) = stack.pop() {
409        let mut current = params.get(&block).copied().unwrap_or(incoming);
410        for inst in func.insts(block).collect::<Vec<_>>() {
411            if !func[inst].opcode.touches_memory() {
412                continue;
413            }
414            let fresh = func.with_mem(inst, current);
415            func.insert_before(fresh, inst);
416            for (old, new) in func[inst].results().zip(func[fresh].results()) {
417                forward.push((old, new));
418            }
419            func.remove_inst(inst);
420            if let Some(next) = func.mem_out(fresh) {
421                current = next;
422            }
423        }
424        ends.insert(block, current);
425        stack.extend(doms.children(block).map(|child| (child, current)));
426    }
427
428    let forward: HashMap<Value, Value> = forward.into_iter().collect();
429    if !forward.is_empty() {
430        substitute(func, &forward);
431    }
432    ends
433}
434
435/// Replaces every use of what a threaded instruction produced with what its replacement produces.
436fn substitute(func: &mut Func, forward: &HashMap<Value, Value>) {
437    let with = |value: Value| forward.get(&value).copied().unwrap_or(value);
438    for block in func.blocks().collect::<Vec<_>>() {
439        for inst in func.insts(block).collect::<Vec<_>>() {
440            let args = func[inst].args;
441            func.rewrite(args, with);
442            for call in func.successors(inst).collect::<Vec<_>>() {
443                func.rewrite(call.args, with);
444            }
445        }
446    }
447    // And the names, which go where the readers went, for the same reason `crate::uses::substitute`
448    // moves them. A call is an instruction that carries memory and `int x = f();` names what one
449    // produced, so a value rewritten here can be a value a declaration is spelled by.
450    let mut moving: Vec<Value> = forward.keys().copied().collect();
451    moving.sort_unstable();
452    for from in moving {
453        func.rename_value(from, with(from));
454    }
455}
456
457/// Passes the version of memory each block ends with to the joins it branches to.
458fn pass_it_on(func: &mut Func, params: &HashMap<Block, Value>, ends: &HashMap<Block, Value>) {
459    for block in func.blocks().collect::<Vec<_>>() {
460        let Some(terminator) = func.terminator(block) else {
461            continue;
462        };
463        let Some(&value) = ends.get(&block) else {
464            continue;
465        };
466        for at in func.target_list(terminator).iter() {
467            let call = func[at];
468            if !params.contains_key(&call.block) {
469                continue;
470            }
471            // The memory parameter was appended last, so the argument goes last too, which is
472            // the same rule the operand follows and for the same reason.
473            let args = func.append_arg(call.args, value);
474            func.set_block_call(at, BlockCall { args, ..call });
475        }
476    }
477}
478
479/// The blocks that need a memory parameter, which is the iterated dominance frontier of the
480/// blocks that define a version of memory.
481fn iterated_frontier(cfg: &Cfg, doms: &Dominators, defs: &[Block]) -> HashSet<Block> {
482    let frontier = frontiers(cfg, doms);
483    let mut placed = HashSet::new();
484    let mut seen: HashSet<Block> = defs.iter().copied().collect();
485    let mut work: Vec<Block> = defs.to_vec();
486    while let Some(block) = work.pop() {
487        let Some(targets) = frontier.get(&block) else {
488            continue;
489        };
490        for &target in targets {
491            if placed.insert(target) && seen.insert(target) {
492                work.push(target);
493            }
494        }
495    }
496    placed
497}
498
499/// The dominance frontier of every block, by Cytron's walk from each join up to its immediate
500/// dominator.
501fn frontiers(cfg: &Cfg, doms: &Dominators) -> HashMap<Block, Vec<Block>> {
502    let mut frontier: HashMap<Block, Vec<Block>> = HashMap::new();
503    for block in cfg.reverse_postorder() {
504        let preds = cfg.predecessors(block);
505        if preds.len() < 2 {
506            continue;
507        }
508        let Some(top) = doms.immediate_dominator(block) else {
509            continue;
510        };
511        for &pred in preds {
512            let mut runner = pred;
513            while runner != top {
514                let at = frontier.entry(runner).or_default();
515                if !at.contains(&block) {
516                    at.push(block);
517                }
518                let Some(next) = doms.immediate_dominator(runner) else {
519                    break;
520                };
521                runner = next;
522            }
523        }
524    }
525    frontier
526}
527
528/// The walk back through the memory chain.
529///
530/// It borrows the function rather than owning anything, and it holds the alias analysis because
531/// every step is a query and the escape analysis inside it is worth building once.
532#[derive(Debug)]
533pub struct Walk<'a> {
534    func: &'a Func,
535    cfg: Cfg,
536    alias: Alias<'a>,
537    limit: u32,
538    counts: Counts,
539    /// The versions the walk under way has been to. Emptied for each walk rather than made again,
540    /// so the room it grew to is used again.
541    seen: Seen,
542}
543
544/// A set of versions of memory, hashed by multiplying the index rather than by SipHash.
545///
546/// A walk starts one of these for every load it is asked about and puts every version it passes
547/// into it, and on jtckdint's `test.c` hashing them was more than a percent of the build. A value
548/// is a small dense number that nobody outside picks, so there is nothing for SipHash to defend
549/// against.
550type Seen = HashSet<Value, BuildHasherDefault<Spread>>;
551
552/// Spreads a value's index over the whole word, the high bits being the ones the table reads.
553#[derive(Debug, Default)]
554struct Spread(u64);
555
556impl Hasher for Spread {
557    fn finish(&self) -> u64 {
558        self.0
559    }
560
561    fn write(&mut self, bytes: &[u8]) {
562        for &byte in bytes {
563            self.write_u64(u64::from(byte));
564        }
565    }
566
567    fn write_u32(&mut self, word: u32) {
568        self.write_u64(u64::from(word));
569    }
570
571    fn write_u64(&mut self, word: u64) {
572        self.0 = (self.0.rotate_left(5) ^ word).wrapping_mul(0x9e37_79b9_7f4a_7c15);
573    }
574}
575
576impl<'a> Walk<'a> {
577    /// A walk over this function, with GCC's budget.
578    #[must_use]
579    pub fn new(func: &'a Func, outside: &'a Outside) -> Self {
580        Self::with(func, outside, Options::default(), MAX_ALIAS_QUERIES_PER_ACCESS)
581    }
582
583    /// The same, with the alias options the command line left and a budget of your own.
584    #[must_use]
585    pub fn with(func: &'a Func, outside: &'a Outside, options: Options, limit: u32) -> Self {
586        Self {
587            func,
588            cfg: Cfg::new(func),
589            alias: Alias::with(func, outside, options),
590            limit,
591            counts: Counts::default(),
592            seen: Seen::default(),
593        }
594    }
595
596    /// What the walks have cost so far.
597    #[must_use]
598    pub const fn counts(&self) -> &Counts {
599        &self.counts
600    }
601
602    /// The same walk, with what the module's functions were worked out to do to memory.
603    ///
604    /// Handed straight to the oracle underneath, where [`Alias::knowing`] says what it is for.
605    #[must_use]
606    pub fn knowing(mut self, summaries: &'a crate::modref::Summaries) -> Self {
607        self.alias = self.alias.knowing(summaries);
608        self
609    }
610
611    /// The alias analysis underneath, whose own counters say which layer answered.
612    #[must_use]
613    pub const fn alias(&self) -> &Alias<'a> {
614        &self.alias
615    }
616
617    /// The store this load sees.
618    ///
619    /// [`Clobber::Unknown`] for an instruction that reads nothing, for one that is not on the
620    /// chain, and for a walk that ran out of budget, because all three mean the same thing to a
621    /// caller, which is that nothing was established.
622    pub fn clobber(&mut self, load: Inst) -> Clobber {
623        self.clobber_with(load, &mut |_, _| Step::Stop)
624    }
625
626    /// The same, with the chance to rewrite the reference at every def the walk cannot see past.
627    ///
628    /// Section 9.2's `translate`. The callback is handed the reference as it stands and the def
629    /// in the way, and answers [`Step::Stop`] to take the clobber or [`Step::Retry`] to carry on
630    /// past it asking about something else. Following a load through a `memcpy` by rewriting the
631    /// reference to the copy's source is the case worth having it for, since that is what a
632    /// struct assignment lowers to.
633    ///
634    /// Section 9.6 calls a `translate` that rewrites the reference wrongly the subtlest bug in
635    /// the document and essentially untestable by unit test, so the defence is differential
636    /// execution per document 41 rather than anything here.
637    pub fn clobber_with(
638        &mut self,
639        load: Inst,
640        translate: &mut dyn FnMut(&Access, Inst) -> Step,
641    ) -> Clobber {
642        let (Some(reference), Some(version)) = (self.alias.reads(load), self.func.mem_in(load))
643        else {
644            return Clobber::Unknown;
645        };
646        self.counts.walks += 1;
647        let mut budget = self.limit;
648        let mut seen = std::mem::take(&mut self.seen);
649        seen.clear();
650        let answer = self.back(reference, version, &mut budget, &mut seen, translate);
651        self.seen = seen;
652        // Nothing new on any path back is nothing that wrote it, which is the same answer as
653        // reaching the start of the chain and is only reachable through a cycle of parameters.
654        answer.unwrap_or(Clobber::NoClobber)
655    }
656
657    /// One version of memory, and everything that reaches it.
658    ///
659    /// `None` means this version has already been accounted for on another path, which is the
660    /// neutral answer: it is how a loop is cut, since the back edge of a loop whose body writes
661    /// nothing relevant leads back to the parameter the walk started from.
662    fn back(
663        &mut self,
664        reference: Access,
665        version: Value,
666        budget: &mut u32,
667        seen: &mut Seen,
668        translate: &mut dyn FnMut(&Access, Inst) -> Step,
669    ) -> Option<Clobber> {
670        if !seen.insert(version) {
671            return None;
672        }
673        match self.func[version].def {
674            // A memory phi. The answer is the same down every path into the block or it is not
675            // an answer, which is conservative and is what keeps a caller from acting on a store
676            // that only one predecessor made.
677            Def::Param { block, index } => {
678                let mut answer = None;
679                let func = self.func;
680                // By place rather than by copying the list out, since the walk below needs the
681                // walker and a phi is the step it takes most often.
682                for at in 0..self.cfg.predecessors(block).len() {
683                    let pred = self.cfg.predecessors(block)[at];
684                    let Some(terminator) = func.terminator(pred) else {
685                        continue;
686                    };
687                    for call in func.successors(terminator) {
688                        if call.block != block {
689                            continue;
690                        }
691                        let Some(&incoming) = self.func[call.args].get(index as usize) else {
692                            continue;
693                        };
694                        let one = self.back(reference, incoming, budget, seen, translate);
695                        answer = combine(answer, one);
696                        if answer == Some(Clobber::Unknown) {
697                            return answer;
698                        }
699                    }
700                }
701                answer
702            }
703            Def::Result { inst, .. } => {
704                if self.func[inst].opcode == Opcode::MemEntry {
705                    return Some(Clobber::NoClobber);
706                }
707                if *budget == 0 {
708                    self.counts.exhausted += 1;
709                    return Some(Clobber::Unknown);
710                }
711                *budget -= 1;
712                self.counts.steps += 1;
713                let past = match self.wrote(&reference, inst) {
714                    None => reference,
715                    Some(answer) => match translate(&reference, inst) {
716                        Step::Stop => return Some(answer),
717                        // A rewritten question is a walk of its own and gets a visited set of its
718                        // own. The set is there to stop a cycle being walked twice, and what makes
719                        // the second time round pointless is that the answer at a version is an
720                        // answer about one reference: a version this walk has already been to was
721                        // visited asking something else, and what it said then says nothing about
722                        // what is being asked now. Carrying the set across the rewrite loses an
723                        // answer rather than repeating one, because a version declined as already
724                        // seen contributes nothing to the join above it, and a join whose two paths
725                        // disagree would come back holding whichever of them was walked first
726                        // rather than `Unknown`.
727                        Step::Retry(next) => {
728                            self.counts.rewritten += 1;
729                            let before = self.func.mem_in(inst)?;
730                            let mut fresh = Seen::default();
731                            return self.back(next, before, budget, &mut fresh, translate);
732                        }
733                    },
734                };
735                let next = self.func.mem_in(inst)?;
736                self.back(past, next, budget, seen, translate)
737            }
738        }
739    }
740
741    /// Whether this def wrote the reference, and how much of it.
742    ///
743    /// `None` is the answer that lets the walk carry on, and it is only given where the alias
744    /// analysis said the two cannot touch the same byte.
745    fn wrote(&mut self, reference: &Access, inst: Inst) -> Option<Clobber> {
746        // Section 9.5, and it is first. Alias analysis says nothing about how many times an
747        // access happens and `volatile` constrains that too, so this is a separate bit rather
748        // than a strong alias fact, and it is checked before the analysis is asked anything.
749        if reference.volatile || self.func[inst].flags.contains(Flags::VOLATILE) {
750            return Some(Clobber::Maybe(inst));
751        }
752        // Every atomic and every fence is a full def and a full use. Pessimistic for lock-free
753        // code and correct, and section 9.5 says doing better means modelling the memory model
754        // rather than the memory, which is post-1.0.
755        if self.ordered(inst) {
756            return Some(Clobber::Maybe(inst));
757        }
758        if let Some(write) = self.alias.writes(inst) {
759            return match self.alias.query(reference, &write) {
760                Answer::No(_) => None,
761                Answer::May => Some(self.extent(reference, &write, inst)),
762            };
763        }
764        // A call, or anything else that writes memory without an access saying what. What a call
765        // touches is its attributes and the escape analysis, which is section 8.4's, and without
766        // those the honest answer is that it wrote everything.
767        match self.alias.clobbered_by(reference, inst) {
768            Answer::No(_) => None,
769            Answer::May => Some(Clobber::Maybe(inst)),
770        }
771    }
772
773    /// How much of the reference a write that may touch it covered.
774    ///
775    /// Two accesses to the same origin with both offsets and both sizes known are two runs of
776    /// bytes at known places, and comparing them is what tells `Exact` from `Partial`. Anything
777    /// less is `Maybe`, since a `May` from the alias analysis is not a proof that anything was
778    /// written at all.
779    ///
780    /// `Exact` is the same bytes and not merely a superset of them. A four byte store and the
781    /// one byte load at offset one inside it is `Partial`, because the byte the load wants is
782    /// somewhere in the value the store wrote and getting it out is a shift and a truncate that
783    /// document 16 decides on rather than this. Two runs that are the same bytes can still be
784    /// two different types, and checking that is the caller's as well.
785    fn extent(&self, reference: &Access, write: &Access, inst: Inst) -> Clobber {
786        if reference.origin != write.origin {
787            return Clobber::Maybe(inst);
788        }
789        let (Some(want), Some(wrote)) = (reference.range(), write.range()) else {
790            return Clobber::Maybe(inst);
791        };
792        if want == wrote {
793            Clobber::Exact(inst)
794        } else if wrote.0 < want.1 && want.0 < wrote.1 {
795            Clobber::Partial(inst)
796        } else {
797            // No overlap at all, which the alias analysis should have said no to. Saying `Maybe`
798            // rather than walking past is the conservative reading of a disagreement.
799            Clobber::Maybe(inst)
800        }
801    }
802
803    /// Whether the instruction orders memory, which is every atomic and every fence.
804    fn ordered(&self, inst: Inst) -> bool {
805        use rucc_ir::Extra;
806        let order = match self.func[inst].extra {
807            Extra::Mem(at) => self.func[at].order,
808            Extra::Rmw(_, at) => self.func[at].order,
809            Extra::Order(order) => order,
810            _ => return false,
811        };
812        order != MemOrder::NotAtomic
813    }
814}
815
816/// Two answers from two paths into a join.
817///
818/// The same answer on both is the answer. Nothing on one path is whatever the other said, which
819/// is how a cycle contributes nothing. Anything else is a disagreement, and a disagreement is
820/// `Unknown` rather than the weaker of the two, because there is no order on these that a caller
821/// could act on.
822fn combine(a: Option<Clobber>, b: Option<Clobber>) -> Option<Clobber> {
823    match (a, b) {
824        (None, other) | (other, None) => other,
825        (Some(one), Some(other)) if one == other => Some(one),
826        _ => Some(Clobber::Unknown),
827    }
828}
829
830#[cfg(test)]
831mod tests {
832    use rucc_base::Interner;
833    use rucc_ir::{Builder, MemInfo, Module, Restrict, Signature, parse, verify_func};
834
835    use super::*;
836
837    /// A module and a function built from the text, which is how these are written.
838    fn read(text: &str) -> (Module, Interner) {
839        let mut names = Interner::new();
840        let module = parse(text, &mut names).expect("the text parses");
841        (module, names)
842    }
843
844    const HEADER: &str = "\
845; ModuleID = 'mem.c'
846; format 0
847target triple = \"x86_64-unknown-linux-gnu\"
848target datalayout = \"e-p:64:64-i64:64-f80:128-S128\"
849";
850
851    fn wrap(signature: &str, body: &str) -> String {
852        format!("{HEADER}\nfunc @f{signature}, linkage(external) {{\n{body}}}\n")
853    }
854
855    /// Builds memory SSA over the function and insists the result verifies, which is where most
856    /// of the strength of these tests is: the rules in the verifier are the specification of the
857    /// chain and construction has to satisfy all of them.
858    fn built(text: &str) -> (Module, bool) {
859        let (mut module, names) = read(text);
860        let id = module.funcs().next().expect("one function");
861        let changed = build(&mut module[id]);
862        if let Err(errors) = verify_func(&module, &module[id], &names) {
863            panic!("{errors:#?}");
864        }
865        (module, changed)
866    }
867
868    fn one(module: &Module) -> &Func {
869        &module[module.funcs().next().expect("one function")]
870    }
871
872    /// The instruction with that opcode, counting from the top of the function.
873    fn nth(func: &Func, opcode: Opcode, want: usize) -> Inst {
874        func.blocks()
875            .flat_map(|block| func.insts(block).collect::<Vec<_>>())
876            .filter(|&inst| func[inst].opcode == opcode)
877            .nth(want)
878            .expect("that many of them")
879    }
880
881    #[test]
882    fn a_function_with_no_memory_in_it_gets_no_chain() {
883        let text = wrap(
884            "(i32) -> i32",
885            "block0(%0: i32):
886    %1 = add %0, %0
887    return %1
888",
889        );
890        let (module, changed) = built(&text);
891        assert!(!changed);
892        assert_eq!(one(&module).blocks().count(), 1);
893    }
894
895    #[test]
896    fn a_straight_line_is_threaded_in_order() {
897        let text = wrap(
898            "(ptr) -> i32",
899            "block0(%0: ptr):
900    %1 = iconst.i32 7
901    store %1 -> %0, align 4
902    %2 = load.i32 %0, align 4
903    return %2
904",
905        );
906        let (module, changed) = built(&text);
907        assert!(changed);
908        let func = one(&module);
909        let start = nth(func, Opcode::MemEntry, 0);
910        let store = nth(func, Opcode::Store, 0);
911        let load = nth(func, Opcode::Load, 0);
912        assert_eq!(func.mem_in(store), func.mem_out(start));
913        assert_eq!(func.mem_in(load), func.mem_out(store));
914        assert_eq!(func.mem_out(load), None);
915    }
916
917    #[test]
918    fn a_join_gets_a_memory_parameter_and_every_branch_passes_one() {
919        let text = wrap(
920            "(ptr, i1) -> i32",
921            "block0(%0: ptr, %1: i1):
922    br_if %1, block1, block2
923
924block1:
925    %2 = iconst.i32 7
926    store %2 -> %0, align 4
927    jump block3
928
929block2:
930    jump block3
931
932block3:
933    %3 = load.i32 %0, align 4
934    return %3
935",
936        );
937        let (module, _) = built(&text);
938        let func = one(&module);
939        let join = func.blocks().nth(3).expect("four blocks");
940        assert_eq!(func[join].params.len(), 1);
941        let param = func[join].params[0];
942        assert!(func[param].ty.is_mem());
943        assert_eq!(func.mem_in(nth(func, Opcode::Load, 0)), Some(param));
944    }
945
946    #[test]
947    fn a_block_that_only_reads_needs_no_parameter() {
948        let text = wrap(
949            "(ptr, i1) -> i32",
950            "block0(%0: ptr, %1: i1):
951    br_if %1, block1, block2
952
953block1:
954    %2 = load.i32 %0, align 4
955    jump block3
956
957block2:
958    jump block3
959
960block3:
961    %3 = load.i32 %0, align 4
962    return %3
963",
964        );
965        let (module, _) = built(&text);
966        let func = one(&module);
967        // One version of memory reaches the whole function, so no join needs a parameter and
968        // every load reads what `mem_entry` produced.
969        for block in func.blocks() {
970            assert!(func[block].params.iter().all(|&param| !func[param].ty.is_mem()));
971        }
972    }
973
974    #[test]
975    fn every_arm_of_a_switch_passes_its_own_version_along() {
976        let text = wrap(
977            "(ptr, i32) -> i32",
978            "block0(%0: ptr, %1: i32):
979    switch %1, block1, [0 => block2, 1 => block3]
980
981block1:
982    %2 = iconst.i32 1
983    store %2 -> %0, align 4
984    jump block4
985
986block2:
987    %3 = iconst.i32 2
988    store %3 -> %0, align 4
989    jump block4
990
991block3:
992    jump block4
993
994block4:
995    %4 = load.i32 %0, align 4
996    return %4
997",
998        );
999        let (module, _) = built(&text);
1000        let func = one(&module);
1001        let join = func.blocks().nth(4).expect("five blocks");
1002        let param = *func[join].params.last().expect("a parameter");
1003        assert!(func[param].ty.is_mem());
1004        // Each arm reaches the join with the version it ended on, and the two that wrote reach
1005        // it with the version their own store produced.
1006        for (arm, want) in [(1, Some(0)), (2, Some(1)), (3, None)] {
1007            let block = func.blocks().nth(arm).expect("that block");
1008            let jump = func.terminator(block).expect("a terminator");
1009            let call = func.successors(jump).next().expect("one target");
1010            let sent = *func[call.args].last().expect("an argument");
1011            let expect = match want {
1012                Some(store) => func.mem_out(nth(func, Opcode::Store, store)),
1013                None => func.mem_out(nth(func, Opcode::MemEntry, 0)),
1014            };
1015            assert_eq!(Some(sent), expect, "arm {arm} passed the wrong version");
1016        }
1017    }
1018
1019    #[test]
1020    fn a_function_with_a_block_nothing_reaches_is_left_alone() {
1021        let text = wrap(
1022            "(ptr) -> i32",
1023            "block0(%0: ptr):
1024    %1 = iconst.i32 7
1025    store %1 -> %0, align 4
1026    jump block2
1027
1028block1:
1029    %2 = iconst.i32 9
1030    store %2 -> %0, align 4
1031    jump block2
1032
1033block2:
1034    %3 = load.i32 %0, align 4
1035    return %3
1036",
1037        );
1038        // Block 1 has no predecessor. Half a function on the chain is worse than none of it, so
1039        // this declines rather than producing something the verifier would turn down.
1040        let (mut module, _) = read(&text);
1041        let id = module.funcs().next().expect("one function");
1042        assert!(!build(&mut module[id]));
1043        assert_eq!(module[id].blocks().filter(|&b| !module[id][b].params.is_empty()).count(), 1);
1044    }
1045
1046    /// The last load in the function, which is the one every walk here starts from.
1047    fn last_load(func: &Func) -> Inst {
1048        func.blocks()
1049            .flat_map(|block| func.insts(block).collect::<Vec<_>>())
1050            .filter(|&inst| func[inst].opcode == Opcode::Load)
1051            .last()
1052            .expect("a load")
1053    }
1054
1055    /// A load, a store and the walk between them, over a function written as text.
1056    fn walked(text: &str) -> (Clobber, Counts) {
1057        let (module, changed) = built(text);
1058        assert!(changed, "the function has memory in it");
1059        let func = one(&module);
1060        let outside = Outside::of(&module);
1061        let mut walk = Walk::new(func, &outside);
1062        let answer = walk.clobber(last_load(func));
1063        (answer, *walk.counts())
1064    }
1065
1066    #[test]
1067    fn a_load_sees_the_store_before_it() {
1068        let text = wrap(
1069            "(ptr) -> i32",
1070            "block0(%0: ptr):
1071    %1 = iconst.i32 7
1072    store %1 -> %0, align 4
1073    %2 = load.i32 %0, align 4
1074    return %2
1075",
1076        );
1077        let (answer, counts) = walked(&text);
1078        assert!(matches!(answer, Clobber::Exact(_)));
1079        assert_eq!(counts.walks(), 1);
1080        assert_eq!(counts.steps(), 1);
1081        assert_eq!(counts.exhausted(), 0);
1082    }
1083
1084    #[test]
1085    fn a_load_walks_past_a_store_to_another_object() {
1086        let text = wrap(
1087            "() -> i32",
1088            "block0:
1089    %0 = alloca, size 8, align 8
1090    %1 = alloca, size 8, align 8
1091    %2 = iconst.i32 7
1092    store %2 -> %0, align 4
1093    %3 = load.i32 %1, align 4
1094    return %3
1095",
1096        );
1097        let (answer, counts) = walked(&text);
1098        assert_eq!(answer, Clobber::NoClobber);
1099        // It looked at the store, said no, and reached the start of the chain.
1100        assert_eq!(counts.steps(), 1);
1101    }
1102
1103    #[test]
1104    fn a_load_of_one_byte_of_a_wider_store_is_partial() {
1105        let text = wrap(
1106            "() -> i8",
1107            "block0:
1108    %0 = alloca, size 8, align 8
1109    %1 = iconst.i32 7
1110    store %1 -> %0, align 4
1111    %2 = iconst.i64 1
1112    %3 = ptr_add %0, %2
1113    %4 = load.i8 %3, align 1
1114    return %4
1115",
1116        );
1117        let (answer, _) = walked(&text);
1118        assert!(matches!(answer, Clobber::Partial(_)), "{answer:?}");
1119    }
1120
1121    #[test]
1122    fn a_load_after_a_call_that_cannot_reach_it_walks_past_the_call() {
1123        let text = wrap(
1124            "() -> i32",
1125            "block0:
1126    %0 = alloca, size 8, align 8
1127    %1 = iconst.i32 7
1128    store %1 -> %0, align 4
1129    call @g() : ()
1130    %2 = load.i32 %0, align 4
1131    return %2
1132",
1133        );
1134        // The local's address never leaves the function, so the call cannot touch it and the
1135        // walk goes straight past to the store. That is the escape layer paying for itself.
1136        let (answer, _) = walked(&text);
1137        assert!(matches!(answer, Clobber::Exact(_)), "{answer:?}");
1138    }
1139
1140    #[test]
1141    fn a_load_after_a_call_that_could_have_the_address_sees_the_call() {
1142        let text = wrap(
1143            "(ptr) -> i32",
1144            "block0(%0: ptr):
1145    %1 = iconst.i32 7
1146    store %1 -> %0, align 4
1147    call @g() : ()
1148    %2 = load.i32 %0, align 4
1149    return %2
1150",
1151        );
1152        let (answer, _) = walked(&text);
1153        assert!(matches!(answer, Clobber::Maybe(_)), "{answer:?}");
1154    }
1155
1156    #[test]
1157    fn a_load_after_an_atomic_store_sees_it_whatever_it_wrote() {
1158        let text = wrap(
1159            "() -> i32",
1160            "block0:
1161    %0 = alloca, size 8, align 8
1162    %1 = alloca, size 8, align 8
1163    %2 = iconst.i32 7
1164    atomic_store %2 -> %0, align 4, release
1165    %3 = load.i32 %1, align 4
1166    return %3
1167",
1168        );
1169        // Two different objects, and it still stops: an atomic is a full def and a full use, per
1170        // section 9.5, and this is the test that says so rather than a comment.
1171        let (answer, _) = walked(&text);
1172        assert!(matches!(answer, Clobber::Maybe(_)), "{answer:?}");
1173    }
1174
1175    #[test]
1176    fn a_load_after_a_volatile_store_sees_it_whatever_it_wrote() {
1177        let text = wrap(
1178            "() -> i32",
1179            "block0:
1180    %0 = alloca, size 8, align 8
1181    %1 = alloca, size 8, align 8
1182    %2 = iconst.i32 7
1183    store.volatile %2 -> %0, align 4
1184    %3 = load.i32 %1, align 4
1185    return %3
1186",
1187        );
1188        let (answer, _) = walked(&text);
1189        assert!(matches!(answer, Clobber::Maybe(_)), "{answer:?}");
1190    }
1191
1192    #[test]
1193    fn paths_that_disagree_are_unknown_rather_than_the_weaker_of_the_two() {
1194        let text = wrap(
1195            "(i1) -> i32",
1196            "block0(%0: i1):
1197    %1 = alloca, size 8, align 8
1198    br_if %0, block1, block2
1199
1200block1:
1201    %2 = iconst.i32 7
1202    store %2 -> %1, align 4
1203    jump block3
1204
1205block2:
1206    jump block3
1207
1208block3:
1209    %3 = load.i32 %1, align 4
1210    return %3
1211",
1212        );
1213        let (answer, _) = walked(&text);
1214        assert_eq!(answer, Clobber::Unknown);
1215    }
1216
1217    #[test]
1218    fn a_loop_that_writes_nothing_relevant_walks_out_of_it() {
1219        let text = wrap(
1220            "(i32) -> i32",
1221            "block0(%0: i32):
1222    %1 = alloca, size 8, align 8
1223    %2 = alloca, size 8, align 8
1224    %3 = iconst.i32 7
1225    store %3 -> %1, align 4
1226    jump block1(%0)
1227
1228block1(%4: i32):
1229    %5 = iconst.i32 1
1230    %6 = sub %4, %5
1231    store %5 -> %2, align 4
1232    %7 = icmp sgt %6, %5
1233    br_if %7, block1(%6), block2
1234
1235block2:
1236    %8 = load.i32 %1, align 4
1237    return %8
1238",
1239        );
1240        // The store in the loop is to the other object, so the walk goes round the back edge,
1241        // meets the parameter it started from, contributes nothing, and takes the answer from
1242        // the path that leaves the loop.
1243        let (answer, counts) = walked(&text);
1244        assert!(matches!(answer, Clobber::Exact(_)), "{answer:?}");
1245        assert_eq!(counts.exhausted(), 0);
1246    }
1247
1248    #[test]
1249    fn a_budget_of_nothing_gives_unknown_and_says_so() {
1250        let text = wrap(
1251            "(ptr) -> i32",
1252            "block0(%0: ptr):
1253    %1 = iconst.i32 7
1254    store %1 -> %0, align 4
1255    %2 = load.i32 %0, align 4
1256    return %2
1257",
1258        );
1259        let (module, _) = built(&text);
1260        let func = one(&module);
1261        let load = nth(func, Opcode::Load, 0);
1262        let outside = Outside::of(&module);
1263        let mut walk = Walk::with(func, &outside, Options::default(), 0);
1264        assert_eq!(walk.clobber(load), Clobber::Unknown);
1265        assert_eq!(walk.counts().exhausted(), 1);
1266    }
1267
1268    #[test]
1269    fn translate_carries_the_walk_past_a_def_it_would_have_stopped_at() {
1270        let text = wrap(
1271            "(ptr) -> i32",
1272            "block0(%0: ptr):
1273    %1 = iconst.i32 7
1274    store %1 -> %0, align 4
1275    memcpy %0, %0, size 4, align 4
1276    %2 = load.i32 %0, align 4
1277    return %2
1278",
1279        );
1280        let (module, _) = built(&text);
1281        let func = one(&module);
1282        let load = nth(func, Opcode::Load, 0);
1283
1284        // With no rewrite to offer, the copy is where it stops.
1285        let outside = Outside::of(&module);
1286        let mut walk = Walk::new(func, &outside);
1287        let stopped_at = walk.clobber(load).inst().expect("something wrote it");
1288        assert_eq!(func[stopped_at].opcode, Opcode::Memcpy);
1289
1290        // The same walk, with a caller that can see through the copy. It says nothing about the
1291        // reference here, which is enough to show the callback is reached and obeyed.
1292        let mut walk = Walk::new(func, &outside);
1293        let mut seen = Vec::new();
1294        let answer = walk.clobber_with(load, &mut |reference, inst| {
1295            seen.push(func[inst].opcode);
1296            if func[inst].opcode == Opcode::Memcpy { Step::Retry(*reference) } else { Step::Stop }
1297        });
1298        assert_eq!(seen, [Opcode::Memcpy, Opcode::Store]);
1299        assert_eq!(answer.inst().map(|inst| func[inst].opcode), Some(Opcode::Store));
1300
1301        // One rewrite offered and one taken, which is the counter a caller reads to find out
1302        // whether its callback reached anything.
1303        assert_eq!(walk.counts().rewritten(), 1);
1304    }
1305
1306    #[test]
1307    fn building_twice_changes_nothing_the_second_time() {
1308        let text = wrap(
1309            "(ptr) -> i32",
1310            "block0(%0: ptr):
1311    %1 = load.i32 %0, align 4
1312    return %1
1313",
1314        );
1315        let (mut module, _) = read(&text);
1316        let id = module.funcs().next().expect("one function");
1317        let func = &mut module[id];
1318        assert!(build(func));
1319        let before = func.counts().insts;
1320        assert!(!build(func));
1321        assert_eq!(func.counts().insts, before);
1322    }
1323
1324    /// The builder path rather than the parser path, since a pass that adds a store adds it with
1325    /// the builder and the chain has to survive that too.
1326    #[test]
1327    fn a_function_built_by_hand_threads_the_same_way() {
1328        let mut names = Interner::new();
1329        let i32_ = Type::int(32);
1330        let mut func = Func::new(
1331            names.intern("f"),
1332            Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]),
1333        );
1334        let entry = func.create_block();
1335        let addr = func.append_param(entry, Type::PTR);
1336        let info = MemInfo {
1337            size: 4,
1338            align: 4,
1339            order: MemOrder::NotAtomic,
1340            tbaa: None,
1341            owns: 0,
1342            restrict: Restrict::NONE,
1343        };
1344        let mut b = Builder::new(&mut func, entry);
1345        let seven = b.iconst(i32_, 7);
1346        b.store(seven, addr, info, Flags::NONE);
1347        let read = b.load(i32_, addr, info, Flags::NONE);
1348        b.ret(&[read]);
1349
1350        assert!(build(&mut func));
1351        let store = nth(&func, Opcode::Store, 0);
1352        let load = nth(&func, Opcode::Load, 0);
1353        assert_eq!(func.mem_in(load), func.mem_out(store));
1354    }
1355
1356    /// Builds the chain, takes it back off, and insists the result verifies both times. A half
1357    /// removed chain is exactly the kind of thing that would pass a shape assertion and fail on a
1358    /// real file, so the verifier is the assertion that matters here too.
1359    fn stripped(text: &str) -> (Module, bool) {
1360        let (mut module, names) = read(text);
1361        let id = module.funcs().next().expect("one function");
1362        build(&mut module[id]);
1363        if let Err(errors) = verify_func(&module, &module[id], &names) {
1364            panic!("after building: {errors:#?}");
1365        }
1366        let changed = strip(&mut module[id]);
1367        if let Err(errors) = verify_func(&module, &module[id], &names) {
1368            panic!("after stripping: {errors:#?}");
1369        }
1370        (module, changed)
1371    }
1372
1373    /// Nothing anywhere in the function is on the chain any more.
1374    fn off(func: &Func) {
1375        for block in func.blocks() {
1376            assert!(
1377                func[block].params.iter().all(|&param| !func[param].ty.is_mem()),
1378                "a block kept a memory parameter"
1379            );
1380            for inst in func.insts(block) {
1381                assert_ne!(
1382                    func[inst].opcode,
1383                    Opcode::MemEntry,
1384                    "the start of the chain is still here"
1385                );
1386                assert!(!func.carries_mem(inst), "an instruction is still on the chain");
1387            }
1388        }
1389    }
1390
1391    #[test]
1392    fn a_straight_line_comes_off_the_chain_the_way_it_went_on() {
1393        let text = wrap(
1394            "(ptr) -> i32",
1395            "block0(%0: ptr):
1396    %1 = iconst.i32 7
1397    store %1 -> %0, align 4
1398    %2 = load.i32 %0, align 4
1399    return %2
1400",
1401        );
1402        let (module, changed) = stripped(&text);
1403        assert!(changed);
1404        let func = one(&module);
1405        off(func);
1406        // The instructions are the same ones doing the same thing, which is the whole claim: the
1407        // address the load reads is still the function's parameter and the value returned is
1408        // still what the load read.
1409        let load = nth(func, Opcode::Load, 0);
1410        let param = func[func.entry().expect("an entry")].params[0];
1411        assert_eq!(func[func[load].args][0], param);
1412        let ret = nth(func, Opcode::Return, 0);
1413        assert_eq!(func[func[ret].args][0], func[load].results().next().expect("a result"));
1414    }
1415
1416    #[test]
1417    fn a_join_gives_its_memory_parameter_back_and_so_does_every_branch_to_it() {
1418        let text = wrap(
1419            "(ptr, i1) -> i32",
1420            "block0(%0: ptr, %1: i1):
1421    br_if %1, block1, block2
1422
1423block1:
1424    %2 = iconst.i32 7
1425    store %2 -> %0, align 4
1426    jump block3
1427
1428block2:
1429    jump block3
1430
1431block3:
1432    %3 = load.i32 %0, align 4
1433    return %3
1434",
1435        );
1436        let (module, changed) = stripped(&text);
1437        assert!(changed);
1438        let func = one(&module);
1439        off(func);
1440        let join = func.blocks().nth(3).expect("four blocks");
1441        assert!(func[join].params.is_empty(), "the join kept a parameter");
1442        for block in func.blocks() {
1443            let Some(terminator) = func.terminator(block) else { continue };
1444            for call in func.successors(terminator) {
1445                assert!(func[call.args].is_empty(), "a branch kept an argument");
1446            }
1447        }
1448    }
1449
1450    #[test]
1451    fn a_parameter_that_was_never_memory_keeps_its_place() {
1452        // The argument a branch passes goes by position, so a block with a memory parameter
1453        // beside an ordinary one is where taking the wrong one out would show.
1454        let text = wrap(
1455            "(ptr, i1) -> i32",
1456            "block0(%0: ptr, %1: i1):
1457    %2 = iconst.i32 7
1458    br_if %1, block1(%2), block2
1459
1460block1(%3: i32):
1461    store %3 -> %0, align 4
1462    jump block3
1463
1464block2:
1465    jump block3
1466
1467block3:
1468    %4 = load.i32 %0, align 4
1469    return %4
1470",
1471        );
1472        let (module, _) = stripped(&text);
1473        let func = one(&module);
1474        off(func);
1475        let arm = func.blocks().nth(1).expect("four blocks");
1476        assert_eq!(func[arm].params.len(), 1);
1477        let param = func[arm].params[0];
1478        assert_eq!(func[param].ty, Type::int(32));
1479        let store = nth(func, Opcode::Store, 0);
1480        assert_eq!(func[func[store].args][0], param, "the store lost the value it writes");
1481    }
1482
1483    #[test]
1484    fn a_function_that_was_never_on_the_chain_is_left_alone() {
1485        let text = wrap(
1486            "(i32) -> i32",
1487            "block0(%0: i32):
1488    %1 = add %0, %0
1489    return %1
1490",
1491        );
1492        let (mut module, names) = read(&text);
1493        let id = module.funcs().next().expect("one function");
1494        assert!(!strip(&mut module[id]));
1495        if let Err(errors) = verify_func(&module, &module[id], &names) {
1496            panic!("{errors:#?}");
1497        }
1498    }
1499
1500    #[test]
1501    fn a_call_that_returns_something_keeps_it() {
1502        // A call is threaded like a store and gives back a value as well, so its results are the
1503        // one place where the version of memory sits behind something that has a reader.
1504        let text = format!(
1505            "{HEADER}\nfunc @f() -> i32, linkage(external) {{\nblock0:\n    %0 = call @g() : () -> \
1506             i32\n    return %0\n}}\n"
1507        );
1508        let (module, changed) = stripped(&text);
1509        assert!(changed);
1510        let func = one(&module);
1511        off(func);
1512        let call = nth(func, Opcode::Call, 0);
1513        let ret = nth(func, Opcode::Return, 0);
1514        assert_eq!(func[call].results().count(), 1);
1515        assert_eq!(func[func[ret].args][0], func[call].results().next().expect("a result"));
1516    }
1517}