Skip to main content

rucc_opt/
dom.rs

1//! Dominance, which is the question of what every path has to go through.
2//!
3//! Design: `spec/optimizer/06-cfg-and-dominators.md`.
4//!
5//! Block `a` dominates block `b` when every path from the entry to `b` goes through `a`. That
6//! is what makes it safe to say a value defined in `a` is available in `b`, so almost every
7//! transformation that moves code asks this and the ones that do not are asking the reverse
8//! question, which is post-dominance: whether every path from `b` to the exit goes through `a`.
9//!
10//! One algorithm answers both, the iterative one of Cooper, Harvey and Kennedy, "A Simple, Fast
11//! Dominance Algorithm" (2001). GCC uses Lengauer and Tarjan, which is asymptotically better
12//! and, on graphs the size of a function, slower. That is the argument of the paper and it is
13//! why the verifier already uses this algorithm. There is no ET-forest here and no incremental
14//! update: a change to the shape of a function throws the tree away and the next request builds
15//! a new one. Section 6.3 of the design says why, and section 6.7 says what measurement would
16//! change it, which is the analyses taking more than three percent of `-O2` time.
17//!
18//! The one thing to be careful about is that this is the analysis most likely to be quietly
19//! wrong, because a tree that is stale rather than absent produces a miscompilation rather than
20//! a crash.
21
22use rucc_ir::Block;
23
24use crate::cfg::Cfg;
25
26/// A node of whichever graph is being walked, which is a block number for the function's own
27/// graph and a block number or the added exit for the reverse of it.
28type Node = u32;
29
30/// No such node, for an immediate dominator that has not been worked out and for a node the
31/// root does not reach.
32const NONE: Node = Node::MAX;
33
34/// A graph the dominator computation can walk.
35///
36/// Two implement it, the function's graph and the reverse of the function's graph, and having
37/// them behind one trait is what stops post-dominance from being a second copy of the algorithm
38/// with the arrows turned around by hand. Static dispatch throughout, so the reverse case pays
39/// nothing for the abstraction and the forward case does not copy the adjacency lists to
40/// renumber them.
41trait Graph {
42    /// How many nodes there are, which is the length of every array indexed by node.
43    fn nodes(&self) -> usize;
44
45    /// Where the walk starts, which is the entry going forwards and the exit going backwards.
46    fn root(&self) -> Node;
47
48    /// Where control arrives at this node from.
49    fn preds(&self, node: Node) -> impl Iterator<Item = Node>;
50
51    /// Where control goes from this node.
52    fn succs(&self, node: Node) -> impl Iterator<Item = Node>;
53}
54
55/// A dominator tree over a graph whose nodes are numbered from zero.
56#[derive(Clone, Debug)]
57struct Tree {
58    /// The immediate dominator of each node, the root's own number for the root, and [`NONE`]
59    /// for a node the root does not reach.
60    idom: Vec<Node>,
61    /// The nodes each node immediately dominates.
62    children: Vec<Vec<Node>>,
63    /// When a depth first walk of the tree entered each node.
64    enter: Vec<u32>,
65    /// One past the largest number handed out anywhere in a node's subtree.
66    leave: Vec<u32>,
67    /// Where the walk started.
68    root: Node,
69}
70
71impl Tree {
72    /// Builds the tree, which is a postorder walk, a fixed point over it, and a second walk to
73    /// number the result.
74    fn new(graph: &impl Graph) -> Self {
75        let nodes = graph.nodes();
76        let root = graph.root();
77
78        // Postorder by an explicit stack. The recursive form runs out of stack on a function
79        // that is one long chain of blocks, and a ten thousand statement function is a real
80        // thing that people write and that generated code writes more of.
81        let mut order: Vec<Node> = Vec::new();
82        let mut seen = vec![false; nodes];
83        seen[root as usize] = true;
84        let mut stack = vec![(root, graph.succs(root))];
85        while let Some((node, mut walk)) = stack.pop() {
86            // Out of the `match` scrutinee, because the closure borrows the seen set and the
87            // arm below writes to it.
88            let step = walk.find(|&next| !seen[next as usize]);
89            match step {
90                Some(next) => {
91                    stack.push((node, walk));
92                    seen[next as usize] = true;
93                    stack.push((next, graph.succs(next)));
94                }
95                None => order.push(node),
96            }
97        }
98
99        // Reverse postorder is the order this fixed point settles fastest in, because every
100        // node other than a loop header is reached after a predecessor that already has an
101        // answer. It converges in one sweep over a reducible graph and in two over the ones a
102        // `goto` into a loop body produces, which is the entire special case irreducibility
103        // needs here.
104        let rpo: Vec<Node> = order.iter().rev().copied().collect();
105        let mut rank = vec![NONE; nodes];
106        for (index, &node) in rpo.iter().enumerate() {
107            rank[node as usize] = index as u32;
108        }
109        let mut preds: Vec<Vec<u32>> = vec![Vec::new(); rpo.len()];
110        for (index, &node) in rpo.iter().enumerate() {
111            for pred in graph.preds(node) {
112                if rank[pred as usize] != NONE {
113                    preds[index].push(rank[pred as usize]);
114                }
115            }
116        }
117
118        // The root dominates itself and everything else starts with no answer, which is what
119        // the sentinel is. A predecessor with no answer yet is skipped rather than met, because
120        // meeting with nothing is not the same as meeting with the root.
121        let mut idom_by_rank = vec![NONE; rpo.len()];
122        if !rpo.is_empty() {
123            idom_by_rank[0] = 0;
124        }
125        let mut changed = true;
126        while changed {
127            changed = false;
128            for index in 1..rpo.len() {
129                let mut new = NONE;
130                for &pred in &preds[index] {
131                    if idom_by_rank[pred as usize] == NONE {
132                        continue;
133                    }
134                    new = if new == NONE { pred } else { meet(&idom_by_rank, new, pred) };
135                }
136                if new != NONE && idom_by_rank[index] != new {
137                    idom_by_rank[index] = new;
138                    changed = true;
139                }
140            }
141        }
142
143        let mut idom = vec![NONE; nodes];
144        let mut children: Vec<Vec<Node>> = vec![Vec::new(); nodes];
145        for (index, &node) in rpo.iter().enumerate() {
146            let parent = rpo[idom_by_rank[index] as usize];
147            idom[node as usize] = parent;
148            if node != root {
149                children[parent as usize].push(node);
150            }
151        }
152
153        // The numbering that makes a query two comparisons instead of a walk up the tree. The
154        // walk is fine when the caller asks once per value definition and it is not fine for
155        // GVN or code motion, which ask per pair inside a loop. This is GCC's answer, from
156        // `compute_dom_fast_query`, and it costs one linear pass over a tree that is already
157        // built.
158        let mut enter = vec![0; nodes];
159        let mut leave = vec![0; nodes];
160        let mut time = 0;
161        let mut stack = vec![(root, 0usize)];
162        enter[root as usize] = time;
163        time += 1;
164        while let Some((node, next)) = stack.pop() {
165            match children[node as usize].get(next) {
166                Some(&child) => {
167                    stack.push((node, next + 1));
168                    enter[child as usize] = time;
169                    time += 1;
170                    stack.push((child, 0));
171                }
172                None => leave[node as usize] = time,
173            }
174        }
175
176        Self { idom, children, enter, leave, root }
177    }
178
179    /// Whether the root reaches this node at all.
180    ///
181    /// A node number the tree has no room for is one of a function with no blocks, and the
182    /// answer is the same: nothing reaches it.
183    fn reached(&self, node: Node) -> bool {
184        self.idom.get(node as usize).is_some_and(|&parent| parent != NONE)
185    }
186
187    /// Whether every path from the root to `node` goes through `of`.
188    ///
189    /// False when either end is unreachable, which is the one place this differs from the
190    /// verifier's copy of the same algorithm. The verifier calls an unreachable block
191    /// vacuously dominated so that it is reported once, for being unreachable, rather than
192    /// again for every value it uses. An optimizer must not: "defined in a block that dominates
193    /// this one" and "defined in a block control reaches" are different questions, and a pass
194    /// that answers the first when it meant the second will move a use of a value into a place
195    /// the value does not exist.
196    fn dominates(&self, of: Node, node: Node) -> bool {
197        if !self.reached(of) || !self.reached(node) {
198            return false;
199        }
200        let (enter, leave) = (self.enter[of as usize], self.leave[of as usize]);
201        enter <= self.enter[node as usize] && self.enter[node as usize] < leave
202    }
203
204    /// The nearest node that dominates both, which is the root at worst.
205    fn common(&self, a: Node, b: Node) -> Option<Node> {
206        if !self.reached(a) || !self.reached(b) {
207            return None;
208        }
209        let mut walk = a;
210        while !self.dominates(walk, b) {
211            walk = self.idom[walk as usize];
212        }
213        Some(walk)
214    }
215}
216
217/// The nearest node dominating both, walking the two chains towards the root by rank.
218///
219/// Ranks are reverse postorder positions, so the larger number is the deeper node and stepping
220/// the deeper one towards the root is what makes the two meet.
221fn meet(idom: &[u32], mut a: u32, mut b: u32) -> u32 {
222    while a != b {
223        while a > b {
224            a = idom[a as usize];
225        }
226        while b > a {
227            b = idom[b as usize];
228        }
229    }
230    a
231}
232
233/// The function's own graph, walked forwards.
234struct Forward<'a>(&'a Cfg);
235
236impl Graph for Forward<'_> {
237    fn nodes(&self) -> usize {
238        self.0.capacity()
239    }
240
241    fn root(&self) -> Node {
242        self.0.entry().map_or(0, |block| block.index() as Node)
243    }
244
245    fn preds(&self, node: Node) -> impl Iterator<Item = Node> {
246        self.0.predecessors(Block::from_usize(node as usize)).iter().map(|b| b.index() as Node)
247    }
248
249    fn succs(&self, node: Node) -> impl Iterator<Item = Node> {
250        self.0.successors(Block::from_usize(node as usize)).iter().map(|b| b.index() as Node)
251    }
252}
253
254/// Which block every path from the entry has to pass through to reach another.
255#[derive(Clone, Debug)]
256pub struct Dominators {
257    tree: Tree,
258}
259
260impl Dominators {
261    /// Builds the tree from the graph.
262    ///
263    /// A function with no blocks gives a tree that reaches nothing, and every query against it
264    /// answers no, which is what a caller handed a declaration should see.
265    #[must_use]
266    pub fn new(cfg: &Cfg) -> Self {
267        if cfg.entry().is_none() {
268            return Self { tree: Tree::empty() };
269        }
270        Self { tree: Tree::new(&Forward(cfg)) }
271    }
272
273    /// Whether every path from the entry to `block` goes through `of`.
274    ///
275    /// A block dominates itself. Both ends have to be blocks control reaches, and an
276    /// unreachable one dominates nothing and is dominated by nothing.
277    #[must_use]
278    pub fn dominates(&self, of: Block, block: Block) -> bool {
279        self.tree.dominates(of.index() as Node, block.index() as Node)
280    }
281
282    /// The same, without a block dominating itself.
283    #[must_use]
284    pub fn strictly_dominates(&self, of: Block, block: Block) -> bool {
285        of != block && self.dominates(of, block)
286    }
287
288    /// The nearest block that dominates this one and is not it.
289    ///
290    /// `None` for the entry, which has no dominator above it, and for a block control does not
291    /// reach.
292    #[must_use]
293    pub fn immediate_dominator(&self, block: Block) -> Option<Block> {
294        let node = block.index() as Node;
295        if !self.tree.reached(node) || node == self.tree.root {
296            return None;
297        }
298        Some(Block::from_usize(self.tree.idom[node as usize] as usize))
299    }
300
301    /// The blocks whose immediate dominator is this one.
302    ///
303    /// Walking these from the entry is how a pass visits a function in dominator tree order,
304    /// which is the order that has every definition in hand before any use of it.
305    pub fn children(&self, block: Block) -> impl Iterator<Item = Block> + use<'_> {
306        self.tree
307            .children
308            .get(block.index())
309            .map_or(&[][..], Vec::as_slice)
310            .iter()
311            .map(|&node| Block::from_usize(node as usize))
312    }
313
314    /// The nearest block that dominates both, which is the entry at worst.
315    ///
316    /// `None` when either block is one control does not reach.
317    #[must_use]
318    pub fn nearest_common_dominator(&self, a: Block, b: Block) -> Option<Block> {
319        self.tree
320            .common(a.index() as Node, b.index() as Node)
321            .map(|node| Block::from_usize(node as usize))
322    }
323}
324
325/// The function's graph with every arrow turned around, and an exit for them all to end at.
326///
327/// The exit is a node this adds, numbered one past the last block, because the real graph has
328/// as many blocks with no successors as it has `return` statements and a dominator computation
329/// wants one root. Every block with no successors gets an edge to it, and so does every
330/// infinite loop, through [`Reverse::connect`].
331struct Reverse {
332    /// Where control comes from, by node, which is where it goes in the real graph.
333    succs: Vec<Vec<Node>>,
334    /// Where control goes, by node, which is where it comes from in the real graph.
335    preds: Vec<Vec<Node>>,
336    /// The added node, numbered one past the last block.
337    exit: Node,
338}
339
340impl Graph for Reverse {
341    fn nodes(&self) -> usize {
342        self.succs.len()
343    }
344
345    fn root(&self) -> Node {
346        self.exit
347    }
348
349    fn preds(&self, node: Node) -> impl Iterator<Item = Node> {
350        self.preds[node as usize].iter().copied()
351    }
352
353    fn succs(&self, node: Node) -> impl Iterator<Item = Node> {
354        self.succs[node as usize].iter().copied()
355    }
356}
357
358impl Reverse {
359    /// Turns the graph around, keeping only the blocks control reaches.
360    fn new(cfg: &Cfg) -> Self {
361        let exit = cfg.capacity() as Node;
362        let mut succs: Vec<Vec<Node>> = vec![Vec::new(); cfg.capacity() + 1];
363        let mut preds: Vec<Vec<Node>> = vec![Vec::new(); cfg.capacity() + 1];
364        for &block in cfg.postorder() {
365            let from = block.index() as Node;
366            for &next in cfg.successors(block) {
367                succs[next.index()].push(from);
368                preds[from as usize].push(next.index() as Node);
369            }
370            if cfg.successors(block).is_empty() {
371                succs[exit as usize].push(from);
372                preds[from as usize].push(exit);
373            }
374        }
375        Self { succs, preds, exit }
376    }
377
378    /// Adds an edge to the exit from every region that has no path to one.
379    ///
380    /// `while (1) { }` has no path to the exit, so the reverse graph is disconnected and
381    /// post-dominance is undefined for everything in the loop. GCC's answer is
382    /// `connect_infinite_loops_to_exit`, which is to add a fake edge from the far end of each
383    /// such region, and this is the same thing. The edges are recorded rather than hidden, so a
384    /// pass that would treat one as a real path can ask.
385    ///
386    /// Anything reachable from a block with no path to the exit also has no path to the exit,
387    /// which is why walking forwards from one stays inside the region, and why the block this
388    /// stops at is always a sensible place to attach.
389    fn connect(&mut self, cfg: &Cfg) -> Vec<Block> {
390        let mut arrives = vec![false; self.nodes()];
391        let mut stack = vec![self.exit];
392        arrives[self.exit as usize] = true;
393        let mut fake = Vec::new();
394        let mut stamp = vec![u32::MAX; self.nodes()];
395        let mut round = 0;
396        loop {
397            while let Some(node) = stack.pop() {
398                for &next in &self.succs[node as usize] {
399                    if !arrives[next as usize] {
400                        arrives[next as usize] = true;
401                        stack.push(next);
402                    }
403                }
404            }
405            // Outermost first, so the walk to the far end starts above the loop rather than
406            // inside it, which is what makes the attachment point the same one a person would
407            // pick by hand.
408            let Some(stranded) = cfg.reverse_postorder().find(|block| !arrives[block.index()])
409            else {
410                break;
411            };
412            let end = far_end(cfg, stranded, &mut stamp, round).index() as Node;
413            round += 1;
414            self.succs[self.exit as usize].push(end);
415            self.preds[end as usize].push(self.exit);
416            fake.push(Block::from_usize(end as usize));
417            arrives[end as usize] = true;
418            stack.push(end);
419        }
420        fake
421    }
422}
423
424/// The block a forward walk from here stops at, which is the far end of the region.
425///
426/// It stops when every successor has already been stepped through, so on a loop it is the
427/// deepest block of the loop rather than the header, and on a chain it is the last block.
428fn far_end(cfg: &Cfg, from: Block, stamp: &mut [u32], round: u32) -> Block {
429    let mut block = from;
430    loop {
431        stamp[block.index()] = round;
432        let next = cfg.successors(block).iter().copied().find(|b| stamp[b.index()] != round);
433        match next {
434            Some(next) => block = next,
435            None => return block,
436        }
437    }
438}
439
440/// Which block every path to the exit has to pass through after leaving another.
441#[derive(Clone, Debug)]
442pub struct PostDominators {
443    tree: Tree,
444    exit: Node,
445    fake: Vec<Block>,
446}
447
448impl PostDominators {
449    /// Builds the tree over the reversed graph.
450    ///
451    /// # Panics
452    ///
453    /// Panics if a block control reaches still has no path to the exit after the fake edges
454    /// have been added, which would mean the answers below were arbitrary. Section 6.8 of the
455    /// design asks for exactly this, on the grounds that a wrong answer here is worth turning
456    /// into a crash.
457    #[must_use]
458    pub fn new(cfg: &Cfg) -> Self {
459        if cfg.entry().is_none() {
460            return Self { tree: Tree::empty(), exit: 0, fake: Vec::new() };
461        }
462        let mut reverse = Reverse::new(cfg);
463        let fake = reverse.connect(cfg);
464        let exit = reverse.exit;
465        let tree = Tree::new(&reverse);
466        for &block in cfg.postorder() {
467            assert!(
468                tree.reached(block.index() as Node),
469                "a block control reaches has no path to the exit, so post-dominance is undefined"
470            );
471        }
472        Self { tree, exit, fake }
473    }
474
475    /// Whether every path from `block` to the exit goes through `of`.
476    ///
477    /// A block post-dominates itself. Both ends have to be blocks control reaches.
478    #[must_use]
479    pub fn post_dominates(&self, of: Block, block: Block) -> bool {
480        self.tree.dominates(of.index() as Node, block.index() as Node)
481    }
482
483    /// The same, without a block post-dominating itself.
484    #[must_use]
485    pub fn strictly_post_dominates(&self, of: Block, block: Block) -> bool {
486        of != block && self.post_dominates(of, block)
487    }
488
489    /// The nearest block that post-dominates this one and is not it.
490    ///
491    /// `None` when the next thing on every path out is the end of the function, and for a block
492    /// control does not reach.
493    #[must_use]
494    pub fn immediate_post_dominator(&self, block: Block) -> Option<Block> {
495        let node = block.index() as Node;
496        if !self.tree.reached(node) {
497            return None;
498        }
499        let parent = self.tree.idom[node as usize];
500        if parent == self.exit || parent == node {
501            return None;
502        }
503        Some(Block::from_usize(parent as usize))
504    }
505
506    /// The blocks an edge to the exit was invented for, because nothing led there from them.
507    ///
508    /// A block is in here when it is the far end of an infinite loop. The list is public
509    /// because a pass that reasons about paths should be able to tell that one of them was
510    /// added by this analysis and is not a path the program can take.
511    #[must_use]
512    pub fn fake_exits(&self) -> &[Block] {
513        &self.fake
514    }
515}
516
517impl Tree {
518    /// The tree of a function that has no blocks, which reaches nothing.
519    fn empty() -> Self {
520        Self {
521            idom: Vec::new(),
522            children: Vec::new(),
523            enter: Vec::new(),
524            leave: Vec::new(),
525            root: NONE,
526        }
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use rucc_ir::Block;
533
534    use crate::cfg::Cfg;
535    use crate::dom::{Dominators, PostDominators};
536    use crate::testing::{computed_goto, graph};
537
538    /// Block number `n`, spelled the way the tests read.
539    fn b(n: usize) -> Block {
540        Block::from_usize(n)
541    }
542
543    /// The immediate dominator of every block, as block numbers, `None` where there is none.
544    fn idoms(doms: &Dominators, blocks: usize) -> Vec<Option<usize>> {
545        (0..blocks).map(|n| doms.immediate_dominator(b(n)).map(|d| d.index())).collect()
546    }
547
548    #[test]
549    fn a_straight_line_is_a_chain() {
550        let func = graph(&[&[1], &[2], &[]]);
551        let doms = Dominators::new(&Cfg::new(&func));
552        assert_eq!(idoms(&doms, 3), [None, Some(0), Some(1)]);
553        assert!(doms.dominates(b(0), b(2)));
554        assert!(!doms.dominates(b(2), b(0)));
555        assert!(doms.dominates(b(1), b(1)));
556        assert!(!doms.strictly_dominates(b(1), b(1)));
557    }
558
559    #[test]
560    fn a_diamond_is_dominated_by_the_block_it_came_from() {
561        let func = graph(&[&[1, 2], &[3], &[3], &[]]);
562        let doms = Dominators::new(&Cfg::new(&func));
563        // The join is dominated by the branch and not by either arm, which is the whole point.
564        assert_eq!(idoms(&doms, 4), [None, Some(0), Some(0), Some(0)]);
565        assert!(!doms.dominates(b(1), b(3)));
566        assert_eq!(doms.nearest_common_dominator(b(1), b(2)), Some(b(0)));
567    }
568
569    #[test]
570    fn a_loop_header_dominates_its_body_and_its_latch() {
571        let func = graph(&[&[1], &[2, 3], &[1], &[]]);
572        let doms = Dominators::new(&Cfg::new(&func));
573        assert_eq!(idoms(&doms, 4), [None, Some(0), Some(1), Some(1)]);
574        assert!(doms.dominates(b(1), b(2)));
575        // The header has two predecessors, one of which it dominates. Meeting the answer from
576        // the latch with the answer from outside is what a back edge asks of the fixed point.
577        assert!(!doms.dominates(b(2), b(1)));
578    }
579
580    #[test]
581    fn neither_entry_of_an_irreducible_loop_dominates_the_other() {
582        // The classic two-entry loop. Block 0 branches into the middle of the cycle either way,
583        // so blocks 1 and 2 are each reachable without the other.
584        let func = graph(&[&[1, 2], &[2], &[1, 3], &[]]);
585        let doms = Dominators::new(&Cfg::new(&func));
586        assert_eq!(idoms(&doms, 4), [None, Some(0), Some(0), Some(2)]);
587        assert!(!doms.dominates(b(1), b(2)));
588        assert!(!doms.dominates(b(2), b(1)));
589    }
590
591    #[test]
592    fn an_unreachable_block_dominates_nothing_and_nothing_dominates_it() {
593        let func = graph(&[&[1], &[], &[2]]);
594        let doms = Dominators::new(&Cfg::new(&func));
595        assert!(!doms.dominates(b(0), b(2)));
596        assert!(!doms.dominates(b(2), b(2)));
597        assert!(doms.immediate_dominator(b(2)).is_none());
598        assert!(doms.nearest_common_dominator(b(0), b(2)).is_none());
599    }
600
601    #[test]
602    fn a_block_only_a_computed_goto_reaches_is_dominated_by_the_branch() {
603        let doms = Dominators::new(&Cfg::new(&computed_goto()));
604        assert_eq!(doms.immediate_dominator(b(2)), Some(b(1)));
605    }
606
607    #[test]
608    fn a_declaration_answers_no_to_everything() {
609        let func =
610            rucc_ir::Func::new(rucc_base::Interner::new().intern("f"), rucc_ir::Signature::new());
611        let cfg = Cfg::new(&func);
612        let doms = Dominators::new(&cfg);
613        // A declaration is an ordinary thing for a pipeline to be handed, so asking about a
614        // block it does not have answers rather than panicking.
615        assert!(!doms.dominates(b(0), b(0)));
616        assert!(doms.immediate_dominator(b(0)).is_none());
617        assert_eq!(doms.children(b(0)).count(), 0);
618        let posts = PostDominators::new(&cfg);
619        assert!(posts.fake_exits().is_empty());
620        assert!(!posts.post_dominates(b(0), b(0)));
621    }
622
623    #[test]
624    fn the_children_of_a_block_are_what_it_immediately_dominates() {
625        let func = graph(&[&[1, 2], &[3], &[3], &[]]);
626        let doms = Dominators::new(&Cfg::new(&func));
627        let mut children: Vec<usize> = doms.children(b(0)).map(|c| c.index()).collect();
628        children.sort_unstable();
629        assert_eq!(children, [1, 2, 3]);
630        assert_eq!(doms.children(b(1)).count(), 0);
631    }
632
633    #[test]
634    fn a_join_is_post_dominated_by_what_comes_after_it() {
635        let func = graph(&[&[1, 2], &[3], &[3], &[]]);
636        let posts = PostDominators::new(&Cfg::new(&func));
637        assert!(posts.post_dominates(b(3), b(0)));
638        assert!(!posts.post_dominates(b(1), b(0)));
639        assert_eq!(posts.immediate_post_dominator(b(1)), Some(b(3)));
640        // Nothing comes after the return, so the next thing on the way out is the end of the
641        // function, which is not a block.
642        assert!(posts.immediate_post_dominator(b(3)).is_none());
643        assert!(posts.fake_exits().is_empty());
644    }
645
646    #[test]
647    fn an_infinite_loop_gets_an_edge_to_the_exit_and_says_which() {
648        // Block 1 is `while (1) { }` and block 2 is the only way out of the function.
649        let func = graph(&[&[1, 2], &[1], &[]]);
650        let posts = PostDominators::new(&Cfg::new(&func));
651        let fake: Vec<usize> = posts.fake_exits().iter().map(|block| block.index()).collect();
652        assert_eq!(fake, [1]);
653        // Without the invented edge this is undefined rather than false, which is the failure
654        // the design asks to be turned into a crash.
655        assert!(!posts.post_dominates(b(2), b(0)));
656        assert!(posts.post_dominates(b(1), b(1)));
657    }
658
659    #[test]
660    fn two_infinite_loops_get_an_edge_each() {
661        let func = graph(&[&[1, 2, 3], &[1], &[2], &[]]);
662        let posts = PostDominators::new(&Cfg::new(&func));
663        let mut fake: Vec<usize> = posts.fake_exits().iter().map(|block| block.index()).collect();
664        fake.sort_unstable();
665        assert_eq!(fake, [1, 2]);
666        // Three ways out of block 0 and no block on all three, so the next thing every path
667        // shares is the end of the function.
668        assert!(posts.immediate_post_dominator(b(0)).is_none());
669    }
670}