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            settle(func, an, copy, &mut stats);
151            an.clear();
152        }
153        if stats.changed() {
154            // Section 6.5 leaves the stranded blocks to whoever stranded them, and a loop whose
155            // entry test the ranges disproved is a loop nothing reaches any more.
156            simplify_cfg::sweep(func, an, &mut stats);
157        }
158        an.clear();
159        stats
160    }
161}
162
163/// One loop to copy the header of, worked out against the function as it stands.
164#[derive(Debug)]
165struct Job {
166    /// The block holding the exit test.
167    header: Block,
168    /// The one block outside the loop the header is reached from.
169    entry: Block,
170    /// The header's successor inside the loop, which the copy makes the new header.
171    ///
172    /// The other one, which is where the loop leaves from, is not recorded. The copy branches to
173    /// both by copying the header's own terminator, and nothing after that has a question to ask
174    /// about the one that goes out.
175    body: Block,
176    /// The values the header defines and the rest of the loop reads, which need a merge at
177    /// [`Job::body`] once there are two ways to get there.
178    carried: Vec<Value>,
179}
180
181impl HeaderCopy {
182    /// The first loop worth copying the header of, and what it would take.
183    ///
184    /// One at a time, because the copy adds a block to the loop it is made for and the forest the
185    /// next answer would be read out of is the one this call just invalidated. `say` is false on
186    /// every call after the first so that a loop this declines is declined once rather than once
187    /// per round.
188    fn plan(
189        &self,
190        func: &Func,
191        an: &mut Analyses,
192        done: &HashSet<Block>,
193        stats: &mut Stats,
194        say: bool,
195    ) -> Option<Job> {
196        let cfg = an.cfg(func).clone();
197        let dom = an.dominators(func).clone();
198        let loops = an.loops(func).clone();
199        let mut found = None;
200        for id in loops.all() {
201            let header = loops.header(id);
202            if done.contains(&header) {
203                continue;
204            }
205            match self.consider(func, &cfg, &dom, &loops, id, header) {
206                Ok(job) => {
207                    if found.is_none() {
208                        found = Some(job);
209                    }
210                    if !say {
211                        break;
212                    }
213                }
214                Err(why) if say && why == ALREADY => stats.note(ALREADY),
215                Err(why) if say => stats.missed(why),
216                Err(_) => (),
217            }
218        }
219        found
220    }
221
222    /// Whether this loop can have its header copied, and why not when it cannot.
223    fn consider(
224        &self,
225        func: &Func,
226        cfg: &Cfg,
227        dom: &Dominators,
228        loops: &Loops,
229        id: crate::loops::LoopId,
230        header: Block,
231    ) -> Result<Job, &'static str> {
232        let leaves = cfg.successors(header).iter().any(|&to| !loops.contains(id, to));
233        if !leaves {
234            // The exit test is somewhere below, which is the shape this pass is trying to reach.
235            // GCC asks the same question the other way round in `do_while_loop_p`.
236            return Err(ALREADY);
237        }
238        let entry = loops.preheader(cfg, id).ok_or(NO_PREHEADER)?;
239        let term = func.terminator(header).ok_or(SHAPE)?;
240        if func[term].opcode != Opcode::BrIf {
241            return Err(SHAPE);
242        }
243        let calls: Vec<BlockCall> = func.successors(term).collect();
244        let [then_call, else_call] = calls[..].try_into().map_err(|_| SHAPE)?;
245        let body = match (loops.contains(id, then_call.block), loops.contains(id, else_call.block))
246        {
247            (true, false) => then_call.block,
248            (false, true) => else_call.block,
249            _ => return Err(SHAPE),
250        };
251        if body == header {
252            return Err(SHAPE);
253        }
254        let insts: Vec<Inst> = func.insts(header).filter(|&inst| inst != term).collect();
255        if insts.len() > self.budget as usize {
256            return Err(TOO_BIG);
257        }
258        for &inst in &insts {
259            if !repeatable(func, inst) {
260                return Err(EFFECTS);
261            }
262        }
263        let carried = carried(func, dom, loops, id, header, body, &insts)?;
264        Ok(Job { header, entry, body, carried })
265    }
266}
267
268/// Whether an instruction may stand in a second copy of the block it is in.
269///
270/// Two questions rather than one. [`Opcode::has_effects`] is document 17.1's whitelist and is what
271/// section 26.8 names. The second is about this pass rather than about the program: an instruction
272/// carrying a side table entry is copied here by copying the index, which is right for an
273/// immediate, a symbol and a comparison because those tables are written once and read for ever,
274/// and is not something to assume about a table nobody has checked. So the copy is restricted to
275/// the payloads it has been thought about, and an instruction with any other is declined the same
276/// way one with an effect is.
277fn repeatable(func: &Func, inst: Inst) -> bool {
278    let data = func[inst];
279    if data.opcode.has_effects() || func.carries_mem(inst) {
280        return false;
281    }
282    matches!(
283        data.extra.kind(),
284        ExtraKind::None
285            | ExtraKind::Imm
286            | ExtraKind::Symbol
287            | ExtraKind::IntPred
288            | ExtraKind::FloatPred
289    )
290}
291
292/// The values the header defines that the rest of the loop reads.
293///
294/// These are what the copy owes a merge at the body. A value read outside the loop is an error
295/// rather than an entry, because merging it would need a second parameter at the exit and after
296/// [`crate::canon`] there is no such value to merge: loop-closed form has already routed it.
297fn carried(
298    func: &Func,
299    dom: &Dominators,
300    loops: &Loops,
301    id: crate::loops::LoopId,
302    header: Block,
303    body: Block,
304    insts: &[Inst],
305) -> Result<Vec<Value>, &'static str> {
306    let mut defined: Vec<Value> = func[header].params.clone();
307    for &inst in insts {
308        defined.extend(func[inst].results());
309    }
310    let mut carried = Vec::new();
311    for value in defined {
312        let mut read = false;
313        for block in func.blocks() {
314            if block == header || !reads(func, block, value) {
315                continue;
316            }
317            if !loops.contains(id, block) || !dom.dominates(body, block) {
318                return Err(ESCAPES);
319            }
320            read = true;
321        }
322        if read {
323            carried.push(value);
324        }
325    }
326    Ok(carried)
327}
328
329/// Whether anything in this block names the value, as an operand or on an edge out of it.
330fn reads(func: &Func, block: Block, value: Value) -> bool {
331    for inst in func.insts(block) {
332        if func[func[inst].args].contains(&value) {
333            return true;
334        }
335        for call in func.successors(inst) {
336            if func[call.args].contains(&value) {
337                return true;
338            }
339        }
340    }
341    false
342}
343
344/// Makes the copy, puts it on the edge into the loop, and returns it.
345fn apply(func: &mut Func, job: &Job) -> Block {
346    let term = func.terminator(job.header).expect("the plan read this terminator");
347    let entry_term = func.terminator(job.entry).expect("a preheader ends in a jump");
348    // The header's parameters stand for whatever the one edge in hands them, so the copy is
349    // written in terms of those arguments and needs no parameters of its own.
350    let incoming = edge_args(func, entry_term, job.header);
351    let mut map: HashMap<Value, Value> = HashMap::new();
352    for (&param, &arg) in func[job.header].params.clone().iter().zip(&incoming) {
353        map.insert(param, arg);
354    }
355    let copy = func.create_block();
356    let insts: Vec<Inst> = func.insts(job.header).filter(|&inst| inst != term).collect();
357    for inst in insts {
358        clone_into(func, copy, inst, &mut map);
359    }
360    clone_branch(func, copy, term, &map);
361    for at in func.target_list(entry_term).iter() {
362        if func[at].block == job.header {
363            func.set_block_call(at, BlockCall { block: copy, args: ValueList::EMPTY });
364        }
365    }
366    for &value in &job.carried {
367        let arrived = map.get(&value).copied().unwrap_or(value);
368        merge(func, job, copy, value, arrived);
369    }
370    copy
371}
372
373/// The arguments a terminator hands one of its targets.
374fn edge_args(func: &Func, term: Inst, to: Block) -> Vec<Value> {
375    for call in func.successors(term) {
376        if call.block == to {
377            return func[call.args].to_vec();
378        }
379    }
380    Vec::new()
381}
382
383/// Copies one instruction to the end of a block, under the substitution, and records its results.
384fn clone_into(func: &mut Func, into: Block, inst: Inst, map: &mut HashMap<Value, Value>) {
385    let data = func[inst];
386    let args: Vec<Value> =
387        func[data.args].iter().map(|value| map.get(value).copied().unwrap_or(*value)).collect();
388    let types: Vec<Type> = data.results().map(|result| func[result].ty).collect();
389    let span = func.span(inst);
390    let args = func.push_values(&args);
391    let fresh = func.create_inst(InstData { args, ..data }, &types, span);
392    func.append_inst(into, fresh);
393    for (old, new) in data.results().zip(func[fresh].results()) {
394        map.insert(old, new);
395    }
396}
397
398/// Copies the header's two way branch to the end of the copy, under the substitution.
399///
400/// The targets are the header's own. What that means for the graph is that the copy decides,
401/// before the loop, which of the two places the header would have gone control goes to, and the
402/// header is left deciding it for every iteration after the first.
403fn clone_branch(func: &mut Func, into: Block, term: Inst, map: &HashMap<Value, Value>) {
404    let at = |value: &Value| map.get(value).copied().unwrap_or(*value);
405    let cond = at(&func[func[term].args][0]);
406    let calls: Vec<BlockCall> = func.successors(term).collect();
407    let args: Vec<Vec<Value>> =
408        calls.iter().map(|call| func[call.args].iter().map(at).collect()).collect();
409    Builder::new(func, into).br_if(cond, calls[0].block, &args[0], calls[1].block, &args[1]);
410}
411
412/// Gives the body a parameter for a value the header defines, and points the loop at it.
413///
414/// Three kinds of edge arrive at the body once the copy is in place. The header's carries what the
415/// header worked out, which is the value on every iteration after the first. The copy's carries
416/// what the copy worked out, which is the value on the first. Anything else is a block inside the
417/// loop, and what is current there is the parameter itself, which is available because the body
418/// dominates the whole loop the moment the copy is the only way in.
419fn merge(func: &mut Func, job: &Job, copy: Block, value: Value, arrived: Value) {
420    let param = func.append_param(job.body, func[value].ty);
421    for block in func.blocks().collect::<Vec<_>>() {
422        let Some(term) = func.terminator(block) else { continue };
423        let carry = if block == job.header {
424            value
425        } else if block == copy {
426            arrived
427        } else {
428            param
429        };
430        for at in func.target_list(term).iter() {
431            let call = func[at];
432            if call.block != job.body {
433                continue;
434            }
435            let args = func.append_arg(call.args, carry);
436            func.set_block_call(at, BlockCall { block: call.block, args });
437        }
438    }
439    // Everything the header used to reach reads the parameter now. The header itself does not:
440    // what it hands the body is still its own definition, and that is the edge the parameter was
441    // put there to distinguish.
442    for block in func.blocks().collect::<Vec<_>>() {
443        if block == job.header || block == copy {
444            continue;
445        }
446        for inst in func.insts(block).collect::<Vec<_>>() {
447            let swap = |had: Value| if had == value { param } else { had };
448            func.rewrite(func[inst].args, swap);
449            for at in func.target_list(inst).iter() {
450                func.rewrite(func[at].args, swap);
451            }
452        }
453    }
454}
455
456/// Asks the ranges whether the copied test is settled, and takes it out where it is.
457///
458/// This is section 26.6's point about the entry condition. A test that always holds leaves a loop
459/// known to run at least once, which is what document 07.5's trip count wanted. One that never
460/// holds leaves the loop unreachable, and taking it out is [`crate::simplify_cfg::sweep`]'s job
461/// rather than this one's.
462fn settle(func: &mut Func, an: &mut Analyses, copy: Block, stats: &mut Stats) {
463    let Some(term) = func.terminator(copy) else { return };
464    let cond = func[func[term].args][0];
465    let answer = {
466        let cfg = an.cfg(func).clone();
467        let dom = an.dominators(func).clone();
468        let mut ranges = Ranges::new(func, &cfg, &dom);
469        prune::settled(func, &mut ranges, copy, cond)
470    };
471    let Some(taken) = answer else {
472        stats.missed(UNDECIDED);
473        return;
474    };
475    let calls: Vec<BlockCall> = func.successors(term).collect();
476    let call = if taken { calls[0] } else { calls[1] };
477    simplify_cfg::jump_to(func, term, call);
478    stats.optimized(if taken { ENTERED } else { SKIPPED });
479}
480
481#[cfg(test)]
482mod tests {
483    use rucc_base::Interner;
484    use rucc_ir::{
485        Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
486        Signature, Type, verify_func,
487    };
488    use rucc_target::{TargetInfo, Triple};
489
490    use super::{HeaderCopy, SIZE, SPEED};
491    use crate::canon::Canon;
492    use crate::cfg::Cfg;
493    use crate::dom::Dominators;
494    use crate::loops::Loops;
495    use crate::stats::Kind;
496    use crate::{Analyses, Fuel, Pass, Stats};
497
498    /// Canonicalizes and then copies, with as much fuel as both want.
499    ///
500    /// Both, because the pass is written against the shape [`Canon`] leaves and running it over
501    /// anything else is a test of a situation the pipeline does not produce. Section 26.7 puts the
502    /// two next to each other in that order and so does this.
503    fn copied(func: &mut Func, pass: &HeaderCopy) -> Stats {
504        let mut an = Analyses::new();
505        Canon.run(func, &mut an, &mut Fuel::unlimited());
506        pass.run(func, &mut an, &mut Fuel::unlimited())
507    }
508
509    /// The forest of the function as it is now.
510    fn forest(func: &Func) -> (Cfg, Dominators, Loops) {
511        let cfg = Cfg::new(func);
512        let dom = Dominators::new(&cfg);
513        let loops = Loops::new(&cfg, &dom);
514        (cfg, dom, loops)
515    }
516
517    /// Insists the function is one the rest of the compiler may believe.
518    ///
519    /// This is where most of the strength of these tests is. The copy gives the loop a second way
520    /// in, which is exactly the edit that breaks a definition's dominance over its uses, and the
521    /// verifier is what says whether the merge the pass wrote is the merge the graph needed.
522    fn sound(func: &Func, names: &mut Interner) {
523        let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
524        let module = Module::new(names.intern("t.c"), &target);
525        if let Err(errors) = verify_func(&module, func, names) {
526            panic!("{errors:#?}");
527        }
528    }
529
530    /// A counted loop that tests at the top, which is what `while (i < n)` lowers to.
531    ///
532    /// ```text
533    /// entry: i0 = 0; jump head(i0)
534    /// head(i): t = i < n; br t -> body, done
535    /// body: next = i + 1; jump head(next)
536    /// done: ret i
537    /// ```
538    ///
539    /// `bound` is the limit as a constant, or nothing for a limit the function was handed and
540    /// which the ranges therefore cannot settle.
541    fn counted(bound: Option<i128>) -> (Func, Interner, Vec<Block>) {
542        let mut names = Interner::new();
543        let params: &[Type] = if bound.is_some() { &[] } else { &[Type::int(32)] };
544        let signature = Signature::new().with_params(params).with_returns(&[Type::int(32)]);
545        let mut func = Func::new(names.intern("f"), signature);
546        let entry = func.create_block();
547        let head = func.create_block();
548        let body = func.create_block();
549        let done = func.create_block();
550        let limit = match bound {
551            Some(value) => Builder::new(&mut func, entry).iconst(Type::int(32), value),
552            None => func.append_param(entry, Type::int(32)),
553        };
554        let i = func.append_param(head, Type::int(32));
555        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
556        Builder::new(&mut func, entry).jump(head, &[zero]);
557        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
558        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
559        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
560        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
561        Builder::new(&mut func, body).jump(head, &[next]);
562        Builder::new(&mut func, done).ret(&[i]);
563        (func, names, vec![entry, head, body, done])
564    }
565
566    /// Whether the header of the only loop leaves it, which is the question section 26.6 is about.
567    fn tests_at_the_top(func: &Func) -> bool {
568        let (cfg, dom, loops) = forest(func);
569        let _ = dom;
570        let id = loops.all().next().expect("there is a loop");
571        let header = loops.header(id);
572        cfg.successors(header).iter().any(|&to| !loops.contains(id, to))
573    }
574
575    #[test]
576    fn a_loop_that_tests_at_the_top_ends_up_testing_at_the_bottom() {
577        let (mut func, mut names, _) = counted(None);
578        assert!(tests_at_the_top(&func), "the shape this pass is for");
579
580        let stats = copied(&mut func, &SPEED);
581        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
582        assert!(!tests_at_the_top(&func), "the header no longer leaves the loop");
583        sound(&func, &mut names);
584    }
585
586    #[test]
587    fn the_value_the_header_defined_is_merged_where_the_two_ways_in_meet() {
588        let (mut func, mut names, blocks) = counted(None);
589        let body = blocks[2];
590        assert!(func[body].params.is_empty(), "the body carries nothing to start with");
591
592        copied(&mut func, &SPEED);
593        assert_eq!(func[body].params.len(), 1, "the counter arrives as a parameter now");
594        assert_eq!(
595            Cfg::new(&func).predecessors(body).len(),
596            2,
597            "one edge from the header and one from the copy"
598        );
599        sound(&func, &mut names);
600    }
601
602    #[test]
603    fn an_entry_test_the_ranges_settle_is_taken_out() {
604        let (mut func, mut names, _) = counted(Some(10));
605
606        let stats = copied(&mut func, &SPEED);
607        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
608        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 1);
609        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 0);
610        sound(&func, &mut names);
611
612        let (cfg, _dom, loops) = forest(&func);
613        let id = loops.all().next().expect("the loop is still there");
614        let entry = func.entry().expect("there is an entry");
615        assert!(cfg.reaches(loops.header(id)), "and it is still reached");
616        assert_eq!(cfg.successors(entry).len(), 1, "the guard in front of it has gone");
617    }
618
619    #[test]
620    fn a_loop_the_ranges_say_never_runs_is_removed() {
621        let (mut func, mut names, blocks) = counted(Some(0));
622
623        let stats = copied(&mut func, &SPEED);
624        assert_eq!(stats.count(Kind::Optimized, super::SKIPPED), 1);
625        sound(&func, &mut names);
626
627        let (_cfg, _dom, loops) = forest(&func);
628        assert_eq!(loops.count(), 0, "there is no loop left");
629        assert!(!func.blocks().any(|block| block == blocks[2]), "and the body has gone with it");
630    }
631
632    #[test]
633    fn a_test_the_ranges_cannot_settle_leaves_the_guard_where_it_is() {
634        let (mut func, _names, _) = counted(None);
635
636        let stats = copied(&mut func, &SPEED);
637        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
638        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 1);
639        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 0);
640    }
641
642    #[test]
643    fn a_second_run_changes_nothing() {
644        let (mut func, mut names, _) = counted(None);
645        copied(&mut func, &SPEED);
646        let again = SPEED.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited());
647        assert_eq!(again.count(Kind::Optimized, super::COPIED), 0, "there is nothing left to do");
648        assert_eq!(again.count(Kind::Note, super::ALREADY), 1, "and it says why");
649        sound(&func, &mut names);
650    }
651
652    #[test]
653    fn a_header_that_writes_to_memory_is_left_alone() {
654        let mut names = Interner::new();
655        let signature = Signature::new().with_params(&[Type::int(32), Type::PTR]).with_returns(&[]);
656        let mut func = Func::new(names.intern("f"), signature);
657        let entry = func.create_block();
658        let head = func.create_block();
659        let body = func.create_block();
660        let done = func.create_block();
661        let limit = func.append_param(entry, Type::int(32));
662        let addr = func.append_param(entry, Type::PTR);
663        let i = func.append_param(head, Type::int(32));
664        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
665        Builder::new(&mut func, entry).jump(head, &[zero]);
666        let access = MemInfo {
667            size: 4,
668            align: 4,
669            order: MemOrder::NotAtomic,
670            tbaa: None,
671            restrict: Restrict::NONE,
672        };
673        Builder::new(&mut func, head).store(i, addr, access, Flags::NONE);
674        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
675        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
676        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
677        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
678        Builder::new(&mut func, body).jump(head, &[next]);
679        Builder::new(&mut func, done).ret(&[]);
680
681        let stats = copied(&mut func, &SPEED);
682        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
683        assert_eq!(stats.count(Kind::Missed, super::EFFECTS), 1);
684        assert!(tests_at_the_top(&func), "the loop is exactly as it was");
685    }
686
687    #[test]
688    fn a_header_larger_than_the_level_allows_is_left_alone() {
689        // Seven instructions in the header, which is over the size budget and well under the
690        // speed one, so the two instances of the pass disagree about the same function.
691        let stats = copied(&mut padded(6), &SIZE);
692        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
693        assert_eq!(stats.count(Kind::Missed, super::TOO_BIG), 1);
694
695        let stats = copied(&mut padded(6), &SPEED);
696        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1, "the speed budget is wider");
697    }
698
699    /// The counted loop with that many more instructions in its header, which do nothing.
700    fn padded(extra: usize) -> Func {
701        let (mut func, _names, blocks) = counted(None);
702        let head = blocks[1];
703        let term = func.terminator(head).expect("the header branches");
704        for _ in 0..extra {
705            let filler = Builder::new(&mut func, head).iconst(Type::int(32), 7);
706            let Def::Result { inst, .. } = func[filler].def else { unreachable!("an iconst") };
707            func.remove_inst(inst);
708            func.insert_before(inst, term);
709        }
710        func
711    }
712
713    #[test]
714    fn fuel_stops_the_copy_where_it_stands() {
715        let (mut func, _names, _) = counted(None);
716        let mut an = Analyses::new();
717        Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
718
719        let stats = SPEED.run(&mut func, &mut an, &mut Fuel::of(0));
720        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
721        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
722        assert!(tests_at_the_top(&func), "and the loop is as it was");
723    }
724
725    #[test]
726    fn a_value_the_header_defines_and_the_code_after_the_loop_reads_is_declined() {
727        // Straight to the copy, so loop-closed form has not been established and the counter the
728        // return names is still the header's own definition. That is the one value this pass will
729        // not merge, and section 26.7's answer is that [`Canon`] has already routed it by the time
730        // the pipeline gets here.
731        let (mut func, _names, _) = counted(None);
732
733        let stats = SPEED.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited());
734        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
735        assert_eq!(stats.count(Kind::Missed, super::ESCAPES), 1);
736        assert!(tests_at_the_top(&func), "and the loop is as it was");
737    }
738
739    #[test]
740    fn a_loop_with_no_preheader_is_declined() {
741        let mut names = Interner::new();
742        let signature = Signature::new().with_params(&[Type::int(1), Type::int(32)]);
743        let mut func = Func::new(names.intern("f"), signature);
744        let entry = func.create_block();
745        let one = func.create_block();
746        let two = func.create_block();
747        let head = func.create_block();
748        let body = func.create_block();
749        let done = func.create_block();
750        let c = func.append_param(entry, Type::int(1));
751        let limit = func.append_param(entry, Type::int(32));
752        let i = func.append_param(head, Type::int(32));
753        Builder::new(&mut func, entry).br_if(c, one, &[], two, &[]);
754        let zero = Builder::new(&mut func, one).iconst(Type::int(32), 0);
755        Builder::new(&mut func, one).jump(head, &[zero]);
756        let start = Builder::new(&mut func, two).iconst(Type::int(32), 1);
757        Builder::new(&mut func, two).jump(head, &[start]);
758        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
759        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
760        Builder::new(&mut func, body).jump(head, &[i]);
761        Builder::new(&mut func, done).ret(&[]);
762
763        let stats = SPEED.run(&mut func, &mut Analyses::new(), &mut Fuel::unlimited());
764        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
765        assert_eq!(stats.count(Kind::Missed, super::NO_PREHEADER), 1);
766
767        // And with one, which is what the pipeline hands it, the same loop is copied.
768        let stats = copied(&mut func, &SPEED);
769        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
770        sound(&func, &mut names);
771    }
772}