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