Skip to main content

rucc_opt/
loops.rs

1//! The loop forest: what loops there are, how they nest, and what is in a cycle that is not one.
2//!
3//! Design: `spec/optimizer/07-loops-and-scev.md` sections 7.1 through 7.3 and 7.6.
4//!
5//! A back edge is an edge whose head dominates its tail, the natural loop of a back edge is the
6//! set of blocks that reach the tail without passing through the head, and natural loops with
7//! different headers are either disjoint or one contains the other. That is the textbook
8//! construction and it needs the dominator tree and nothing else.
9//!
10//! What is built here is the same set of loops by a different route, because the textbook route
11//! answers three questions with three walks and this one answers them with one. Take the
12//! strongly connected components of the graph. A component with a cycle in it is either a
13//! natural loop or an irreducible region, and which one it is comes down to a single question:
14//! whether the nearest block dominating all of it is one of its own blocks. If it is, that block
15//! is the header, every way into the component goes through it, and the component is exactly the
16//! natural loop of the back edges arriving at it. If it is not, the component has two ways in
17//! and is what `goto` into a loop body produces. Taking the header out and doing it again finds
18//! what is nested inside, so the nesting falls out of the recursion rather than being worked out
19//! afterwards by comparing block sets.
20//!
21//! Section 7.1's other decision is here by omission: two back edges to one header are two
22//! latches of one loop and this does not try to guess whether one of them is really an inner
23//! loop's. GCC guesses, from the profile if it has one and from induction variables if it does
24//! not. rucc requires a single latch as a canonical form instead, and the canonicalizer in
25//! document 26 creates one, which is an edit rather than a guess and is always right.
26
27use rucc_ir::{Block, Def, Func, Value};
28
29use crate::cfg::Cfg;
30use crate::dom::Dominators;
31
32/// One loop, by number.
33///
34/// A handle rather than a reference, because a loop's parent and children are loops and a tree
35/// of references to each other is not a thing to hand a pass.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct LoopId(u32);
38
39impl LoopId {
40    /// Its number, for indexing something the caller keeps alongside.
41    #[must_use]
42    pub fn index(self) -> usize {
43        self.0 as usize
44    }
45}
46
47/// An edge that leaves a loop.
48#[derive(Clone, Copy, Debug, PartialEq, Eq)]
49pub struct Exit {
50    /// The block inside the loop the edge leaves from.
51    pub from: Block,
52    /// The block outside the loop it arrives at.
53    pub to: Block,
54}
55
56/// What is known about one loop.
57#[derive(Clone, Debug, PartialEq, Eq)]
58struct LoopData {
59    header: Block,
60    /// Every block in the loop, including the blocks of the loops nested in it.
61    blocks: Vec<Block>,
62    /// The blocks with a back edge to the header. Canonical form wants exactly one.
63    latches: Vec<Block>,
64    /// Every edge out of the loop, cached because every loop pass asks and the alternative is
65    /// a walk of the body each time.
66    exits: Vec<Exit>,
67    parent: Option<LoopId>,
68    children: Vec<LoopId>,
69    depth: u32,
70}
71
72/// The loops of a function, nested.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct Loops {
75    loops: Vec<LoopData>,
76    /// The innermost loop holding each block, indexed by block number.
77    innermost: Vec<Option<LoopId>>,
78    /// The loops nothing encloses.
79    roots: Vec<LoopId>,
80    /// Blocks in a cycle that is not a natural loop, in block order.
81    irreducible: Vec<Block>,
82}
83
84impl Loops {
85    /// Finds the loops.
86    ///
87    /// Linear in the graph per level of nesting, so linear with a small constant on the code
88    /// people write. Section 7.8 says this is not the part of loop analysis to worry about.
89    #[must_use]
90    pub fn new(cfg: &Cfg, doms: &Dominators) -> Self {
91        let mut build = Build::new(cfg, doms);
92        let region: Vec<Block> = cfg.postorder().to_vec();
93        build.region(&region, None);
94        build.finish()
95    }
96
97    /// How many loops there are, counting nested ones.
98    #[must_use]
99    pub fn count(&self) -> usize {
100        self.loops.len()
101    }
102
103    /// Every loop, outer before inner.
104    pub fn all(&self) -> impl Iterator<Item = LoopId> + use<> {
105        (0..self.loops.len()).map(|index| LoopId(index as u32))
106    }
107
108    /// The loops nothing encloses.
109    #[must_use]
110    pub fn roots(&self) -> &[LoopId] {
111        &self.roots
112    }
113
114    /// The one block every path into the loop arrives at.
115    #[must_use]
116    pub fn header(&self, id: LoopId) -> Block {
117        self.loops[id.index()].header
118    }
119
120    /// Every block in the loop, including the blocks of the loops nested in it.
121    #[must_use]
122    pub fn blocks(&self, id: LoopId) -> &[Block] {
123        &self.loops[id.index()].blocks
124    }
125
126    /// The blocks with an edge back to the header.
127    ///
128    /// Canonical form wants exactly one of these, and section 7.3 says why: it is what makes
129    /// "the last thing that happens in an iteration" a place rather than a question.
130    #[must_use]
131    pub fn latches(&self, id: LoopId) -> &[Block] {
132        &self.loops[id.index()].latches
133    }
134
135    /// Every edge that leaves the loop.
136    #[must_use]
137    pub fn exits(&self, id: LoopId) -> &[Exit] {
138        &self.loops[id.index()].exits
139    }
140
141    /// The loop this one is nested in.
142    #[must_use]
143    pub fn parent(&self, id: LoopId) -> Option<LoopId> {
144        self.loops[id.index()].parent
145    }
146
147    /// The loops nested directly in this one.
148    #[must_use]
149    pub fn children(&self, id: LoopId) -> &[LoopId] {
150        &self.loops[id.index()].children
151    }
152
153    /// How many loops enclose this one, counting from zero for one nothing encloses.
154    #[must_use]
155    pub fn depth(&self, id: LoopId) -> u32 {
156        self.loops[id.index()].depth
157    }
158
159    /// The innermost loop holding this block, if any holds it.
160    #[must_use]
161    pub fn innermost(&self, block: Block) -> Option<LoopId> {
162        *self.innermost.get(block.index()).unwrap_or(&None)
163    }
164
165    /// Whether the block is in this loop or in a loop nested in it.
166    #[must_use]
167    pub fn contains(&self, id: LoopId, block: Block) -> bool {
168        let mut walk = self.innermost(block);
169        while let Some(inner) = walk {
170            if inner == id {
171                return true;
172            }
173            walk = self.parent(inner);
174        }
175        false
176    }
177
178    /// The block outside the loop that every path in comes through, when there is exactly one
179    /// such block and its only successor is the header.
180    ///
181    /// This is the preheader of section 7.3, which is where loop invariant code motion puts
182    /// what it hoists. `None` means the loop is not in canonical form yet, and the answer is to
183    /// run the canonicalizer rather than to split an edge here.
184    #[must_use]
185    pub fn preheader(&self, cfg: &Cfg, id: LoopId) -> Option<Block> {
186        let header = self.header(id);
187        let mut outside = cfg.predecessors(header).iter().filter(|&&pred| !self.contains(id, pred));
188        let candidate = *outside.next()?;
189        if outside.next().is_some() || cfg.successors(candidate).len() != 1 {
190            return None;
191        }
192        Some(candidate)
193    }
194
195    /// Blocks that are in a cycle and in no natural loop.
196    ///
197    /// These are the irreducible regions of section 7.1, which is what `goto` into a loop body
198    /// and some state machines written as a `switch` inside a `for` produce. rucc does not turn
199    /// them into reducible ones: node splitting can blow up code size exponentially and the
200    /// payoff is a handful of loop optimizations applying to code that is rare and usually
201    /// cold. GCC does not do it either. The analysis reports them, the loop passes decline
202    /// them, and the value level passes are unaffected because they only need dominance.
203    ///
204    /// The search stops at an irreducible region rather than looking inside it, so a back edge
205    /// buried in one does not become a loop even when its head dominates its tail. That loses a
206    /// self loop inside a two entry region and nothing else anyone has produced, and it loses
207    /// nothing in practice because a loop pass skips those blocks either way. What it buys is
208    /// that "in a loop" and "irreducible" are decided by one walk, so they cannot disagree.
209    #[must_use]
210    pub fn irreducible(&self) -> &[Block] {
211        &self.irreducible
212    }
213
214    /// Whether this block is in a cycle that is not a natural loop.
215    #[must_use]
216    pub fn is_irreducible(&self, block: Block) -> bool {
217        self.irreducible.binary_search_by_key(&block.index(), |b| b.index()).is_ok()
218    }
219
220    /// Whether the value is the same on every iteration of this loop.
221    ///
222    /// The second of the four questions section 7.6 says this analysis answers. A value is
223    /// invariant when what defines it is outside the loop, which covers the function's
224    /// arguments and everything computed before the loop was entered. A value defined inside
225    /// can still be invariant, when everything it reads is, and answering that is a walk this
226    /// deliberately does not do: the caller that wants it is loop invariant code motion, which
227    /// has to walk the body in order anyway and gets the transitive answer for free as it goes.
228    #[must_use]
229    pub fn is_invariant(&self, func: &Func, id: LoopId, value: Value) -> bool {
230        let block = match func[value].def {
231            Def::Result { inst, .. } => func.block_of(inst),
232            Def::Param { block, .. } => Some(block),
233        };
234        // A value whose defining instruction is in no block was removed, and nothing should be
235        // asking about it. Saying it is not invariant is the answer that stops a caller acting
236        // on it.
237        block.is_some_and(|block| !self.contains(id, block))
238    }
239
240    /// What is wrong with the forest, which on a forest this built is nothing.
241    ///
242    /// Section 7.2 asks for the equivalent of GCC's `verify_loop_structure`, and it is a
243    /// separate thing from document 04.3's check that a pass did not lie about what it
244    /// preserved. That one catches a pass claiming to have kept the forest when it changed the
245    /// graph under it. This one catches a pass that rebuilt the forest into something malformed,
246    /// which is a different mistake and is the one that follows from an edit near a header.
247    ///
248    /// This does not check canonical form. A preheader, a single latch, loop-closed SSA and a
249    /// dedicated exit are what document 26's canonicalizer establishes before the loop pipeline
250    /// runs, and a forest read off an arbitrary function has none of them.
251    #[must_use]
252    pub fn problems(&self, cfg: &Cfg, doms: &Dominators) -> Vec<String> {
253        let mut found = Vec::new();
254        for id in self.all() {
255            let header = self.header(id);
256            for &block in self.blocks(id) {
257                if !doms.dominates(header, block) {
258                    found.push(format!("loop {} holds a block its header does not dominate", id.0));
259                    break;
260                }
261            }
262            if self.latches(id).is_empty() {
263                found.push(format!("loop {} has no way back to its header", id.0));
264            }
265            for &latch in self.latches(id) {
266                if !cfg.successors(latch).contains(&header) {
267                    found.push(format!("loop {} has a latch that does not reach its header", id.0));
268                    break;
269                }
270            }
271            for exit in self.exits(id) {
272                if !self.contains(id, exit.from) || self.contains(id, exit.to) {
273                    found.push(format!("loop {} has an exit that does not leave it", id.0));
274                    break;
275                }
276            }
277            if let Some(parent) = self.parent(id) {
278                if !self.blocks(id).iter().all(|&block| self.contains(parent, block)) {
279                    found.push(format!("loop {} is not inside the loop it says it is in", id.0));
280                }
281                if self.depth(id) != self.depth(parent) + 1 {
282                    found.push(format!("loop {} is not one deeper than its parent", id.0));
283                }
284            } else if self.depth(id) != 0 {
285                found.push(format!("loop {} has a depth and nothing to be deep inside", id.0));
286            }
287        }
288        found
289    }
290}
291
292/// The state of one construction, which recurses into what it finds.
293struct Build<'a> {
294    cfg: &'a Cfg,
295    doms: &'a Dominators,
296    loops: Vec<LoopData>,
297    innermost: Vec<Option<LoopId>>,
298    roots: Vec<LoopId>,
299    irreducible: Vec<Block>,
300    /// Whether each block is in the region being looked at, so an edge out of it is skipped in
301    /// O(1) rather than by searching the region.
302    inside: Vec<bool>,
303    /// Tarjan's numbering, and the lowest one reachable from each block.
304    index: Vec<u32>,
305    low: Vec<u32>,
306    stacked: Vec<bool>,
307}
308
309/// A block Tarjan's walk has not numbered yet.
310const UNVISITED: u32 = u32::MAX;
311
312impl<'a> Build<'a> {
313    fn new(cfg: &'a Cfg, doms: &'a Dominators) -> Self {
314        let blocks = cfg.capacity();
315        Self {
316            cfg,
317            doms,
318            loops: Vec::new(),
319            innermost: vec![None; blocks],
320            roots: Vec::new(),
321            irreducible: Vec::new(),
322            inside: vec![false; blocks],
323            index: vec![UNVISITED; blocks],
324            low: vec![0; blocks],
325            stacked: vec![false; blocks],
326        }
327    }
328
329    /// Finds the loops of one region and then of what is left of each of them.
330    ///
331    /// The region of the first call is every block control reaches. The region of a later one is
332    /// a loop with its header taken out, which is the graph the loops nested in it live in.
333    fn region(&mut self, region: &[Block], parent: Option<LoopId>) {
334        for &block in region {
335            self.inside[block.index()] = true;
336            self.index[block.index()] = UNVISITED;
337            self.stacked[block.index()] = false;
338        }
339        let components = self.components(region);
340        for &block in region {
341            self.inside[block.index()] = false;
342        }
343
344        for component in components {
345            // The nearest block dominating all of it. If it is one of the component's own
346            // blocks then every path in arrives there, because a block outside the component
347            // that the header dominates is a block the header reaches and that reaches back
348            // into the component, which would put it in the component. So a header inside means
349            // one way in, which is what reducible means.
350            let Some(header) = component
351                .iter()
352                .copied()
353                .try_fold(component[0], |a, b| self.doms.nearest_common_dominator(a, b))
354            else {
355                continue;
356            };
357            if !component.contains(&header) {
358                self.irreducible.extend_from_slice(&component);
359                continue;
360            }
361            let id = self.record(header, component, parent);
362            let inner: Vec<Block> =
363                self.loops[id.index()].blocks.iter().copied().filter(|&b| b != header).collect();
364            if !inner.is_empty() {
365                self.region(&inner, Some(id));
366            }
367        }
368    }
369
370    /// Adds a loop, and says which blocks are in it.
371    fn record(&mut self, header: Block, blocks: Vec<Block>, parent: Option<LoopId>) -> LoopId {
372        let id = LoopId(self.loops.len() as u32);
373        let mut latches = Vec::new();
374        let mut exits = Vec::new();
375        for &block in &blocks {
376            // Every block of the loop belongs to it until an inner call says otherwise, and an
377            // inner call runs after this, so the last writer is the innermost loop.
378            self.innermost[block.index()] = Some(id);
379            if self.cfg.successors(block).contains(&header) {
380                latches.push(block);
381            }
382            for &next in self.cfg.successors(block) {
383                if !blocks.contains(&next) {
384                    exits.push(Exit { from: block, to: next });
385                }
386            }
387        }
388        let depth = parent.map_or(0, |parent| self.loops[parent.index()].depth + 1);
389        self.loops.push(LoopData {
390            header,
391            blocks,
392            latches,
393            exits,
394            parent,
395            children: Vec::new(),
396            depth,
397        });
398        match parent {
399            Some(parent) => self.loops[parent.index()].children.push(id),
400            None => self.roots.push(id),
401        }
402        id
403    }
404
405    /// The strongly connected components of the region that have a cycle in them.
406    ///
407    /// Tarjan's algorithm, with the recursion written out, because the depth of the walk is the
408    /// length of the longest path in the function and a long C function has one.
409    fn components(&mut self, region: &[Block]) -> Vec<Vec<Block>> {
410        let mut found = Vec::new();
411        let mut next = 0;
412        let mut component: Vec<Block> = Vec::new();
413        let mut walk: Vec<(Block, usize)> = Vec::new();
414        for &start in region {
415            if self.index[start.index()] != UNVISITED {
416                continue;
417            }
418            self.enter(start, &mut next, &mut component);
419            walk.push((start, 0));
420            while let Some((block, step)) = walk.pop() {
421                let successors = self.cfg.successors(block);
422                if step < successors.len() {
423                    let next_block = successors[step];
424                    walk.push((block, step + 1));
425                    if !self.inside[next_block.index()] {
426                        continue;
427                    }
428                    if self.index[next_block.index()] == UNVISITED {
429                        self.enter(next_block, &mut next, &mut component);
430                        walk.push((next_block, 0));
431                    } else if self.stacked[next_block.index()] {
432                        let seen = self.index[next_block.index()];
433                        self.low[block.index()] = self.low[block.index()].min(seen);
434                    }
435                    continue;
436                }
437                if self.low[block.index()] == self.index[block.index()] {
438                    let start = component.iter().rposition(|&b| b == block).expect("on the stack");
439                    let members: Vec<Block> = component.split_off(start);
440                    for &member in &members {
441                        self.stacked[member.index()] = false;
442                    }
443                    if members.len() > 1 || self.cfg.successors(block).contains(&block) {
444                        found.push(members);
445                    }
446                }
447                if let Some(&(above, _)) = walk.last() {
448                    let reached = self.low[block.index()];
449                    self.low[above.index()] = self.low[above.index()].min(reached);
450                }
451            }
452        }
453        found
454    }
455
456    /// Numbers a block and puts it on the component stack.
457    fn enter(&mut self, block: Block, next: &mut u32, component: &mut Vec<Block>) {
458        self.index[block.index()] = *next;
459        self.low[block.index()] = *next;
460        *next += 1;
461        component.push(block);
462        self.stacked[block.index()] = true;
463    }
464
465    fn finish(mut self) -> Loops {
466        self.irreducible.sort_unstable_by_key(|block| block.index());
467        self.irreducible.dedup();
468        Loops {
469            loops: self.loops,
470            innermost: self.innermost,
471            roots: self.roots,
472            irreducible: self.irreducible,
473        }
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use rucc_ir::Block;
480
481    use crate::cfg::Cfg;
482    use crate::dom::Dominators;
483    use crate::loops::Loops;
484    use crate::testing::graph;
485
486    /// Block number `n`, spelled the way the tests read.
487    fn b(n: usize) -> Block {
488        Block::from_usize(n)
489    }
490
491    /// The forest of a graph, along with the graph and the tree it was read from.
492    fn forest(edges: &[&[usize]]) -> (Cfg, Loops) {
493        let func = graph(edges);
494        let cfg = Cfg::new(&func);
495        let doms = Dominators::new(&cfg);
496        let loops = Loops::new(&cfg, &doms);
497        assert_eq!(loops.problems(&cfg, &doms), Vec::<String>::new());
498        (cfg, loops)
499    }
500
501    /// The blocks of a loop, as sorted block numbers.
502    fn blocks(loops: &Loops, id: crate::loops::LoopId) -> Vec<usize> {
503        let mut list: Vec<usize> = loops.blocks(id).iter().map(|b| b.index()).collect();
504        list.sort_unstable();
505        list
506    }
507
508    #[test]
509    fn a_function_with_no_cycle_has_no_loops() {
510        let (_, loops) = forest(&[&[1, 2], &[3], &[3], &[]]);
511        assert_eq!(loops.count(), 0);
512        assert!(loops.innermost(b(1)).is_none());
513        assert!(loops.irreducible().is_empty());
514    }
515
516    #[test]
517    fn a_block_that_branches_to_itself_is_a_loop() {
518        let (_, loops) = forest(&[&[1], &[1, 2], &[]]);
519        assert_eq!(loops.count(), 1);
520        let id = loops.roots()[0];
521        assert_eq!(loops.header(id), b(1));
522        assert_eq!(blocks(&loops, id), [1]);
523        assert_eq!(loops.latches(id), [b(1)]);
524        assert_eq!(loops.exits(id), [crate::loops::Exit { from: b(1), to: b(2) }]);
525    }
526
527    #[test]
528    fn a_loop_holds_its_body_and_names_its_latch() {
529        // 0 -> 1, 1 branches to the body 2 and the exit 3, 2 goes back to 1.
530        let (cfg, loops) = forest(&[&[1], &[2, 3], &[1], &[]]);
531        let id = loops.roots()[0];
532        assert_eq!(loops.header(id), b(1));
533        assert_eq!(blocks(&loops, id), [1, 2]);
534        assert_eq!(loops.latches(id), [b(2)]);
535        assert_eq!(loops.depth(id), 0);
536        assert_eq!(loops.preheader(&cfg, id), Some(b(0)));
537    }
538
539    #[test]
540    fn a_loop_inside_a_loop_is_a_child_of_it() {
541        // 0 -> 1; the outer header 1 goes to the inner header 2 and to the exit 4; 2 goes to 3
542        // and back to 2; 3 goes back to 1.
543        let (_, loops) = forest(&[&[1], &[2, 4], &[2, 3], &[1], &[]]);
544        assert_eq!(loops.count(), 2);
545        let outer = loops.roots()[0];
546        assert_eq!(loops.header(outer), b(1));
547        assert_eq!(blocks(&loops, outer), [1, 2, 3]);
548        assert_eq!(loops.children(outer).len(), 1);
549        let inner = loops.children(outer)[0];
550        assert_eq!(loops.header(inner), b(2));
551        assert_eq!(blocks(&loops, inner), [2]);
552        assert_eq!(loops.depth(inner), 1);
553        assert_eq!(loops.parent(inner), Some(outer));
554        // The innermost loop of a block is the one it is deepest inside, and the block set of
555        // the outer loop still holds it.
556        assert_eq!(loops.innermost(b(2)), Some(inner));
557        assert!(loops.contains(outer, b(2)));
558        assert!(!loops.contains(inner, b(3)));
559    }
560
561    #[test]
562    fn two_back_edges_to_one_header_are_two_latches_of_one_loop() {
563        // 1 is the header, 2 and 3 both go back to it. GCC would try to work out whether one of
564        // them is really an inner loop's latch. This does not guess: they are two latches, the
565        // canonical form wants one, and the canonicalizer is what makes one.
566        let (_, loops) = forest(&[&[1], &[2, 3], &[1], &[1, 4], &[]]);
567        assert_eq!(loops.count(), 1);
568        let id = loops.roots()[0];
569        let mut latches: Vec<usize> = loops.latches(id).iter().map(|b| b.index()).collect();
570        latches.sort_unstable();
571        assert_eq!(latches, [2, 3]);
572    }
573
574    #[test]
575    fn a_two_entry_loop_is_irreducible_and_is_not_a_loop() {
576        // The classic one. Block 0 branches into the cycle at 1 or at 2, so neither of them
577        // dominates the other and neither is a header.
578        let (_, loops) = forest(&[&[1, 2], &[2], &[1, 3], &[]]);
579        assert_eq!(loops.count(), 0);
580        assert_eq!(loops.irreducible(), [b(1), b(2)]);
581        assert!(loops.is_irreducible(b(1)));
582        assert!(!loops.is_irreducible(b(0)));
583    }
584
585    #[test]
586    fn an_irreducible_region_inside_a_loop_is_found_and_the_loop_is_still_a_loop() {
587        // The outer loop 1 -> {2, 3} -> {2, 3} -> 4 -> 1 is a natural loop, and the cycle
588        // between 2 and 3 has two ways in. Taking the header out and looking again is what
589        // finds the second one, which is the reason the search recurses rather than reporting
590        // whatever is left over at the end.
591        let (_, loops) = forest(&[&[1], &[2, 3], &[3, 4], &[2, 4], &[1, 5], &[]]);
592        assert_eq!(loops.count(), 1);
593        let id = loops.roots()[0];
594        assert_eq!(loops.header(id), b(1));
595        assert_eq!(blocks(&loops, id), [1, 2, 3, 4]);
596        assert_eq!(loops.irreducible(), [b(2), b(3)]);
597    }
598
599    #[test]
600    fn a_self_loop_inside_an_irreducible_region_is_declined_along_with_it() {
601        // 0 branches to 1 and 2, 1 goes back to 0 and on to 2, and 2 branches to 1 and to
602        // itself. The whole thing is one natural loop headed at 0. Inside it, the cycle between
603        // 1 and 2 has two ways in and is irreducible, and block 2's branch to itself is a back
604        // edge sitting in the middle of it.
605        //
606        // The textbook construction would report that back edge as a second loop. This does not,
607        // because it stops at the irreducible region rather than looking inside, and the answer
608        // it gives instead is that block 2 is irreducible. A loop pass reads that and leaves the
609        // block alone, which is what it would have done with a one block loop it was told not to
610        // touch. The property test in `tests/loops.rs` is where the difference is pinned down.
611        let (_, loops) = forest(&[&[1, 2], &[0, 2], &[1, 2]]);
612        assert_eq!(loops.count(), 1);
613        let id = loops.roots()[0];
614        assert_eq!(loops.header(id), b(0));
615        assert_eq!(blocks(&loops, id), [0, 1, 2]);
616        assert_eq!(loops.irreducible(), [b(1), b(2)]);
617    }
618
619    #[test]
620    fn a_loop_with_more_than_one_way_in_has_no_preheader() {
621        // Both 0 and 1 branch to the header, so there is no single block to hoist into.
622        let (cfg, loops) = forest(&[&[1, 2], &[2], &[2, 3], &[]]);
623        let id = loops.roots()[0];
624        assert_eq!(loops.header(id), b(2));
625        assert!(loops.preheader(&cfg, id).is_none());
626    }
627
628    #[test]
629    fn a_predecessor_that_goes_two_ways_is_not_a_preheader() {
630        // Block 0 branches to the header and to somewhere else, so hoisting into it would run
631        // the hoisted code on a path that never enters the loop.
632        let (cfg, loops) = forest(&[&[1, 3], &[1, 2], &[], &[]]);
633        let id = loops.roots()[0];
634        assert_eq!(loops.header(id), b(1));
635        assert!(loops.preheader(&cfg, id).is_none());
636    }
637
638    #[test]
639    fn an_unreachable_cycle_is_not_a_loop() {
640        // Blocks 2 and 3 are a cycle nothing reaches. Section 6.5 says an unreachable block is
641        // invisible to every analysis, and a loop nothing can enter is not a loop to unroll.
642        let (_, loops) = forest(&[&[1], &[], &[3], &[2]]);
643        assert_eq!(loops.count(), 0);
644        assert!(loops.irreducible().is_empty());
645    }
646
647    #[test]
648    fn a_value_defined_outside_the_loop_is_invariant_in_it() {
649        use rucc_base::Interner;
650        use rucc_ir::{Builder, Func, Signature, Type};
651
652        let mut names = Interner::new();
653        let mut func = Func::new(names.intern("f"), Signature::new());
654        let entry = func.create_block();
655        let header = func.create_block();
656        let after = func.create_block();
657
658        let mut build = Builder::new(&mut func, entry);
659        let outside = build.iconst(Type::int(32), 7);
660        build.jump(header, &[]);
661        let mut build = Builder::new(&mut func, header);
662        let inside = build.iconst(Type::int(32), 9);
663        let cond = build.iconst(Type::int(1), 1);
664        build.br_if(cond, header, &[], after, &[]);
665        let mut build = Builder::new(&mut func, after);
666        build.ret(&[]);
667
668        let cfg = Cfg::new(&func);
669        let loops = Loops::new(&cfg, &Dominators::new(&cfg));
670        let id = loops.roots()[0];
671        assert!(loops.is_invariant(&func, id, outside));
672        assert!(!loops.is_invariant(&func, id, inside));
673    }
674}