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