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, Start, Type, Value,
76    ValueList,
77};
78
79use crate::cfg::Cfg;
80use crate::dom::Dominators;
81use crate::loops::{LoopId, Loops};
82use crate::range::query::Ranges;
83use crate::{Analyses, Fuel, Pass, Preserved, Stats, prune, simplify_cfg};
84
85const COPIED: &str = "loop header copied in front of the loop so the test is at the bottom";
86const ENTERED: &str = "entry test removed, the value ranges say the loop runs";
87const SKIPPED: &str = "loop removed, the value ranges say the entry test never holds";
88const UNDECIDED: &str = "entry test kept, the value ranges do not settle whether the loop runs";
89const ALREADY: &str = "loop left as it was, it already tests at the bottom";
90const TOO_BIG: &str = "loop header not copied, it is larger than this level allows";
91const EFFECTS: &str = "loop header not copied, something in it may not be repeated";
92const SHAPE: &str = "loop header not copied, its exit is not a two way branch";
93const ESCAPES: &str = "loop header not copied, a value it defines is read outside the loop";
94const NO_PREHEADER: &str = "loop header not copied, the loop has not been canonicalized";
95const NO_FUEL: &str = "loop left as it was, the pass ran out of fuel";
96
97/// Section 26.6's transformation, at one budget.
98///
99/// The budget is a field rather than a constant because the two levels that run this want
100/// different ones, and it comes with the name for the same reason: the two instances are two
101/// entries in [`crate::pass::PASSES`] and a pipeline picks one by naming it.
102#[derive(Debug)]
103pub struct HeaderCopy {
104    /// What a `-f` flag spells.
105    name: &'static str,
106    /// How many instructions a header may hold and still be worth copying, which is
107    /// [`heuristics::LOOP_HEADER_INSNS_FOR_SPEED`] or the size one next to it.
108    budget: u32,
109}
110
111/// The instance `-O1`, `-O2` and `-O3` run, at GCC's budget.
112pub static SPEED: HeaderCopy =
113    HeaderCopy { name: "header-copy", budget: heuristics::LOOP_HEADER_INSNS_FOR_SPEED };
114
115/// The instance `-Os` runs, at section 26.6's smaller one.
116pub static SIZE: HeaderCopy =
117    HeaderCopy { name: "header-copy-small", budget: heuristics::LOOP_HEADER_INSNS_FOR_SIZE };
118
119impl Pass for HeaderCopy {
120    fn name(&self) -> &'static str {
121        self.name
122    }
123
124    fn describe(&self) -> &'static str {
125        "copies a loop header in front of the loop, turning a while into a do-while"
126    }
127
128    fn preserves(&self) -> Preserved {
129        // A block appears, two edges become four, and the body grows a parameter the loop carries.
130        Preserved::NONE
131    }
132
133    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
134        let mut stats = Stats::new();
135        if func.entry().is_none() {
136            return stats;
137        }
138        let mut done = HashSet::new();
139        let mut say = true;
140        let mut dry = false;
141        loop {
142            let jobs = self.plan(func, an, &done, &mut stats, say);
143            say = false;
144            if jobs.is_empty() {
145                break;
146            }
147            let mut copies = Vec::with_capacity(jobs.len());
148            for job in &jobs {
149                if !fuel.take() {
150                    stats.missed(NO_FUEL);
151                    dry = true;
152                    break;
153                }
154                done.insert(job.header);
155                done.insert(job.body);
156                copies.push(apply(func, job));
157                stats.optimized(COPIED);
158            }
159            an.clear();
160            if settle(func, an, &copies, &mut stats) {
161                an.clear();
162            }
163            if dry {
164                break;
165            }
166        }
167        if stats.changed() {
168            // Section 6.5 leaves the stranded blocks to whoever stranded them, and a loop whose
169            // entry test the ranges disproved is a loop nothing reaches any more.
170            simplify_cfg::sweep(func, an, &mut stats);
171        }
172        an.clear();
173        stats
174    }
175}
176
177/// One loop to copy the header of, worked out against the function as it stands.
178#[derive(Debug)]
179struct Job {
180    /// The loop, which is what [`independent`] asks about to decide whether two jobs meet.
181    id: LoopId,
182    /// The block holding the exit test.
183    header: Block,
184    /// The one block outside the loop the header is reached from.
185    entry: Block,
186    /// The header's successor inside the loop, which the copy makes the new header.
187    ///
188    /// The other one, which is where the loop leaves from, is not recorded. The copy branches to
189    /// both by copying the header's own terminator, and nothing after that has a question to ask
190    /// about the one that goes out.
191    body: Block,
192    /// The values the header defines and the rest of the loop reads, which need a merge at
193    /// [`Job::body`] once there are two ways to get there.
194    carried: Vec<Value>,
195    /// Every block in the loop, which is everywhere [`merge`] has to look. [`carried`] has already
196    /// refused a loop whose values are read anywhere else, and every edge into the body comes from
197    /// inside the loop, so the rest of the function has nothing to rewrite.
198    blocks: Vec<Block>,
199}
200
201/// One loop the cheap checks accepted, waiting on the walk that says what it carries.
202#[derive(Debug)]
203struct Candidate {
204    /// The loop.
205    id: LoopId,
206    /// The block holding the exit test.
207    header: Block,
208    /// The one block outside the loop the header is reached from.
209    entry: Block,
210    /// The header's successor inside the loop.
211    body: Block,
212    /// Everything the header defines, which [`carried`] sorts into what the loop reads and what
213    /// nothing does.
214    defined: Vec<Value>,
215}
216
217impl HeaderCopy {
218    /// Every loop worth copying the header of that can be copied without looking again.
219    ///
220    /// A round rather than one at a time. A copy changes the shape of the loop it is made for, so
221    /// the forest this was read out of is wrong about that loop afterwards, and the answer used to
222    /// be to rebuild the graph, the dominator tree and the forest and ask again. On a function with
223    /// sixteen hundred loops that is two thousand rebuilds, which was most of what an optimized
224    /// build of tamnd/rucc#1086's test spent its time on. What the rebuild protects is one loop's
225    /// shape, so this takes the loops whose shapes do not touch and copies all of their headers
226    /// from the one look. [`independent`] is the argument for why that is the same edit.
227    ///
228    /// `say` is false on every call after the first so that a loop this declines is declined once
229    /// rather than once per round.
230    fn plan(
231        &self,
232        func: &Func,
233        an: &mut Analyses,
234        done: &HashSet<Block>,
235        stats: &mut Stats,
236        say: bool,
237    ) -> Vec<Job> {
238        let (cfg, dom, loops) = (an.cfg(func), an.dominators(func), an.loops(func));
239        let mut wanted = Vec::new();
240        for id in loops.all() {
241            let header = loops.header(id);
242            if done.contains(&header) {
243                continue;
244            }
245            match self.consider(func, cfg, loops, id, header) {
246                Ok(candidate) => wanted.push(candidate),
247                Err(why) if say && why == ALREADY => stats.note(ALREADY),
248                Err(why) if say => stats.missed(why),
249                Err(_) => (),
250            }
251        }
252        let jobs = carried(func, dom, loops, wanted, stats, say);
253        let mut jobs = independent(loops, jobs);
254        for job in &mut jobs {
255            job.blocks = loops.blocks(job.id).to_vec();
256        }
257        jobs
258    }
259
260    /// Whether this loop can have its header copied, and why not when it cannot.
261    ///
262    /// Everything here is answered out of the loop itself. What the header defines and who reads it
263    /// is the one question that is about the whole function, and [`carried`] asks it for all the
264    /// candidates at once.
265    fn consider(
266        &self,
267        func: &Func,
268        cfg: &Cfg,
269        loops: &Loops,
270        id: LoopId,
271        header: Block,
272    ) -> Result<Candidate, &'static str> {
273        let leaves = cfg.successors(header).iter().any(|&to| !loops.contains(id, to));
274        if !leaves {
275            // The exit test is somewhere below, which is the shape this pass is trying to reach.
276            // GCC asks the same question the other way round in `do_while_loop_p`.
277            return Err(ALREADY);
278        }
279        let entry = loops.preheader(cfg, id).ok_or(NO_PREHEADER)?;
280        let term = func.terminator(header).ok_or(SHAPE)?;
281        if func[term].opcode != Opcode::BrIf {
282            return Err(SHAPE);
283        }
284        let calls: Vec<BlockCall> = func.successors(term).collect();
285        let [then_call, else_call] = calls[..].try_into().map_err(|_| SHAPE)?;
286        let body = match (loops.contains(id, then_call.block), loops.contains(id, else_call.block))
287        {
288            (true, false) => then_call.block,
289            (false, true) => else_call.block,
290            _ => return Err(SHAPE),
291        };
292        if body == header {
293            return Err(SHAPE);
294        }
295        let insts: Vec<Inst> = func.insts(header).filter(|&inst| inst != term).collect();
296        if insts.len() > self.budget as usize {
297            return Err(TOO_BIG);
298        }
299        for &inst in &insts {
300            if !repeatable(func, inst) {
301                return Err(EFFECTS);
302            }
303        }
304        let mut defined: Vec<Value> = func[header].params.clone();
305        for &inst in &insts {
306            defined.extend(func[inst].results());
307        }
308        Ok(Candidate { id, header, entry, body, defined })
309    }
310}
311
312/// Whether an instruction may stand in a second copy of the block it is in.
313///
314/// Two questions rather than one. [`Opcode::has_effects`] is document 17.1's whitelist and is what
315/// section 26.8 names. The second is about this pass rather than about the program: an instruction
316/// carrying a side table entry is copied here by copying the index, which is right for an
317/// immediate, a symbol and a comparison because those tables are written once and read for ever,
318/// and is not something to assume about a table nobody has checked. So the copy is restricted to
319/// the payloads it has been thought about, and an instruction with any other is declined the same
320/// way one with an effect is.
321pub(crate) fn repeatable(func: &Func, inst: Inst) -> bool {
322    let data = func[inst];
323    if data.opcode.has_effects() || func.carries_mem(inst) {
324        return false;
325    }
326    matches!(
327        data.extra.kind(),
328        ExtraKind::None
329            | ExtraKind::Imm
330            | ExtraKind::Symbol
331            | ExtraKind::IntPred
332            | ExtraKind::FloatPred
333    )
334}
335
336/// Turns the candidates into jobs by working out, for each, which of the values its header defines
337/// the rest of its loop reads.
338///
339/// Those are what the copy owes a merge at the body. A value read outside the loop takes the
340/// candidate out rather than joining the list, because merging it would need a second parameter at
341/// the exit and after [`crate::canon`] there is no such value to merge: loop-closed form has already
342/// routed it.
343///
344/// One walk of the function for every candidate at once. tamnd/rucc#1015 made this one walk per
345/// candidate instead of one per value, and tamnd/rucc#1086 is the same move one level up: a header
346/// defines a handful of values, the function it is in can be very large, and a function with sixteen
347/// hundred loops in it was paying for sixteen hundred walks per round. A value belongs to exactly
348/// one candidate, because two candidates are two loops and two loops have two headers, so one map
349/// from value to candidate is enough to share the walk.
350fn carried(
351    func: &Func,
352    dom: &Dominators,
353    loops: &Loops,
354    wanted: Vec<Candidate>,
355    stats: &mut Stats,
356    say: bool,
357) -> Vec<Job> {
358    let mut watched: HashMap<Value, usize> = HashMap::new();
359    for (which, candidate) in wanted.iter().enumerate() {
360        for &value in &candidate.defined {
361            watched.insert(value, which);
362        }
363    }
364    let mut read: Vec<HashSet<Value>> = vec![HashSet::new(); wanted.len()];
365    let mut escapes = vec![false; wanted.len()];
366    let mut names: Vec<usize> = Vec::new();
367    for block in func.blocks() {
368        names.clear();
369        for inst in func.insts(block) {
370            reads(func, inst, block, &wanted, &watched, &mut read, &mut names);
371        }
372        for &which in &names {
373            let candidate = &wanted[which];
374            if !loops.contains(candidate.id, block) || !dom.dominates(candidate.body, block) {
375                escapes[which] = true;
376            }
377        }
378    }
379    let mut jobs = Vec::new();
380    for (which, candidate) in wanted.into_iter().enumerate() {
381        if escapes[which] {
382            if say {
383                stats.missed(ESCAPES);
384            }
385            continue;
386        }
387        let taken = &read[which];
388        let carried = candidate.defined.into_iter().filter(|value| taken.contains(value)).collect();
389        jobs.push(Job {
390            id: candidate.id,
391            header: candidate.header,
392            entry: candidate.entry,
393            body: candidate.body,
394            carried,
395            blocks: Vec::new(),
396        });
397    }
398    jobs
399}
400
401/// Records every watched value this instruction names, as an operand or on an edge out of it, and
402/// notes which candidates the block named something of.
403///
404/// Which candidates rather than which values, because a block that reads one of these from the wrong
405/// place is an error for that candidate whichever of its values it read. A candidate's own header is
406/// left out on both counts: the header is where these values are defined and reading one there is
407/// neither a carry nor an escape.
408fn reads(
409    func: &Func,
410    inst: Inst,
411    block: Block,
412    wanted: &[Candidate],
413    watched: &HashMap<Value, usize>,
414    read: &mut [HashSet<Value>],
415    names: &mut Vec<usize>,
416) {
417    let mut note = |value: Value| {
418        let Some(&which) = watched.get(&value) else { return };
419        if block == wanted[which].header {
420            return;
421        }
422        read[which].insert(value);
423        if !names.contains(&which) {
424            names.push(which);
425        }
426    };
427    for &value in &func[func[inst].args] {
428        note(value);
429    }
430    for call in func.successors(inst) {
431        for &value in &func[call.args] {
432            note(value);
433        }
434    }
435}
436
437/// The jobs out of a round that may all be applied before the function is looked at again.
438///
439/// Two jobs are safe together when the loops they are about share no block and neither loop holds
440/// the other's preheader. The argument is that a job writes only inside its own loop and to its own
441/// preheader. The copy is a new block put on the edge into the header, and the only block outside
442/// the loop it edits is the preheader, whose one successor is the header by the definition
443/// [`Loops::preheader`] uses. The merge gives the body a parameter and hands a value over on every
444/// edge into the body, and every one of those edges comes from inside the loop, because a natural
445/// loop is entered at its header alone and the body is not the header. The rewrite that follows
446/// reaches only the blocks that read the carried values, and [`carried`] has already taken the job
447/// out if any of those is outside the loop. So two jobs whose loops and preheaders do not meet edit
448/// two disjoint sets of blocks, and applying both from one look at the function is the same function
449/// as applying one, looking again, and applying the other.
450///
451/// Loops that share a block at all are nested, so the loops a taken one rules out are the ones it is
452/// nested in and the ones nested in it.
453fn independent(loops: &Loops, jobs: Vec<Job>) -> Vec<Job> {
454    let mut blocked = vec![false; loops.count()];
455    let mut taken = vec![false; loops.count()];
456    let mut kept: Vec<Job> = Vec::new();
457    for job in jobs {
458        if blocked[job.id.index()] || inside(loops, &taken, job.entry) {
459            continue;
460        }
461        let mut up = Some(job.id);
462        while let Some(id) = up {
463            blocked[id.index()] = true;
464            up = loops.parent(id);
465        }
466        let mut down = vec![job.id];
467        while let Some(id) = down.pop() {
468            blocked[id.index()] = true;
469            down.extend(loops.children(id));
470        }
471        // And no later job may be about a loop this one's preheader sits in.
472        let mut around = loops.innermost(job.entry);
473        while let Some(id) = around {
474            blocked[id.index()] = true;
475            around = loops.parent(id);
476        }
477        taken[job.id.index()] = true;
478        kept.push(job);
479    }
480    kept
481}
482
483/// Whether any loop holding this block has been taken already.
484fn inside(loops: &Loops, taken: &[bool], block: Block) -> bool {
485    let mut walk = loops.innermost(block);
486    while let Some(id) = walk {
487        if taken[id.index()] {
488            return true;
489        }
490        walk = loops.parent(id);
491    }
492    false
493}
494
495/// Makes the copy, puts it on the edge into the loop, and returns it.
496fn apply(func: &mut Func, job: &Job) -> Block {
497    let term = func.terminator(job.header).expect("the plan read this terminator");
498    let entry_term = func.terminator(job.entry).expect("a preheader ends in a jump");
499    // The header's parameters stand for whatever the one edge in hands them, so the copy is
500    // written in terms of those arguments and needs no parameters of its own.
501    let incoming = edge_args(func, entry_term, job.header);
502    let mut map: HashMap<Value, Value> = HashMap::new();
503    for (&param, &arg) in func[job.header].params.clone().iter().zip(&incoming) {
504        map.insert(param, arg);
505    }
506    let copy = func.create_block();
507    let insts: Vec<Inst> = func.insts(job.header).filter(|&inst| inst != term).collect();
508    for inst in insts {
509        clone_into(func, copy, inst, &mut map);
510    }
511    clone_branch(func, copy, term, &map);
512    for at in func.target_list(entry_term).iter() {
513        let call = func[at];
514        if call.block == job.header {
515            func.set_block_call(at, BlockCall { block: copy, args: ValueList::EMPTY, ..call });
516        }
517    }
518    for &value in &job.carried {
519        let arrived = map.get(&value).copied().unwrap_or(value);
520        merge(func, job, copy, value, arrived);
521    }
522    copy
523}
524
525/// The arguments a terminator hands one of its targets.
526fn edge_args(func: &Func, term: Inst, to: Block) -> Vec<Value> {
527    for call in func.successors(term) {
528        if call.block == to {
529            return func[call.args].to_vec();
530        }
531    }
532    Vec::new()
533}
534
535/// Copies one instruction to the end of a block, under the substitution, and records its results.
536pub(crate) fn clone_into(
537    func: &mut Func,
538    into: Block,
539    inst: Inst,
540    map: &mut HashMap<Value, Value>,
541) {
542    let data = func[inst];
543    let args: Vec<Value> =
544        func[data.args].iter().map(|value| map.get(value).copied().unwrap_or(*value)).collect();
545    let types: Vec<Type> = data.results().map(|result| func[result].ty).collect();
546    let span = func.span(inst);
547    let args = func.push_values(&args);
548    let fresh = func.create_inst(InstData { args, ..data }, &types, span);
549    func.append_inst(into, fresh);
550    for (old, new) in data.results().zip(func[fresh].results()) {
551        map.insert(old, new);
552    }
553}
554
555/// Copies the header's two way branch to the end of the copy, under the substitution.
556///
557/// The targets are the header's own. What that means for the graph is that the copy decides,
558/// before the loop, which of the two places the header would have gone control goes to, and the
559/// header is left deciding it for every iteration after the first.
560fn clone_branch(func: &mut Func, into: Block, term: Inst, map: &HashMap<Value, Value>) {
561    let at = |value: &Value| map.get(value).copied().unwrap_or(*value);
562    let cond = at(&func[func[term].args][0]);
563    let calls: Vec<BlockCall> = func.successors(term).collect();
564    let args: Vec<Vec<Value>> =
565        calls.iter().map(|call| func[call.args].iter().map(at).collect()).collect();
566    Builder::new(func, into).br_if(cond, calls[0].block, &args[0], calls[1].block, &args[1]);
567}
568
569/// Gives the body a parameter for a value the header defines, and points the loop at it.
570///
571/// Three kinds of edge arrive at the body once the copy is in place. The header's carries what the
572/// header worked out, which is the value on every iteration after the first. The copy's carries
573/// what the copy worked out, which is the value on the first. Anything else is a block inside the
574/// loop, and what is current there is the parameter itself, which is available because the body
575/// dominates the whole loop the moment the copy is the only way in.
576///
577/// Only the loop and the copy are walked. This used to be two walks of the whole function for
578/// every value carried, and a function with thousands of loops paid for that thousands of times
579/// over: jtckdint's main spent a fifth of its `-O2` build in this pass.
580fn merge(func: &mut Func, job: &Job, copy: Block, value: Value, arrived: Value) {
581    let param = func.append_param(job.body, func[value].ty);
582    for &block in job.blocks.iter().chain([&copy]) {
583        let Some(term) = func.terminator(block) else { continue };
584        let carry = if block == job.header {
585            value
586        } else if block == copy {
587            arrived
588        } else {
589            param
590        };
591        for at in func.target_list(term).iter() {
592            let call = func[at];
593            if call.block != job.body {
594                continue;
595            }
596            let args = func.append_arg(call.args, carry);
597            func.set_block_call(at, BlockCall { args, ..call });
598        }
599    }
600    // The names go where the readers go. The header still has the value and the rest of the loop
601    // has the parameter, so a declaration named on the value is the parameter as well, and a start
602    // anywhere but the header is a place the declaration was given the parameter. Left on the
603    // value, it would say the declaration holds whatever the header is handed once the loop has
604    // turned round, which is the next value rather than this one. tamnd/rucc#1810.
605    for decl in func.value_decls(value).collect::<Vec<u32>>() {
606        func.declare_value(param, decl);
607    }
608    let elsewhere: Vec<Start> = func
609        .value_starts(value)
610        .filter(|&start| {
611            func.start_place(start).map_or(start.block, |(block, _)| block) != job.header
612        })
613        .collect();
614    func.move_starts(value, param, &elsewhere);
615    // Everything the header used to reach reads the parameter now. The header itself does not:
616    // what it hands the body is still its own definition, and that is the edge the parameter was
617    // put there to distinguish.
618    for &block in &job.blocks {
619        if block == job.header {
620            continue;
621        }
622        for inst in func.insts(block).collect::<Vec<_>>() {
623            let swap = |had: Value| if had == value { param } else { had };
624            func.rewrite(func[inst].args, swap);
625            for at in func.target_list(inst).iter() {
626                func.rewrite(func[at].args, swap);
627            }
628        }
629    }
630}
631
632/// Asks the ranges whether the copied tests are settled, and takes out the ones that are.
633///
634/// This is section 26.6's point about the entry condition. A test that always holds leaves a loop
635/// known to run at least once, which is what document 07.5's trip count wanted. One that never
636/// holds leaves the loop unreachable, and taking it out is [`crate::simplify_cfg::sweep`]'s job
637/// rather than this one's.
638///
639/// Every copy the round made is asked from the one set of ranges, and the answers are acted on
640/// afterwards. That is sound because every question is about a different block's terminator and
641/// every answer is a fact about the values arriving there, which taking a branch out somewhere else
642/// cannot make untrue. Asking one at a time would mean a graph and a dominator tree per copy, which
643/// is the cost tamnd/rucc#1086 is about.
644///
645/// Answers whether it moved an edge, which the caller needs because the analyses this built are the
646/// ones the next round wants and they are only stale if a branch came out. The ranges settle the
647/// test on a minority of the loops here and the graph is the size of the function, so the rounds
648/// where nothing happens used to pay for a rebuild that changed nothing. tamnd/rucc#1045.
649fn settle(func: &mut Func, an: &mut Analyses, copies: &[Block], stats: &mut Stats) -> bool {
650    let mut out: Vec<(Inst, BlockCall, bool)> = Vec::new();
651    {
652        let cfg = an.cfg(func);
653        let dom = an.dominators(func);
654        let mut ranges = Ranges::new(func, cfg, dom);
655        for &copy in copies {
656            let Some(term) = func.terminator(copy) else { continue };
657            let cond = func[func[term].args][0];
658            let Some(taken) = prune::settled(func, &mut ranges, copy, cond) else {
659                stats.missed(UNDECIDED);
660                continue;
661            };
662            let calls: Vec<BlockCall> = func.successors(term).collect();
663            out.push((term, if taken { calls[0] } else { calls[1] }, taken));
664        }
665    }
666    if out.is_empty() {
667        return false;
668    }
669    for (term, call, taken) in out {
670        simplify_cfg::jump_to(func, term, call);
671        stats.optimized(if taken { ENTERED } else { SKIPPED });
672    }
673    true
674}
675
676#[cfg(test)]
677mod tests {
678    use rucc_base::Interner;
679    use rucc_ir::{
680        Block, Builder, Def, Flags, Func, IntPred, MemInfo, MemOrder, Module, Opcode, Restrict,
681        Signature, Start, Type, verify_func,
682    };
683    use rucc_target::{TargetInfo, Triple};
684
685    use super::{HeaderCopy, SIZE, SPEED};
686    use crate::canon::Canon;
687    use crate::cfg::Cfg;
688    use crate::dom::Dominators;
689    use crate::loops::Loops;
690    use crate::stats::Kind;
691    use crate::{Fuel, Pass, Stats};
692
693    /// Canonicalizes and then copies, with as much fuel as both want.
694    ///
695    /// Both, because the pass is written against the shape [`Canon`] leaves and running it over
696    /// anything else is a test of a situation the pipeline does not produce. Section 26.7 puts the
697    /// two next to each other in that order and so does this.
698    fn copied(func: &mut Func, pass: &HeaderCopy) -> Stats {
699        let mut an = crate::machine::fixtures::analyses();
700        Canon.run(func, &mut an, &mut Fuel::unlimited());
701        pass.run(func, &mut an, &mut Fuel::unlimited())
702    }
703
704    /// The forest of the function as it is now.
705    fn forest(func: &Func) -> (Cfg, Dominators, Loops) {
706        let cfg = Cfg::new(func);
707        let dom = Dominators::new(&cfg);
708        let loops = Loops::new(&cfg, &dom);
709        (cfg, dom, loops)
710    }
711
712    /// Insists the function is one the rest of the compiler may believe.
713    ///
714    /// This is where most of the strength of these tests is. The copy gives the loop a second way
715    /// in, which is exactly the edit that breaks a definition's dominance over its uses, and the
716    /// verifier is what says whether the merge the pass wrote is the merge the graph needed.
717    fn sound(func: &Func, names: &mut Interner) {
718        let target = TargetInfo::new("x86_64-unknown-linux-gnu".parse::<Triple>().unwrap());
719        let module = Module::new(names.intern("t.c"), &target);
720        if let Err(errors) = verify_func(&module, func, names) {
721            panic!("{errors:#?}");
722        }
723    }
724
725    /// A counted loop that tests at the top, which is what `while (i < n)` lowers to.
726    ///
727    /// ```text
728    /// entry: i0 = 0; jump head(i0)
729    /// head(i): t = i < n; br t -> body, done
730    /// body: next = i + 1; jump head(next)
731    /// done: ret i
732    /// ```
733    ///
734    /// `bound` is the limit as a constant, or nothing for a limit the function was handed and
735    /// which the ranges therefore cannot settle.
736    fn counted(bound: Option<i128>) -> (Func, Interner, Vec<Block>) {
737        let mut names = Interner::new();
738        let params: &[Type] = if bound.is_some() { &[] } else { &[Type::int(32)] };
739        let signature = Signature::new().with_params(params).with_returns(&[Type::int(32)]);
740        let mut func = Func::new(names.intern("f"), signature);
741        let entry = func.create_block();
742        let head = func.create_block();
743        let body = func.create_block();
744        let done = func.create_block();
745        let limit = match bound {
746            Some(value) => Builder::new(&mut func, entry).iconst(Type::int(32), value),
747            None => func.append_param(entry, Type::int(32)),
748        };
749        let i = func.append_param(head, Type::int(32));
750        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
751        Builder::new(&mut func, entry).jump(head, &[zero]);
752        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
753        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
754        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
755        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
756        Builder::new(&mut func, body).jump(head, &[next]);
757        Builder::new(&mut func, done).ret(&[i]);
758        (func, names, vec![entry, head, body, done])
759    }
760
761    /// Whether the header of the only loop leaves it, which is the question section 26.6 is about.
762    fn tests_at_the_top(func: &Func) -> bool {
763        let (cfg, dom, loops) = forest(func);
764        let _ = dom;
765        let id = loops.all().next().expect("there is a loop");
766        let header = loops.header(id);
767        cfg.successors(header).iter().any(|&to| !loops.contains(id, to))
768    }
769
770    #[test]
771    fn a_loop_that_tests_at_the_top_ends_up_testing_at_the_bottom() {
772        let (mut func, mut names, _) = counted(None);
773        assert!(tests_at_the_top(&func), "the shape this pass is for");
774
775        let stats = copied(&mut func, &SPEED);
776        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
777        assert!(!tests_at_the_top(&func), "the header no longer leaves the loop");
778        sound(&func, &mut names);
779    }
780
781    #[test]
782    fn the_value_the_header_defined_is_merged_where_the_two_ways_in_meet() {
783        let (mut func, mut names, blocks) = counted(None);
784        let body = blocks[2];
785        assert!(func[body].params.is_empty(), "the body carries nothing to start with");
786
787        copied(&mut func, &SPEED);
788        assert_eq!(func[body].params.len(), 1, "the counter arrives as a parameter now");
789        assert_eq!(
790            Cfg::new(&func).predecessors(body).len(),
791            2,
792            "one edge from the header and one from the copy"
793        );
794        sound(&func, &mut names);
795    }
796
797    /// `int j = i;` at the top of the body is a place `j` was given the counter, and once the body
798    /// has a parameter of its own that place is where `j` was given the parameter. The header is
799    /// handed the next value when the loop comes round, so a start left on its value would follow
800    /// that one instead.
801    #[test]
802    fn a_name_on_the_value_the_body_now_takes_as_a_parameter_goes_with_it() {
803        let (mut func, mut names, blocks) = counted(None);
804        let (head, body) = (blocks[1], blocks[2]);
805        let i = func[head].params[0];
806        let test = func.insts(head).next();
807        func.declare_value(i, 3);
808        func.declare_value_from(i, Start { decl: 4, block: body, after: None });
809        func.declare_value_from(i, Start { decl: 5, block: head, after: test });
810
811        copied(&mut func, &SPEED);
812        let param = func[body].params[0];
813        assert_eq!(func.value_decls(param).collect::<Vec<u32>>(), vec![3]);
814        assert_eq!(func.value_decls(i).collect::<Vec<u32>>(), vec![3], "still the header's");
815        let decls = |value| func.value_starts(value).map(|start| start.decl).collect::<Vec<u32>>();
816        assert_eq!(decls(param), vec![4], "the body's start is on the body's parameter");
817        assert_eq!(decls(i), vec![5], "and the header's stays on the header's value");
818        sound(&func, &mut names);
819    }
820
821    #[test]
822    fn an_entry_test_the_ranges_settle_is_taken_out() {
823        let (mut func, mut names, _) = counted(Some(10));
824
825        let stats = copied(&mut func, &SPEED);
826        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
827        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 1);
828        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 0);
829        sound(&func, &mut names);
830
831        let (cfg, _dom, loops) = forest(&func);
832        let id = loops.all().next().expect("the loop is still there");
833        let entry = func.entry().expect("there is an entry");
834        assert!(cfg.reaches(loops.header(id)), "and it is still reached");
835        assert_eq!(cfg.successors(entry).len(), 1, "the guard in front of it has gone");
836    }
837
838    #[test]
839    fn a_loop_the_ranges_say_never_runs_is_removed() {
840        let (mut func, mut names, blocks) = counted(Some(0));
841
842        let stats = copied(&mut func, &SPEED);
843        assert_eq!(stats.count(Kind::Optimized, super::SKIPPED), 1);
844        sound(&func, &mut names);
845
846        let (_cfg, _dom, loops) = forest(&func);
847        assert_eq!(loops.count(), 0, "there is no loop left");
848        assert!(!func.blocks().any(|block| block == blocks[2]), "and the body has gone with it");
849    }
850
851    #[test]
852    fn a_test_the_ranges_cannot_settle_leaves_the_guard_where_it_is() {
853        let (mut func, _names, _) = counted(None);
854
855        let stats = copied(&mut func, &SPEED);
856        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
857        assert_eq!(stats.count(Kind::Missed, super::UNDECIDED), 1);
858        assert_eq!(stats.count(Kind::Optimized, super::ENTERED), 0);
859    }
860
861    #[test]
862    fn a_second_run_changes_nothing() {
863        let (mut func, mut names, _) = counted(None);
864        copied(&mut func, &SPEED);
865        let again =
866            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
867        assert_eq!(again.count(Kind::Optimized, super::COPIED), 0, "there is nothing left to do");
868        assert_eq!(again.count(Kind::Note, super::ALREADY), 1, "and it says why");
869        sound(&func, &mut names);
870    }
871
872    #[test]
873    fn a_header_that_writes_to_memory_is_left_alone() {
874        let mut names = Interner::new();
875        let signature = Signature::new().with_params(&[Type::int(32), Type::PTR]).with_returns(&[]);
876        let mut func = Func::new(names.intern("f"), signature);
877        let entry = func.create_block();
878        let head = func.create_block();
879        let body = func.create_block();
880        let done = func.create_block();
881        let limit = func.append_param(entry, Type::int(32));
882        let addr = func.append_param(entry, Type::PTR);
883        let i = func.append_param(head, Type::int(32));
884        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
885        Builder::new(&mut func, entry).jump(head, &[zero]);
886        let access = MemInfo {
887            size: 4,
888            align: 4,
889            order: MemOrder::NotAtomic,
890            tbaa: None,
891            owns: 0,
892            restrict: Restrict::NONE,
893        };
894        Builder::new(&mut func, head).store(i, addr, access, Flags::NONE);
895        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
896        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
897        let one = Builder::new(&mut func, body).iconst(Type::int(32), 1);
898        let next = Builder::new(&mut func, body).binary(Opcode::Add, i, one, Flags::NONE);
899        Builder::new(&mut func, body).jump(head, &[next]);
900        Builder::new(&mut func, done).ret(&[]);
901
902        let stats = copied(&mut func, &SPEED);
903        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
904        assert_eq!(stats.count(Kind::Missed, super::EFFECTS), 1);
905        assert!(tests_at_the_top(&func), "the loop is exactly as it was");
906    }
907
908    #[test]
909    fn a_header_larger_than_the_level_allows_is_left_alone() {
910        // Seven instructions in the header, which is over the size budget and well under the
911        // speed one, so the two instances of the pass disagree about the same function.
912        let stats = copied(&mut padded(6), &SIZE);
913        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
914        assert_eq!(stats.count(Kind::Missed, super::TOO_BIG), 1);
915
916        let stats = copied(&mut padded(6), &SPEED);
917        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1, "the speed budget is wider");
918    }
919
920    /// The counted loop with that many more instructions in its header, which do nothing.
921    fn padded(extra: usize) -> Func {
922        let (mut func, _names, blocks) = counted(None);
923        let head = blocks[1];
924        let term = func.terminator(head).expect("the header branches");
925        for _ in 0..extra {
926            let filler = Builder::new(&mut func, head).iconst(Type::int(32), 7);
927            let Def::Result { inst, .. } = func[filler].def else { unreachable!("an iconst") };
928            func.remove_inst(inst);
929            func.insert_before(inst, term);
930        }
931        func
932    }
933
934    #[test]
935    fn fuel_stops_the_copy_where_it_stands() {
936        let (mut func, _names, _) = counted(None);
937        let mut an = crate::machine::fixtures::analyses();
938        Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
939
940        let stats = SPEED.run(&mut func, &mut an, &mut Fuel::of(0));
941        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
942        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
943        assert!(tests_at_the_top(&func), "and the loop is as it was");
944    }
945
946    #[test]
947    fn a_value_the_header_defines_and_the_code_after_the_loop_reads_is_declined() {
948        // Straight to the copy, so loop-closed form has not been established and the counter the
949        // return names is still the header's own definition. That is the one value this pass will
950        // not merge, and section 26.7's answer is that [`Canon`] has already routed it by the time
951        // the pipeline gets here.
952        let (mut func, _names, _) = counted(None);
953
954        let stats =
955            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
956        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
957        assert_eq!(stats.count(Kind::Missed, super::ESCAPES), 1);
958        assert!(tests_at_the_top(&func), "and the loop is as it was");
959    }
960
961    #[test]
962    fn a_loop_with_no_preheader_is_declined() {
963        let mut names = Interner::new();
964        let signature = Signature::new().with_params(&[Type::int(1), Type::int(32)]);
965        let mut func = Func::new(names.intern("f"), signature);
966        let entry = func.create_block();
967        let one = func.create_block();
968        let two = func.create_block();
969        let head = func.create_block();
970        let body = func.create_block();
971        let done = func.create_block();
972        let c = func.append_param(entry, Type::int(1));
973        let limit = func.append_param(entry, Type::int(32));
974        let i = func.append_param(head, Type::int(32));
975        Builder::new(&mut func, entry).br_if(c, one, &[], two, &[]);
976        let zero = Builder::new(&mut func, one).iconst(Type::int(32), 0);
977        Builder::new(&mut func, one).jump(head, &[zero]);
978        let start = Builder::new(&mut func, two).iconst(Type::int(32), 1);
979        Builder::new(&mut func, two).jump(head, &[start]);
980        let test = Builder::new(&mut func, head).icmp(IntPred::Slt, i, limit);
981        Builder::new(&mut func, head).br_if(test, body, &[], done, &[]);
982        Builder::new(&mut func, body).jump(head, &[i]);
983        Builder::new(&mut func, done).ret(&[]);
984
985        let stats =
986            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::unlimited());
987        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 0);
988        assert_eq!(stats.count(Kind::Missed, super::NO_PREHEADER), 1);
989
990        // And with one, which is what the pipeline hands it, the same loop is copied.
991        let stats = copied(&mut func, &SPEED);
992        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
993        sound(&func, &mut names);
994    }
995
996    /// Two counted loops one after the other, which share no block and no preheader.
997    ///
998    /// ```text
999    /// entry(n): jump one(0)
1000    /// one(i):  t = i < n; br t -> up, mid
1001    /// up:      i2 = i + 1; jump one(i2)
1002    /// mid:     jump two(0)
1003    /// two(j):  u = j < n; br u -> down, done
1004    /// down:    j2 = j + 1; jump two(j2)
1005    /// done:    ret
1006    /// ```
1007    fn side_by_side() -> (Func, Interner, Vec<Block>) {
1008        let mut names = Interner::new();
1009        let signature = Signature::new().with_params(&[Type::int(32)]);
1010        let mut func = Func::new(names.intern("f"), signature);
1011        let entry = func.create_block();
1012        let one = func.create_block();
1013        let up = func.create_block();
1014        let mid = func.create_block();
1015        let two = func.create_block();
1016        let down = func.create_block();
1017        let done = func.create_block();
1018        let n = func.append_param(entry, Type::int(32));
1019        let i = func.append_param(one, Type::int(32));
1020        let j = func.append_param(two, Type::int(32));
1021        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
1022        Builder::new(&mut func, entry).jump(one, &[zero]);
1023        let t = Builder::new(&mut func, one).icmp(IntPred::Slt, i, n);
1024        Builder::new(&mut func, one).br_if(t, up, &[], mid, &[]);
1025        let step = Builder::new(&mut func, up).iconst(Type::int(32), 1);
1026        let next = Builder::new(&mut func, up).binary(Opcode::Add, i, step, Flags::NONE);
1027        Builder::new(&mut func, up).jump(one, &[next]);
1028        let start = Builder::new(&mut func, mid).iconst(Type::int(32), 0);
1029        Builder::new(&mut func, mid).jump(two, &[start]);
1030        let u = Builder::new(&mut func, two).icmp(IntPred::Slt, j, n);
1031        Builder::new(&mut func, two).br_if(u, down, &[], done, &[]);
1032        let stride = Builder::new(&mut func, down).iconst(Type::int(32), 1);
1033        let after = Builder::new(&mut func, down).binary(Opcode::Add, j, stride, Flags::NONE);
1034        Builder::new(&mut func, down).jump(two, &[after]);
1035        Builder::new(&mut func, done).ret(&[]);
1036        (func, names, vec![up, down])
1037    }
1038
1039    #[test]
1040    fn two_loops_that_do_not_meet_are_both_copied() {
1041        let (mut func, mut names, bodies) = side_by_side();
1042
1043        let stats = copied(&mut func, &SPEED);
1044        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 2);
1045        sound(&func, &mut names);
1046
1047        let (_cfg, _dom, loops) = forest(&func);
1048        assert_eq!(loops.count(), 2, "both loops are still loops");
1049        for id in loops.all() {
1050            let header = loops.header(id);
1051            assert!(
1052                !Cfg::new(&func).successors(header).iter().any(|&to| !loops.contains(id, to)),
1053                "and neither of them tests at the top any more"
1054            );
1055        }
1056        for body in bodies {
1057            assert_eq!(func[body].params.len(), 1, "each body carries its own counter");
1058        }
1059    }
1060
1061    /// A counted loop with a counted loop inside it, where the inner preheader is an outer block.
1062    ///
1063    /// ```text
1064    /// entry(n): jump outer(0)
1065    /// outer(i): t = i < n; br t -> ahead, done
1066    /// ahead:    jump inner(0)
1067    /// inner(j): u = j < n; br u -> under, latch
1068    /// under:    j2 = j + 1; jump inner(j2)
1069    /// latch:    i2 = i + 1; jump outer(i2)
1070    /// done:     ret
1071    /// ```
1072    fn nested() -> (Func, Interner) {
1073        let mut names = Interner::new();
1074        let signature = Signature::new().with_params(&[Type::int(32)]);
1075        let mut func = Func::new(names.intern("f"), signature);
1076        let entry = func.create_block();
1077        let outer = func.create_block();
1078        let ahead = func.create_block();
1079        let inner = func.create_block();
1080        let under = func.create_block();
1081        let latch = func.create_block();
1082        let done = func.create_block();
1083        let n = func.append_param(entry, Type::int(32));
1084        let i = func.append_param(outer, Type::int(32));
1085        let j = func.append_param(inner, Type::int(32));
1086        let zero = Builder::new(&mut func, entry).iconst(Type::int(32), 0);
1087        Builder::new(&mut func, entry).jump(outer, &[zero]);
1088        let t = Builder::new(&mut func, outer).icmp(IntPred::Slt, i, n);
1089        Builder::new(&mut func, outer).br_if(t, ahead, &[], done, &[]);
1090        let start = Builder::new(&mut func, ahead).iconst(Type::int(32), 0);
1091        Builder::new(&mut func, ahead).jump(inner, &[start]);
1092        let u = Builder::new(&mut func, inner).icmp(IntPred::Slt, j, n);
1093        Builder::new(&mut func, inner).br_if(u, under, &[], latch, &[]);
1094        let stride = Builder::new(&mut func, under).iconst(Type::int(32), 1);
1095        let after = Builder::new(&mut func, under).binary(Opcode::Add, j, stride, Flags::NONE);
1096        Builder::new(&mut func, under).jump(inner, &[after]);
1097        let step = Builder::new(&mut func, latch).iconst(Type::int(32), 1);
1098        let next = Builder::new(&mut func, latch).binary(Opcode::Add, i, step, Flags::NONE);
1099        Builder::new(&mut func, latch).jump(outer, &[next]);
1100        Builder::new(&mut func, done).ret(&[]);
1101        (func, names)
1102    }
1103
1104    #[test]
1105    fn a_loop_and_the_loop_inside_it_are_copied_one_round_apart() {
1106        // The two share every block the inner one has, and the inner one's preheader is a block of
1107        // the outer one, so a round may hold at most one of them. Both are still copied, and the
1108        // point of the test is that the second is planned against a function the first has already
1109        // changed rather than against the plan the first was made from.
1110        let (mut func, mut names) = nested();
1111
1112        let stats = copied(&mut func, &SPEED);
1113        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 2);
1114        sound(&func, &mut names);
1115
1116        let (cfg, _dom, loops) = forest(&func);
1117        assert_eq!(loops.count(), 2, "both loops survived the copy");
1118        for id in loops.all() {
1119            let header = loops.header(id);
1120            assert!(
1121                !cfg.successors(header).iter().any(|&to| !loops.contains(id, to)),
1122                "and both test at the bottom now"
1123            );
1124        }
1125    }
1126
1127    #[test]
1128    fn a_round_stops_where_the_fuel_does() {
1129        // Two loops a round may hold together, and one unit of fuel. The first is copied and the
1130        // second is left for a run with more, which is what taking fuel per job rather than per
1131        // round means.
1132        let (mut func, mut names) = {
1133            let (mut func, names, _) = side_by_side();
1134            let mut an = crate::machine::fixtures::analyses();
1135            Canon.run(&mut func, &mut an, &mut Fuel::unlimited());
1136            (func, names)
1137        };
1138
1139        let stats =
1140            SPEED.run(&mut func, &mut crate::machine::fixtures::analyses(), &mut Fuel::of(1));
1141        assert_eq!(stats.count(Kind::Optimized, super::COPIED), 1);
1142        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
1143        sound(&func, &mut names);
1144    }
1145}