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