Skip to main content

rucc_opt/
header_copy.rs

1//! Copies a loop's header in front of the loop, so the test ends up at the bottom.
2//!
3//! Design: `spec/optimizer/26-loop-canonicalization.md` section 26.6, with 26.7 for where it sits
4//! and 26.8 for the two ways it goes wrong.
5//!
6//! [`crate::canon`] establishes the four properties every loop pass is allowed to assume and
7//! generates nothing on its own. This is the fifth property and it is the one that changes the
8//! program. A `while (c) { body }` tests at the top, so its header is a join and a branch at once
9//! and the test runs once more than the body does. Copying the header in front of the loop turns
10//! it into `if (c) { do { body } while (c); }`, which evaluates the condition exactly as often and
11//! leaves a loop whose body is a single region and whose exit test is at the bottom where the
12//! induction variable's last value is.
13//!
14//! # What it is really for
15//!
16//! Section 26.6 says the largest single benefit is not the shape. It is that after the copy the
17//! entry test stands in front of the loop where document 10's ranges can be asked about it, and
18//! where the ranges settle it the loop is known to run at least one iteration. That is what turns
19//! a trip count estimate into a bound, what lets hoisting move a computation out without proving
20//! it safe to speculate, and what saves the vectorizer a guard. So the range query is not a
21//! refinement on the copy, it is half of the reason to make it, and it happens here rather than
22//! being left to [`crate::prune`] because prune has already run by the time the loop pipeline
23//! opens.
24//!
25//! # One block, not a chain
26//!
27//! GCC copies as many blocks as its budget allows, walking down from the header while
28//! `should_duplicate_loop_header_p` keeps saying yes. This copies the header and stops. The header
29//! is where the exit test is, so one block is what the do-while form needs, and a chain buys the
30//! cases where the condition is spread over several blocks that nothing has managed to merge. The
31//! bound is the same either way and the second block can be added when the corpus says which
32//! programs want it.
33//!
34//! Copying a header could otherwise feed itself: the block the copy makes the new header of the
35//! loop may test and exit as well, and copying that one exposes a third. Every header this pass
36//! copies and every block it makes a header of are put aside, so each loop is looked at once per
37//! run and the growth is bounded by the loop count rather than by how the branches happen to nest.
38//!
39//! # Why the copy repeats nothing
40//!
41//! The copy runs exactly where the header's first execution used to, so nothing in the program
42//! happens a different number of times. That argument would let a store or a call be copied, and
43//! section 26.8 refuses both anyway, through document 17.1's whitelist, which is
44//! [`Opcode::has_effects`]. The reason to keep the refusal is that the argument above holds for
45//! one block and stops holding the moment the copy is a chain, and a pass whose correctness
46//! depends on a bound somebody may raise later is one that will be wrong later. Refusing here
47//! costs the headers with a load in them, which document 27's hoisting is the pass for.
48//!
49//! # What the copy owes the values
50//!
51//! The header used to dominate the whole loop. After the copy it does not: the body is reached
52//! from the copy as well, so a value the header defined and the body read has two definitions
53//! reaching it and needs a merge. The merge goes where the two paths meet, which is the body, as
54//! one more block parameter carrying the header's value on the back edge and the copy's on the
55//! way in.
56//!
57//! Values the header defines and something outside the loop reads are refused rather than merged.
58//! After [`crate::canon`] there are none, because loop-closed form has already routed them through
59//! the exit, so the case this declines is the one where somebody ran this pass without the
60//! canonicalizer and the answer to that is a missed optimization rather than a second merge
61//! written for a shape the pipeline does not produce.
62//!
63//! # Which level
64//!
65//! `-O1` and above at [`SPEED`]'s budget, which is GCC's twenty. `-Os` at [`SIZE`]'s, which is
66//! section 26.6's five, because the do-while form is slightly smaller in the steady state and the
67//! copy is what it costs. `-Oz` does not run it at all. Two passes rather than one with a knob,
68//! because a pass here is a name a `-f` flag spells and there is nowhere for a level to hand a
69//! pass a number.
70
71use std::collections::{HashMap, HashSet};
72
73use rucc_cost::heuristics;
74use rucc_ir::{
75    Block, BlockCall, Builder, ExtraKind, Func, Inst, InstData, Opcode, Type, Value, ValueList,
76};
77
78use crate::cfg::Cfg;
79use crate::dom::Dominators;
80use crate::loops::Loops;
81use crate::range::query::Ranges;
82use crate::{Analyses, Fuel, Pass, Preserved, Stats, prune, simplify_cfg};
83
84const COPIED: &str = "loop header copied in front of the loop so the test is at the bottom";
85const ENTERED: &str = "entry test removed, the value ranges say the loop runs";
86const SKIPPED: &str = "loop removed, the value ranges say the entry test never holds";
87const UNDECIDED: &str = "entry test kept, the value ranges do not settle whether the loop runs";
88const ALREADY: &str = "loop left as it was, it already tests at the bottom";
89const TOO_BIG: &str = "loop header not copied, it is larger than this level allows";
90const EFFECTS: &str = "loop header not copied, something in it may not be repeated";
91const SHAPE: &str = "loop header not copied, its exit is not a two way branch";
92const ESCAPES: &str = "loop header not copied, a value it defines is read outside the loop";
93const NO_PREHEADER: &str = "loop header not copied, the loop has not been canonicalized";
94const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
95
96/// Section 26.6's transformation, at one budget.
97///
98/// The budget is a field rather than a constant because the two levels that run this want
99/// different ones, and it comes with the name for the same reason: the two instances are two
100/// entries in [`crate::pass::PASSES`] and a pipeline picks one by naming it.
101#[derive(Debug)]
102pub struct HeaderCopy {
103    /// What a `-f` flag spells.
104    name: &'static str,
105    /// How many instructions a header may hold and still be worth copying, which is
106    /// [`heuristics::LOOP_HEADER_INSNS_FOR_SPEED`] or the size one next to it.
107    budget: u32,
108}
109
110/// The instance `-O1`, `-O2` and `-O3` run, at GCC's budget.
111pub static SPEED: HeaderCopy =
112    HeaderCopy { name: "header-copy", budget: heuristics::LOOP_HEADER_INSNS_FOR_SPEED };
113
114/// The instance `-Os` runs, at section 26.6's smaller one.
115pub static SIZE: HeaderCopy =
116    HeaderCopy { name: "header-copy-small", budget: heuristics::LOOP_HEADER_INSNS_FOR_SIZE };
117
118impl Pass for HeaderCopy {
119    fn name(&self) -> &'static str {
120        self.name
121    }
122
123    fn describe(&self) -> &'static str {
124        "copies a loop header in front of the loop, turning a while into a do-while"
125    }
126
127    fn preserves(&self) -> Preserved {
128        // A block appears, two edges become four, and the body grows a parameter the loop carries.
129        Preserved::NONE
130    }
131
132    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
133        let mut stats = Stats::new();
134        if func.entry().is_none() {
135            return stats;
136        }
137        let mut done = HashSet::new();
138        let mut say = true;
139        while let Some(job) = self.plan(func, an, &done, &mut stats, say) {
140            say = false;
141            if !fuel.take() {
142                stats.missed(NO_FUEL);
143                break;
144            }
145            done.insert(job.header);
146            done.insert(job.body);
147            let copy = apply(func, &job);
148            stats.optimized(COPIED);
149            an.clear();
150            if settle(func, an, copy, &mut stats) {
151                an.clear();
152            }
153        }
154        if stats.changed() {
155            // Section 6.5 leaves the stranded blocks to whoever stranded them, and a loop whose
156            // entry test the ranges disproved is a loop nothing reaches any more.
157            simplify_cfg::sweep(func, an, &mut stats);
158        }
159        an.clear();
160        stats
161    }
162}
163
164/// One loop to copy the header of, worked out against the function as it stands.
165#[derive(Debug)]
166struct Job {
167    /// The block holding the exit test.
168    header: Block,
169    /// The one block outside the loop the header is reached from.
170    entry: Block,
171    /// The header's successor inside the loop, which the copy makes the new header.
172    ///
173    /// The other one, which is where the loop leaves from, is not recorded. The copy branches to
174    /// both by copying the header's own terminator, and nothing after that has a question to ask
175    /// about the one that goes out.
176    body: Block,
177    /// The values the header defines and the rest of the loop reads, which need a merge at
178    /// [`Job::body`] once there are two ways to get there.
179    carried: Vec<Value>,
180}
181
182impl HeaderCopy {
183    /// The first loop worth copying the header of, and what it would take.
184    ///
185    /// One at a time, because the copy adds a block to the loop it is made for and the forest the
186    /// next answer would be read out of is the one this call just invalidated. `say` is false on
187    /// every call after the first so that a loop this declines is declined once rather than once
188    /// per round.
189    fn plan(
190        &self,
191        func: &Func,
192        an: &mut Analyses,
193        done: &HashSet<Block>,
194        stats: &mut Stats,
195        say: bool,
196    ) -> Option<Job> {
197        let (cfg, dom, loops) = (an.cfg(func), an.dominators(func), an.loops(func));
198        let mut found = None;
199        for id in loops.all() {
200            let header = loops.header(id);
201            if done.contains(&header) {
202                continue;
203            }
204            match self.consider(func, cfg, dom, loops, id, header) {
205                Ok(job) => {
206                    if found.is_none() {
207                        found = Some(job);
208                    }
209                    if !say {
210                        break;
211                    }
212                }
213                Err(why) if say && why == ALREADY => stats.note(ALREADY),
214                Err(why) if say => stats.missed(why),
215                Err(_) => (),
216            }
217        }
218        found
219    }
220
221    /// Whether this loop can have its header copied, and why not when it cannot.
222    fn consider(
223        &self,
224        func: &Func,
225        cfg: &Cfg,
226        dom: &Dominators,
227        loops: &Loops,
228        id: crate::loops::LoopId,
229        header: Block,
230    ) -> Result<Job, &'static str> {
231        let leaves = cfg.successors(header).iter().any(|&to| !loops.contains(id, to));
232        if !leaves {
233            // The exit test is somewhere below, which is the shape this pass is trying to reach.
234            // GCC asks the same question the other way round in `do_while_loop_p`.
235            return Err(ALREADY);
236        }
237        let entry = loops.preheader(cfg, id).ok_or(NO_PREHEADER)?;
238        let term = func.terminator(header).ok_or(SHAPE)?;
239        if func[term].opcode != Opcode::BrIf {
240            return Err(SHAPE);
241        }
242        let calls: Vec<BlockCall> = func.successors(term).collect();
243        let [then_call, else_call] = calls[..].try_into().map_err(|_| SHAPE)?;
244        let body = match (loops.contains(id, then_call.block), loops.contains(id, else_call.block))
245        {
246            (true, false) => then_call.block,
247            (false, true) => else_call.block,
248            _ => return Err(SHAPE),
249        };
250        if body == header {
251            return Err(SHAPE);
252        }
253        let insts: Vec<Inst> = func.insts(header).filter(|&inst| inst != term).collect();
254        if insts.len() > self.budget as usize {
255            return Err(TOO_BIG);
256        }
257        for &inst in &insts {
258            if !repeatable(func, inst) {
259                return Err(EFFECTS);
260            }
261        }
262        let carried = carried(func, dom, loops, id, header, body, &insts)?;
263        Ok(Job { header, entry, body, carried })
264    }
265}
266
267/// Whether an instruction may stand in a second copy of the block it is in.
268///
269/// Two questions rather than one. [`Opcode::has_effects`] is document 17.1's whitelist and is what
270/// section 26.8 names. The second is about this pass rather than about the program: an instruction
271/// carrying a side table entry is copied here by copying the index, which is right for an
272/// immediate, a symbol and a comparison because those tables are written once and read for ever,
273/// and is not something to assume about a table nobody has checked. So the copy is restricted to
274/// the payloads it has been thought about, and an instruction with any other is declined the same
275/// way one with an effect is.
276fn repeatable(func: &Func, inst: Inst) -> bool {
277    let data = func[inst];
278    if data.opcode.has_effects() || func.carries_mem(inst) {
279        return false;
280    }
281    matches!(
282        data.extra.kind(),
283        ExtraKind::None
284            | ExtraKind::Imm
285            | ExtraKind::Symbol
286            | ExtraKind::IntPred
287            | ExtraKind::FloatPred
288    )
289}
290
291/// The values the header defines that the rest of the loop reads.
292///
293/// These are what the copy owes a merge at the body. A value read outside the loop is an error
294/// rather than an entry, because merging it would need a second parameter at the exit and after
295/// [`crate::canon`] there is no such value to merge: loop-closed form has already routed it.
296fn carried(
297    func: &Func,
298    dom: &Dominators,
299    loops: &Loops,
300    id: crate::loops::LoopId,
301    header: Block,
302    body: Block,
303    insts: &[Inst],
304) -> Result<Vec<Value>, &'static str> {
305    let mut defined: Vec<Value> = func[header].params.clone();
306    for &inst in insts {
307        defined.extend(func[inst].results());
308    }
309    // One walk of the function for all of them at once rather than one walk each. A header defines
310    // a handful of values and the function it is in can be very large, and this used to be most of
311    // the time an optimized build of a large function spent. See tamnd/rucc#1015.
312    let watched: HashSet<Value> = defined.iter().copied().collect();
313    let mut read: HashSet<Value> = HashSet::new();
314    for block in func.blocks() {
315        if block == header {
316            continue;
317        }
318        let mut names = false;
319        for inst in func.insts(block) {
320            names |= reads(func, inst, &watched, &mut read);
321        }
322        if names && (!loops.contains(id, block) || !dom.dominates(body, block)) {
323            return Err(ESCAPES);
324        }
325    }
326    Ok(defined.into_iter().filter(|value| read.contains(value)).collect())
327}
328
329/// Records every watched value this instruction names, as an operand or on an edge out of it.
330///
331/// Answers whether it named any of them, which is the block's business rather than the value's: a
332/// block that reads one of these from the wrong place is an error whichever one it read.
333fn reads(func: &Func, inst: Inst, watched: &HashSet<Value>, read: &mut HashSet<Value>) -> bool {
334    let mut named = false;
335    for &value in &func[func[inst].args] {
336        if watched.contains(&value) {
337            read.insert(value);
338            named = true;
339        }
340    }
341    for call in func.successors(inst) {
342        for &value in &func[call.args] {
343            if watched.contains(&value) {
344                read.insert(value);
345                named = true;
346            }
347        }
348    }
349    named
350}
351
352/// Makes the copy, puts it on the edge into the loop, and returns it.
353fn apply(func: &mut Func, job: &Job) -> Block {
354    let term = func.terminator(job.header).expect("the plan read this terminator");
355    let entry_term = func.terminator(job.entry).expect("a preheader ends in a jump");
356    // The header's parameters stand for whatever the one edge in hands them, so the copy is
357    // written in terms of those arguments and needs no parameters of its own.
358    let incoming = edge_args(func, entry_term, job.header);
359    let mut map: HashMap<Value, Value> = HashMap::new();
360    for (&param, &arg) in func[job.header].params.clone().iter().zip(&incoming) {
361        map.insert(param, arg);
362    }
363    let copy = func.create_block();
364    let insts: Vec<Inst> = func.insts(job.header).filter(|&inst| inst != term).collect();
365    for inst in insts {
366        clone_into(func, copy, inst, &mut map);
367    }
368    clone_branch(func, copy, term, &map);
369    for at in func.target_list(entry_term).iter() {
370        let call = func[at];
371        if call.block == job.header {
372            func.set_block_call(at, BlockCall { block: copy, args: ValueList::EMPTY, ..call });
373        }
374    }
375    for &value in &job.carried {
376        let arrived = map.get(&value).copied().unwrap_or(value);
377        merge(func, job, copy, value, arrived);
378    }
379    copy
380}
381
382/// The arguments a terminator hands one of its targets.
383fn edge_args(func: &Func, term: Inst, to: Block) -> Vec<Value> {
384    for call in func.successors(term) {
385        if call.block == to {
386            return func[call.args].to_vec();
387        }
388    }
389    Vec::new()
390}
391
392/// Copies one instruction to the end of a block, under the substitution, and records its results.
393fn clone_into(func: &mut Func, into: Block, inst: Inst, map: &mut HashMap<Value, Value>) {
394    let data = func[inst];
395    let args: Vec<Value> =
396        func[data.args].iter().map(|value| map.get(value).copied().unwrap_or(*value)).collect();
397    let types: Vec<Type> = data.results().map(|result| func[result].ty).collect();
398    let span = func.span(inst);
399    let args = func.push_values(&args);
400    let fresh = func.create_inst(InstData { args, ..data }, &types, span);
401    func.append_inst(into, fresh);
402    for (old, new) in data.results().zip(func[fresh].results()) {
403        map.insert(old, new);
404    }
405}
406
407/// Copies the header's two way branch to the end of the copy, under the substitution.
408///
409/// The targets are the header's own. What that means for the graph is that the copy decides,
410/// before the loop, which of the two places the header would have gone control goes to, and the
411/// header is left deciding it for every iteration after the first.
412fn clone_branch(func: &mut Func, into: Block, term: Inst, map: &HashMap<Value, Value>) {
413    let at = |value: &Value| map.get(value).copied().unwrap_or(*value);
414    let cond = at(&func[func[term].args][0]);
415    let calls: Vec<BlockCall> = func.successors(term).collect();
416    let args: Vec<Vec<Value>> =
417        calls.iter().map(|call| func[call.args].iter().map(at).collect()).collect();
418    Builder::new(func, into).br_if(cond, calls[0].block, &args[0], calls[1].block, &args[1]);
419}
420
421/// Gives the body a parameter for a value the header defines, and points the loop at it.
422///
423/// Three kinds of edge arrive at the body once the copy is in place. The header's carries what the
424/// header worked out, which is the value on every iteration after the first. The copy's carries
425/// what the copy worked out, which is the value on the first. Anything else is a block inside the
426/// loop, and what is current there is the parameter itself, which is available because the body
427/// dominates the whole loop the moment the copy is the only way in.
428fn merge(func: &mut Func, job: &Job, copy: Block, value: Value, arrived: Value) {
429    let param = func.append_param(job.body, func[value].ty);
430    for block in func.blocks().collect::<Vec<_>>() {
431        let Some(term) = func.terminator(block) else { continue };
432        let carry = if block == job.header {
433            value
434        } else if block == copy {
435            arrived
436        } else {
437            param
438        };
439        for at in func.target_list(term).iter() {
440            let call = func[at];
441            if call.block != job.body {
442                continue;
443            }
444            let args = func.append_arg(call.args, carry);
445            func.set_block_call(at, BlockCall { args, ..call });
446        }
447    }
448    // Everything the header used to reach reads the parameter now. The header itself does not:
449    // what it hands the body is still its own definition, and that is the edge the parameter was
450    // put there to distinguish.
451    for block in func.blocks().collect::<Vec<_>>() {
452        if block == job.header || block == copy {
453            continue;
454        }
455        for inst in func.insts(block).collect::<Vec<_>>() {
456            let swap = |had: Value| if had == value { param } else { had };
457            func.rewrite(func[inst].args, swap);
458            for at in func.target_list(inst).iter() {
459                func.rewrite(func[at].args, swap);
460            }
461        }
462    }
463}
464
465/// Asks the ranges whether the copied test is settled, and takes it out where it is.
466///
467/// This is section 26.6's point about the entry condition. A test that always holds leaves a loop
468/// known to run at least once, which is what document 07.5's trip count wanted. One that never
469/// holds leaves the loop unreachable, and taking it out is [`crate::simplify_cfg::sweep`]'s job
470/// rather than this one's.
471///
472/// Answers whether it moved an edge, which the caller needs because the analyses it just built to
473/// ask the ranges are the ones the next round wants and they are only stale if this took a branch
474/// out. The ranges settle the test on a minority of the loops here and the graph is the size of
475/// the function, so the rounds where nothing happens used to pay for a rebuild that changed
476/// nothing. tamnd/rucc#1045.
477fn settle(func: &mut Func, an: &mut Analyses, copy: Block, stats: &mut Stats) -> bool {
478    let Some(term) = func.terminator(copy) else { return false };
479    let cond = func[func[term].args][0];
480    let answer = {
481        let cfg = an.cfg(func);
482        let dom = an.dominators(func);
483        let mut ranges = Ranges::new(func, cfg, dom);
484        prune::settled(func, &mut ranges, copy, cond)
485    };
486    let Some(taken) = answer else {
487        stats.missed(UNDECIDED);
488        return false;
489    };
490    let calls: Vec<BlockCall> = func.successors(term).collect();
491    let call = if taken { calls[0] } else { calls[1] };
492    simplify_cfg::jump_to(func, term, call);
493    stats.optimized(if taken { ENTERED } else { SKIPPED });
494    true
495}
496
497#[cfg(test)]
498mod tests {
499    use rucc_base::Interner;
500    use rucc_ir::{
501        Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
502        Signature, Type, verify_func,
503    };
504    use rucc_target::{TargetInfo, Triple};
505
506    use super::{HeaderCopy, SIZE, SPEED};
507    use crate::canon::Canon;
508    use crate::cfg::Cfg;
509    use crate::dom::Dominators;
510    use crate::loops::Loops;
511    use crate::stats::Kind;
512    use crate::{Fuel, Pass, Stats};
513
514    /// Canonicalizes and then copies, with as much fuel as both want.
515    ///
516    /// Both, because the pass is written against the shape [`Canon`] leaves and running it over
517    /// anything else is a test of a situation the pipeline does not produce. Section 26.7 puts the
518    /// two next to each other in that order and so does this.
519    fn copied(func: &mut Func, pass: &HeaderCopy) -> Stats {
520        let mut an = crate::machine::fixtures::analyses();
521        Canon.run(func, &mut an, &mut Fuel::unlimited());
522        pass.run(func, &mut an, &mut Fuel::unlimited())
523    }
524
525    /// The forest of the function as it is now.
526    fn forest(func: &Func) -> (Cfg, Dominators, Loops) {
527        let cfg = Cfg::new(func);
528        let dom = Dominators::new(&cfg);
529        let loops = Loops::new(&cfg, &dom);
530        (cfg, dom, loops)
531    }
532
533    /// Insists the function is one the rest of the compiler may believe.
534    ///
535    /// This is where most of the strength of these tests is. The copy gives the loop a second way
536    /// in, which is exactly the edit that breaks a definition's dominance over its uses, and the
537    /// verifier is what says whether the merge the pass wrote is the merge the graph needed.
538    fn sound(func: &Func, names: &mut Interner) {
539        let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
540        let module = Module::new(names.intern("t.c"), &target);
541        if let Err(errors) = verify_func(&module, func, names) {
542            panic!("{errors:#?}");
543        }
544    }
545
546    /// A counted loop that tests at the top, which is what `while (i < n)` lowers to.
547    ///
548    /// ```text
549    /// entry: i0 = 0; jump head(i0)
550    /// head(i): t = i < n; br t -> body, done
551    /// body: next = i + 1; jump head(next)
552    /// done: ret i
553    /// ```
554    ///
555    /// `bound` is the limit as a constant, or nothing for a limit the function was handed and
556    /// which the ranges therefore cannot settle.
557    fn counted(bound: Option<i128>) -> (Func, Interner, Vec<Block>) {
558        let mut names = Interner::new();
559        let params: &[Type] = if bound.is_some() { &[] } else { &[Type::int(32)] };
560        let signature = Signature::new().with_params(params).with_returns(&[Type::int(32)]);
561        let mut func = Func::new(names.intern("f"), signature);
562        let entry = func.create_block();
563        let head = func.create_block();
564        let body = func.create_block();
565        let done = func.create_block();
566        let limit = match bound {
567            Some(value) => Builder::new(&mut func, entry).iconst(Type::int(32), value),
568            None => func.append_param(entry, Type::int(32)),
569        };
570        let i = func.append_param(head, Type::int(32));
571        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
572        Builder::new(&mut func, entry).jump(head, &[zero]);
573        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
574        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
575        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
576        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
577        Builder::new(&mut func, body).jump(head, &[next]);
578        Builder::new(&mut func, done).ret(&[i]);
579        (func, names, vec![entry, head, body, done])
580    }
581
582    /// Whether the header of the only loop leaves it, which is the question section 26.6 is about.
583    fn tests_at_the_top(func: &Func) -> bool {
584        let (cfg, dom, loops) = forest(func);
585        let _ = dom;
586        let id = loops.all().next().expect("there is a loop");
587        let header = loops.header(id);
588        cfg.successors(header).iter().any(|&to| !loops.contains(id, to))
589    }
590
591    #[test]
592    fn a_loop_that_tests_at_the_top_ends_up_testing_at_the_bottom() {
593        let (mut func, mut names, _) = counted(None);
594        assert!(tests_at_the_top(&func), "the shape this pass is for");
595
596        let stats = copied(&mut func, &SPEED);
597        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
598        assert!(!tests_at_the_top(&func), "the header no longer leaves the loop");
599        sound(&func, &mut names);
600    }
601
602    #[test]
603    fn the_value_the_header_defined_is_merged_where_the_two_ways_in_meet() {
604        let (mut func, mut names, blocks) = counted(None);
605        let body = blocks[2];
606        assert!(func[body].params.is_empty(), "the body carries nothing to start with");
607
608        copied(&mut func, &SPEED);
609        assert_eq!(func[body].params.len(), 1, "the counter arrives as a parameter now");
610        assert_eq!(
611            Cfg::new(&func).predecessors(body).len(),
612            2,
613            "one edge from the header and one from the copy"
614        );
615        sound(&func, &mut names);
616    }
617
618    #[test]
619    fn an_entry_test_the_ranges_settle_is_taken_out() {
620        let (mut func, mut names, _) = counted(Some(10));
621
622        let stats = copied(&mut func, &SPEED);
623        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
624        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 1);
625        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 0);
626        sound(&func, &mut names);
627
628        let (cfg, _dom, loops) = forest(&func);
629        let id = loops.all().next().expect("the loop is still there");
630        let entry = func.entry().expect("there is an entry");
631        assert!(cfg.reaches(loops.header(id)), "and it is still reached");
632        assert_eq!(cfg.successors(entry).len(), 1, "the guard in front of it has gone");
633    }
634
635    #[test]
636    fn a_loop_the_ranges_say_never_runs_is_removed() {
637        let (mut func, mut names, blocks) = counted(Some(0));
638
639        let stats = copied(&mut func, &SPEED);
640        assert_eq!(stats.count(Kind::Optimized, super::SKIPPED), 1);
641        sound(&func, &mut names);
642
643        let (_cfg, _dom, loops) = forest(&func);
644        assert_eq!(loops.count(), 0, "there is no loop left");
645        assert!(!func.blocks().any(|block| block == blocks[2]), "and the body has gone with it");
646    }
647
648    #[test]
649    fn a_test_the_ranges_cannot_settle_leaves_the_guard_where_it_is() {
650        let (mut func, _names, _) = counted(None);
651
652        let stats = copied(&mut func, &SPEED);
653        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
654        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 1);
655        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 0);
656    }
657
658    #[test]
659    fn a_second_run_changes_nothing() {
660        let (mut func, mut names, _) = counted(None);
661        copied(&mut func, &SPEED);
662        let again =
663            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
664        assert_eq!(again.count(Kind::Optimized, super::COPIED), 0, "there is nothing left to do");
665        assert_eq!(again.count(Kind::Note, super::ALREADY), 1, "and it says why");
666        sound(&func, &mut names);
667    }
668
669    #[test]
670    fn a_header_that_writes_to_memory_is_left_alone() {
671        let mut names = Interner::new();
672        let signature = Signature::new().with_params(&[Type::int(32), Type::PTR]).with_returns(&[]);
673        let mut func = Func::new(names.intern("f"), signature);
674        let entry = func.create_block();
675        let head = func.create_block();
676        let body = func.create_block();
677        let done = func.create_block();
678        let limit = func.append_param(entry, Type::int(32));
679        let addr = func.append_param(entry, Type::PTR);
680        let i = func.append_param(head, Type::int(32));
681        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
682        Builder::new(&mut func, entry).jump(head, &[zero]);
683        let access = MemInfo {
684            size: 4,
685            align: 4,
686            order: MemOrder::NotAtomic,
687            tbaa: None,
688            owns: 0,
689            restrict: Restrict::NONE,
690        };
691        Builder::new(&mut func, head).store(i, addr, access, Flags::NONE);
692        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
693        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
694        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
695        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
696        Builder::new(&mut func, body).jump(head, &[next]);
697        Builder::new(&mut func, done).ret(&[]);
698
699        let stats = copied(&mut func, &SPEED);
700        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
701        assert_eq!(stats.count(Kind::Missed, super::EFFECTS), 1);
702        assert!(tests_at_the_top(&func), "the loop is exactly as it was");
703    }
704
705    #[test]
706    fn a_header_larger_than_the_level_allows_is_left_alone() {
707        // Seven instructions in the header, which is over the size budget and well under the
708        // speed one, so the two instances of the pass disagree about the same function.
709        let stats = copied(&mut padded(6), &SIZE);
710        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
711        assert_eq!(stats.count(Kind::Missed, super::TOO_BIG), 1);
712
713        let stats = copied(&mut padded(6), &SPEED);
714        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1, "the speed budget is wider");
715    }
716
717    /// The counted loop with that many more instructions in its header, which do nothing.
718    fn padded(extra: usize) -> Func {
719        let (mut func, _names, blocks) = counted(None);
720        let head = blocks[1];
721        let term = func.terminator(head).expect("the header branches");
722        for _ in 0..extra {
723            let filler = Builder::new(&mut func, head).iconst(Type::int(32), 7);
724            let Def::Result { inst, .. } = func[filler].def else { unreachable!("an iconst") };
725            func.remove_inst(inst);
726            func.insert_before(inst, term);
727        }
728        func
729    }
730
731    #[test]
732    fn fuel_stops_the_copy_where_it_stands() {
733        let (mut func, _names, _) = counted(None);
734        let mut an = crate::machine::fixtures::analyses();
735        Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
736
737        let stats = SPEED.run(&mut func, &mut an, &mut Fuel::of(0));
738        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
739        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
740        assert!(tests_at_the_top(&func), "and the loop is as it was");
741    }
742
743    #[test]
744    fn a_value_the_header_defines_and_the_code_after_the_loop_reads_is_declined() {
745        // Straight to the copy, so loop-closed form has not been established and the counter the
746        // return names is still the header's own definition. That is the one value this pass will
747        // not merge, and section 26.7's answer is that [`Canon`] has already routed it by the time
748        // the pipeline gets here.
749        let (mut func, _names, _) = counted(None);
750
751        let stats =
752            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
753        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
754        assert_eq!(stats.count(Kind::Missed, super::ESCAPES), 1);
755        assert!(tests_at_the_top(&func), "and the loop is as it was");
756    }
757
758    #[test]
759    fn a_loop_with_no_preheader_is_declined() {
760        let mut names = Interner::new();
761        let signature = Signature::new().with_params(&[Type::int(1), Type::int(32)]);
762        let mut func = Func::new(names.intern("f"), signature);
763        let entry = func.create_block();
764        let one = func.create_block();
765        let two = func.create_block();
766        let head = func.create_block();
767        let body = func.create_block();
768        let done = func.create_block();
769        let c = func.append_param(entry, Type::int(1));
770        let limit = func.append_param(entry, Type::int(32));
771        let i = func.append_param(head, Type::int(32));
772        Builder::new(&mut func, entry).br_if(c, one, &[], two, &[]);
773        let zero = Builder::new(&mut func, one).iconst(Type::int(32), 0);
774        Builder::new(&mut func, one).jump(head, &[zero]);
775        let start = Builder::new(&mut func, two).iconst(Type::int(32), 1);
776        Builder::new(&mut func, two).jump(head, &[start]);
777        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
778        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
779        Builder::new(&mut func, body).jump(head, &[i]);
780        Builder::new(&mut func, done).ret(&[]);
781
782        let stats =
783            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
784        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
785        assert_eq!(stats.count(Kind::Missed, super::NO_PREHEADER), 1);
786
787        // And with one, which is what the pipeline hands it, the same loop is copied.
788        let stats = copied(&mut func, &SPEED);
789        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
790        sound(&func, &mut names);
791    }
792}