Skip to main content

llvm_analysis/
dominators.rs

1//! Dominator tree computed with the iterative dataflow algorithm from
2//! Cooper, Harvey & Kennedy — "A Simple, Fast Dominance Algorithm" (2001).
3//!
4//! Also provides dominance frontier computation (Cytron et al. 1991),
5//! required for SSA φ-node placement in `mem2reg`.
6
7use crate::cfg::Cfg;
8use llvm_ir::{BlockId, Function};
9use std::collections::HashMap;
10
11/// Dominator tree for a single function.
12///
13/// `idom[i]` is the immediate dominator of block `i`. The entry block's
14/// entry is `None` (it has no dominator).
15pub struct DomTree {
16    /// Immediate dominator of each block. `None` for the entry block and
17    /// for blocks unreachable from entry.
18    idom: Vec<Option<BlockId>>,
19}
20
21impl DomTree {
22    /// Compute the dominator tree using the Cooper/Harvey/Kennedy iterative
23    /// algorithm on the RPO-numbered CFG.
24    pub fn compute(func: &Function, cfg: &Cfg) -> Self {
25        let n = func.num_blocks();
26        if n == 0 {
27            return DomTree { idom: vec![] };
28        }
29
30        // Assign each block a position in RPO; unreachable blocks get None.
31        let rpo = cfg.rpo();
32        let mut rpo_idx: Vec<Option<usize>> = vec![None; n];
33        for (i, &bid) in rpo.iter().enumerate() {
34            rpo_idx[bid.0 as usize] = Some(i);
35        }
36
37        // idom[rpo_pos] = rpo_pos of immediate dominator, or usize::MAX for undefined.
38        const UNDEF: usize = usize::MAX;
39        let mut idom = vec![UNDEF; rpo.len()];
40        idom[0] = 0; // entry dominates itself
41
42        let mut changed = true;
43        while changed {
44            changed = false;
45            // Process blocks in RPO order (skip entry at index 0).
46            for i in 1..rpo.len() {
47                let bid = rpo[i];
48                let preds = cfg.predecessors(bid);
49
50                // Find the first already-processed predecessor.
51                let new_idom_opt = preds
52                    .iter()
53                    .find_map(|&p| rpo_idx[p.0 as usize].filter(|&pi| idom[pi] != UNDEF));
54
55                if let Some(mut new_idom) = new_idom_opt {
56                    // Intersect with all other processed predecessors.
57                    for &p in preds {
58                        let pi = match rpo_idx[p.0 as usize] {
59                            Some(pi) if idom[pi] != UNDEF => pi,
60                            _ => continue,
61                        };
62                        if pi != new_idom {
63                            new_idom = Self::intersect(pi, new_idom, &idom);
64                        }
65                    }
66                    if idom[i] != new_idom {
67                        idom[i] = new_idom;
68                        changed = true;
69                    }
70                }
71            }
72        }
73
74        // Convert back from RPO positions to BlockIds.
75        let mut result = vec![None; n];
76        for (i, &bid) in rpo.iter().enumerate() {
77            if i == 0 {
78                result[bid.0 as usize] = None; // entry has no dominator
79            } else if idom[i] != UNDEF {
80                result[bid.0 as usize] = Some(rpo[idom[i]]);
81            }
82        }
83
84        DomTree { idom: result }
85    }
86
87    /// Walk up both fingers until they meet — the common dominator.
88    fn intersect(mut a: usize, mut b: usize, idom: &[usize]) -> usize {
89        while a != b {
90            while a > b {
91                a = idom[a];
92            }
93            while b > a {
94                b = idom[b];
95            }
96        }
97        a
98    }
99
100    /// Immediate dominator of `bid`. Returns `None` for the entry block and
101    /// for blocks unreachable from entry.
102    pub fn idom(&self, bid: BlockId) -> Option<BlockId> {
103        self.idom[bid.0 as usize]
104    }
105
106    /// Returns `true` if block `a` dominates block `b`.
107    /// Every block dominates itself.
108    pub fn dominates(&self, a: BlockId, b: BlockId) -> bool {
109        if a == b {
110            return true;
111        }
112        self.strictly_dominates(a, b)
113    }
114
115    /// Returns `true` if block `a` strictly dominates block `b`
116    /// (dominates but is not equal to `b`).
117    pub fn strictly_dominates(&self, a: BlockId, b: BlockId) -> bool {
118        let mut cur = b;
119        loop {
120            match self.idom[cur.0 as usize] {
121                None => return false,
122                Some(p) if p == a => return true,
123                Some(p) if p == cur => return false, // entry reached without finding a
124                Some(p) => cur = p,
125            }
126        }
127    }
128
129    /// Compute the dominance frontier for every block.
130    ///
131    /// `DF[b]` = set of blocks y such that b dominates a predecessor of y
132    /// but does not strictly dominate y. Used for φ-node placement in SSA.
133    pub fn dominance_frontier(&self, cfg: &Cfg) -> HashMap<BlockId, Vec<BlockId>> {
134        let mut df: HashMap<BlockId, Vec<BlockId>> = HashMap::new();
135        let n = self.idom.len();
136
137        for y_idx in 0..n {
138            let y = BlockId(y_idx as u32);
139            // Unreachable blocks have idom = None. Skipping them prevents the
140            // while-loop condition `Some(runner) != None` from being permanently
141            // true and adding spurious DF entries before the `None => break` fires.
142            // The entry block also has idom = None but has 0 predecessors and is
143            // already excluded by the preds.len() < 2 check below.
144            if self.idom[y_idx].is_none() {
145                continue;
146            }
147            let preds = cfg.predecessors(y);
148            if preds.len() < 2 {
149                continue; // only join points have non-empty DF contributions
150            }
151            for &p in preds {
152                let mut runner = p;
153                while Some(runner) != self.idom[y_idx] {
154                    df.entry(runner).or_default().push(y);
155                    match self.idom[runner.0 as usize] {
156                        Some(parent) => runner = parent,
157                        None => break,
158                    }
159                }
160            }
161        }
162
163        // Deduplicate entries (a block can appear multiple times).
164        for v in df.values_mut() {
165            v.sort_unstable_by_key(|b| b.0);
166            v.dedup();
167        }
168        df
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use llvm_ir::{BasicBlock, Context, Function, InstrKind, Instruction, Linkage, ValueRef};
176
177    fn build_func(num_blocks: usize, edges: &[(usize, Vec<usize>)]) -> (Context, Function) {
178        let mut ctx = Context::new();
179        let fn_ty = ctx.mk_fn_type(ctx.void_ty, vec![], false);
180        let mut func = Function::new("test", fn_ty, vec![], Linkage::External);
181        for i in 0..num_blocks {
182            func.add_block(BasicBlock::new(format!("b{}", i)));
183        }
184        let mut has_term = vec![false; num_blocks];
185        for &(src, ref dsts) in edges {
186            has_term[src] = true;
187            let kind = match dsts.as_slice() {
188                [] => InstrKind::Unreachable,
189                [dst] => InstrKind::Br {
190                    dest: BlockId(*dst as u32),
191                },
192                [t, f] => {
193                    let cond = ValueRef::Constant(ctx.const_int(ctx.i1_ty, 0));
194                    InstrKind::CondBr {
195                        cond,
196                        then_dest: BlockId(*t as u32),
197                        else_dest: BlockId(*f as u32),
198                    }
199                }
200                _ => panic!("max 2 successors"),
201            };
202            let iid = func.alloc_instr(Instruction {
203                name: None,
204                ty: ctx.void_ty,
205                kind,
206            });
207            func.blocks[src].set_terminator(iid);
208        }
209        for (i, &needs_term) in has_term.iter().enumerate() {
210            if !needs_term {
211                let iid = func.alloc_instr(Instruction {
212                    name: None,
213                    ty: ctx.void_ty,
214                    kind: InstrKind::Unreachable,
215                });
216                func.blocks[i].set_terminator(iid);
217            }
218        }
219        (ctx, func)
220    }
221
222    #[test]
223    fn domtree_single_block() {
224        let (_ctx, func) = build_func(1, &[(0, vec![])]);
225        let cfg = Cfg::compute(&func);
226        let dom = DomTree::compute(&func, &cfg);
227        assert_eq!(dom.idom(BlockId(0)), None);
228        assert!(dom.dominates(BlockId(0), BlockId(0)));
229    }
230
231    #[test]
232    fn domtree_linear() {
233        // 0 -> 1 -> 2
234        let (_ctx, func) = build_func(3, &[(0, vec![1]), (1, vec![2]), (2, vec![])]);
235        let cfg = Cfg::compute(&func);
236        let dom = DomTree::compute(&func, &cfg);
237        assert_eq!(dom.idom(BlockId(0)), None);
238        assert_eq!(dom.idom(BlockId(1)), Some(BlockId(0)));
239        assert_eq!(dom.idom(BlockId(2)), Some(BlockId(1)));
240        assert!(dom.dominates(BlockId(0), BlockId(2)));
241        assert!(!dom.dominates(BlockId(2), BlockId(0)));
242        assert!(dom.strictly_dominates(BlockId(0), BlockId(1)));
243        assert!(!dom.strictly_dominates(BlockId(1), BlockId(1)));
244    }
245
246    #[test]
247    fn domtree_diamond() {
248        // 0 -> {1,2} -> 3
249        let (_ctx, func) = build_func(
250            4,
251            &[(0, vec![1, 2]), (1, vec![3]), (2, vec![3]), (3, vec![])],
252        );
253        let cfg = Cfg::compute(&func);
254        let dom = DomTree::compute(&func, &cfg);
255        // 0 dominates everything; 3's idom is 0 (not 1 or 2).
256        assert_eq!(dom.idom(BlockId(3)), Some(BlockId(0)));
257        assert!(dom.dominates(BlockId(0), BlockId(3)));
258        assert!(!dom.dominates(BlockId(1), BlockId(3)));
259        assert!(!dom.dominates(BlockId(2), BlockId(3)));
260    }
261
262    #[test]
263    fn domtree_loop() {
264        // 0 -> 1 -> 2 -> {1, 3}
265        let (_ctx, func) = build_func(
266            4,
267            &[(0, vec![1]), (1, vec![2]), (2, vec![1, 3]), (3, vec![])],
268        );
269        let cfg = Cfg::compute(&func);
270        let dom = DomTree::compute(&func, &cfg);
271        assert_eq!(dom.idom(BlockId(1)), Some(BlockId(0)));
272        assert_eq!(dom.idom(BlockId(2)), Some(BlockId(1)));
273        assert_eq!(dom.idom(BlockId(3)), Some(BlockId(2)));
274        // 1 dominates 2 (loop body)
275        assert!(dom.dominates(BlockId(1), BlockId(2)));
276    }
277
278    #[test]
279    fn dominance_frontier_diamond() {
280        // 0 -> {1,2} -> 3  — classic phi-placement example
281        let (_ctx, func) = build_func(
282            4,
283            &[(0, vec![1, 2]), (1, vec![3]), (2, vec![3]), (3, vec![])],
284        );
285        let cfg = Cfg::compute(&func);
286        let dom = DomTree::compute(&func, &cfg);
287        let df = dom.dominance_frontier(&cfg);
288        // DF(1) = DF(2) = {3}, DF(0) = DF(3) = {}
289        assert_eq!(
290            df.get(&BlockId(1)).map(|v| v.as_slice()),
291            Some(&[BlockId(3)][..])
292        );
293        assert_eq!(
294            df.get(&BlockId(2)).map(|v| v.as_slice()),
295            Some(&[BlockId(3)][..])
296        );
297        assert!(df.get(&BlockId(0)).map_or(true, |v| v.is_empty()));
298        assert!(df.get(&BlockId(3)).map_or(true, |v| v.is_empty()));
299    }
300
301    #[test]
302    fn dominance_frontier_loop() {
303        // 0 -> 1 -> 2 -> {1, 3}: DF(2) = {1} (back-edge creates frontier)
304        let (_ctx, func) = build_func(
305            4,
306            &[(0, vec![1]), (1, vec![2]), (2, vec![1, 3]), (3, vec![])],
307        );
308        let cfg = Cfg::compute(&func);
309        let dom = DomTree::compute(&func, &cfg);
310        let df = dom.dominance_frontier(&cfg);
311        assert!(df
312            .get(&BlockId(2))
313            .map_or(false, |v| v.contains(&BlockId(1))));
314    }
315
316    #[test]
317    fn dominance_frontier_unreachable_join_point() {
318        // Block 0 has no successors; blocks 1, 2, 3 are all unreachable.
319        // Block 3 has two unreachable predecessors (1 and 2), so preds.len() >= 2
320        // and idom[3] = None.  Without the fix the while-condition
321        // `Some(runner) != None` would be permanently true until the inner
322        // `None => break` fires, producing spurious DF entries for blocks 1 and 2.
323        let (_ctx, func) = build_func(4, &[(0, vec![]), (1, vec![3]), (2, vec![3]), (3, vec![])]);
324        let cfg = Cfg::compute(&func);
325        let dom = DomTree::compute(&func, &cfg);
326        let df = dom.dominance_frontier(&cfg);
327        // No reachable block exists, so the dominance frontier must be empty.
328        assert!(
329            df.get(&BlockId(1)).map_or(true, |v| v.is_empty()),
330            "spurious DF entry for unreachable block 1: {:?}",
331            df.get(&BlockId(1))
332        );
333        assert!(
334            df.get(&BlockId(2)).map_or(true, |v| v.is_empty()),
335            "spurious DF entry for unreachable block 2: {:?}",
336            df.get(&BlockId(2))
337        );
338    }
339}