Skip to main content

jstd/graph/analysis/
dominator.rs

1//! Dominator analysis for directed graphs.
2//!
3//! Definitions used here follow control-flow graph conventions:
4//! - A node `d` dominates `n` if every path from the graph root to `n` passes
5//!   through `d`.
6//! - The root dominates only itself.
7//!
8//! The main entry point is [`compute_dominators`], which runs the
9//! Lengauer–Tarjan algorithm and returns a [`DominatorTree`]. Full dominator
10//! sets and the children map are computed lazily on first access.
11
12use std::cell::OnceCell;
13use std::collections::{HashMap, HashSet};
14use std::hash::{BuildHasher, Hash};
15
16use crate::graph::{Cfg, FxBuildHasher};
17
18pub fn reachable_from_root<C: Cfg>(graph: &C, root: C::NodeId) -> Vec<C::NodeId> {
19    let mut visited: HashSet<C::NodeId, C::Hasher> = HashSet::default();
20    let mut order = Vec::new();
21    let mut stack = vec![root];
22    while let Some(node) = stack.pop() {
23        if !visited.insert(node) {
24            continue;
25        }
26        order.push(node);
27        for succ in graph.successors(node) {
28            if !visited.contains(&succ) {
29                stack.push(succ);
30            }
31        }
32    }
33    order
34}
35
36// ---------------------------------------------------------------------------
37// DominatorTree
38// ---------------------------------------------------------------------------
39
40/// Dominator tree for a reachable subgraph.
41///
42/// The immediate-dominator map is computed eagerly by [`compute_dominators`].
43/// Full dominator sets and the children map are computed lazily on first
44/// access and then cached.
45///
46/// The tree is generic over the hasher `S` used for its internal, node-keyed
47/// maps — pinned to the source graph's [`Graph::Hasher`](crate::graph::Graph::Hasher) by
48/// [`compute_dominators`] — rather than hardcoding a concrete one. It defaults
49/// to [`FxBuildHasher`] so the common `DominatorTree<NodeId>` spelling keeps
50/// the fast, deterministic hasher that graph consumers (e.g. qcode's `Context`)
51/// select.
52pub struct DominatorTree<N: Copy + Hash + Eq, S: BuildHasher + Default = FxBuildHasher> {
53    root: N,
54    idom: HashMap<N, N, S>,
55    preds: HashMap<N, Vec<N>, S>,
56    dominator_sets: OnceCell<HashMap<N, HashSet<N, S>, S>>,
57    children: OnceCell<HashMap<N, Vec<N>, S>>,
58    frontier: OnceCell<HashMap<N, HashSet<N, S>, S>>,
59}
60
61impl<N: Copy + Hash + Eq, S: BuildHasher + Default> DominatorTree<N, S> {
62    /// The root of the dominator tree (the CFG entry node).
63    pub fn root(&self) -> N {
64        self.root
65    }
66
67    /// Returns the immediate dominator of `node`, or `None` if `node` is the
68    /// root or was not in the analyzed subgraph.
69    pub fn immediate_dominator(&self, node: N) -> Option<N> {
70        self.idom.get(&node).copied()
71    }
72
73    /// Returns the children of `node` in the dominator tree — the nodes for
74    /// which `node` is the immediate dominator.
75    pub fn children_of(&self, node: N) -> &[N] {
76        self.children_map()
77            .get(&node)
78            .map(Vec::as_slice)
79            .unwrap_or(&[])
80    }
81
82    /// Returns `true` if `dominator` dominates `node`.
83    ///
84    /// Every node dominates itself. Triggers lazy set computation on first
85    /// call.
86    pub fn dominates(&self, dominator: N, node: N) -> bool {
87        self.dominator_sets_map()
88            .get(&node)
89            .is_some_and(|s| s.contains(&dominator))
90    }
91
92    /// Returns the set of all nodes that dominate `node`, or `None` if `node`
93    /// was not in the analyzed subgraph.
94    ///
95    /// Triggers lazy set computation on first call.
96    pub fn dominator_set(&self, node: N) -> Option<&HashSet<N, S>> {
97        self.dominator_sets_map().get(&node)
98    }
99
100    fn children_map(&self) -> &HashMap<N, Vec<N>, S> {
101        self.children.get_or_init(|| {
102            let mut map: HashMap<N, Vec<N>, S> = HashMap::default();
103            for (&n, &parent) in &self.idom {
104                map.entry(parent).or_default().push(n);
105            }
106            map
107        })
108    }
109
110    /// Returns the dominator frontier for every node in the analyzed subgraph.
111    ///
112    /// The dominator frontier of `n` is the set of nodes `y` such that `n`
113    /// dominates a predecessor of `y` but does not strictly dominate `y`.
114    /// Frontiers are used by SSA construction to determine phi-node placement.
115    ///
116    /// Computed lazily on first call using the Cytron et al. algorithm.
117    pub fn dominator_frontier(&self) -> &HashMap<N, HashSet<N, S>, S> {
118        self.frontier.get_or_init(|| {
119            let mut all_nodes: HashSet<N, S> = self.idom.keys().copied().collect();
120            all_nodes.insert(self.root);
121
122            let mut frontier: HashMap<N, HashSet<N, S>, S> =
123                all_nodes.iter().map(|&n| (n, HashSet::default())).collect();
124
125            for (&n, preds) in &self.preds {
126                let idom_n = match self.idom.get(&n) {
127                    Some(&d) => d,
128                    None => continue,
129                };
130
131                if preds.len() < 2 {
132                    continue;
133                }
134
135                for &p in preds {
136                    let mut runner = p;
137                    while runner != idom_n {
138                        frontier.entry(runner).or_default().insert(n);
139                        match self.idom.get(&runner) {
140                            Some(&parent) => runner = parent,
141                            None => break,
142                        }
143                    }
144                }
145            }
146
147            frontier
148        })
149    }
150
151    fn dominator_sets_map(&self) -> &HashMap<N, HashSet<N, S>, S> {
152        self.dominator_sets
153            .get_or_init(|| compute_dominator_sets(&self.idom, self.root))
154    }
155}
156
157// ---------------------------------------------------------------------------
158// compute_dominators
159// ---------------------------------------------------------------------------
160
161/// Computes the dominator tree for all nodes reachable from `root`.
162///
163/// Runs the Lengauer–Tarjan algorithm (O(n α(n))). Returns a [`DominatorTree`]
164/// whose immediate-dominator map is available immediately; full dominator sets
165/// and the children map are available lazily via [`DominatorTree::dominator_set`]
166/// and [`DominatorTree::children_of`].
167pub fn compute_dominators<C: Cfg>(
168    graph: &C,
169    root: C::NodeId,
170) -> DominatorTree<C::NodeId, C::Hasher> {
171    let nodes = reachable_from_root(graph, root);
172    let node_set: HashSet<C::NodeId, C::Hasher> = nodes.iter().copied().collect();
173
174    let result = run_lengauer_tarjan(graph, &node_set, root);
175
176    DominatorTree {
177        root,
178        idom: result.idom,
179        preds: result.preds,
180        dominator_sets: OnceCell::new(),
181        children: OnceCell::new(),
182        frontier: OnceCell::new(),
183    }
184}
185
186// ---------------------------------------------------------------------------
187// Private helpers
188// ---------------------------------------------------------------------------
189
190/// Materializes full dominator sets from the immediate-dominator map.
191///
192/// For each node `n`, `dom[n]` is the set of every node on the path from the
193/// root to `n` in the dominator tree, including `n` itself and the root.
194fn compute_dominator_sets<N: Copy + Hash + Eq, S: BuildHasher + Default>(
195    idom: &HashMap<N, N, S>,
196    root: N,
197) -> HashMap<N, HashSet<N, S>, S> {
198    let mut all_nodes: HashSet<N, S> = idom.keys().copied().collect();
199    all_nodes.insert(root);
200
201    let mut out = HashMap::default();
202    for n in all_nodes {
203        let mut set = HashSet::default();
204        let mut cursor = n;
205        loop {
206            set.insert(cursor);
207            match idom.get(&cursor) {
208                Some(&parent) => cursor = parent,
209                None => break,
210            }
211        }
212        out.insert(n, set);
213    }
214    out
215}
216
217struct TarjanResult<C: Cfg> {
218    idom: HashMap<C::NodeId, C::NodeId, C::Hasher>,
219    preds: HashMap<C::NodeId, Vec<C::NodeId>, C::Hasher>,
220}
221
222impl<C: Cfg> Default for TarjanResult<C> {
223    fn default() -> Self {
224        Self {
225            idom: HashMap::default(),
226            preds: HashMap::default(),
227        }
228    }
229}
230
231/// Runs the Lengauer–Tarjan algorithm and returns the idom map and predecessor
232/// map for the reachable subgraph.
233fn run_lengauer_tarjan<C: Cfg>(
234    graph: &C,
235    node_set: &HashSet<C::NodeId, C::Hasher>,
236    root: C::NodeId,
237) -> TarjanResult<C> {
238    let mut state = LtState::new(graph, node_set);
239    state.dfs(root, 0);
240
241    let n = state.last_index();
242    if n == 0 {
243        return TarjanResult::default();
244    }
245
246    for w in (2..=n).rev() {
247        let preds = state.pred[w].clone();
248        for v in preds {
249            let u = state.eval(v);
250            if state.semi[u] < state.semi[w] {
251                state.semi[w] = state.semi[u];
252            }
253        }
254
255        let semi_w = state.semi[w];
256        state.bucket[semi_w].push(w);
257
258        let p = state.parent[w];
259        state.link(p, w);
260
261        let bucket_parent = std::mem::take(&mut state.bucket[p]);
262        for v in bucket_parent {
263            let u = state.eval(v);
264            if state.semi[u] < state.semi[v] {
265                state.idom[v] = u;
266            } else {
267                state.idom[v] = p;
268            }
269        }
270    }
271
272    for w in 2..=n {
273        if state.idom[w] != state.semi[w] {
274            state.idom[w] = state.idom[state.idom[w]];
275        }
276    }
277
278    // Convert DFS-index structures to NodeId maps.
279    let mut result = TarjanResult::default();
280
281    let root_node = state.vertex[1].expect("root must have DFS index 1");
282    result.preds.entry(root_node).or_default();
283
284    for w in 2..=n {
285        let node = state.vertex[w].expect("DFS index must map to a node");
286        let idom_idx = state.idom[w];
287        let idom_node = state.vertex[idom_idx].expect("idom index must map to a node");
288        result.idom.insert(node, idom_node);
289
290        let pred_nodes: Vec<C::NodeId> = state.pred[w]
291            .iter()
292            .map(|&idx| state.vertex[idx].expect("pred index must map to a node"))
293            .collect();
294        result.preds.insert(node, pred_nodes);
295    }
296
297    result
298}
299
300// ---------------------------------------------------------------------------
301// Lengauer–Tarjan internal state
302// ---------------------------------------------------------------------------
303
304struct LtState<'graph, C: Cfg> {
305    graph: &'graph C,
306    node_set: &'graph HashSet<C::NodeId, C::Hasher>,
307
308    number: HashMap<C::NodeId, usize, C::Hasher>,
309    vertex: Vec<Option<C::NodeId>>, // 1-based DFS index -> node
310    parent: Vec<usize>,
311    semi: Vec<usize>,
312    idom: Vec<usize>,
313    ancestor: Vec<usize>,
314    label: Vec<usize>,
315    bucket: Vec<Vec<usize>>,
316    pred: Vec<Vec<usize>>,
317}
318
319impl<'graph, C: Cfg> LtState<'graph, C> {
320    fn new(graph: &'graph C, node_set: &'graph HashSet<C::NodeId, C::Hasher>) -> Self {
321        Self {
322            graph,
323            node_set,
324            number: HashMap::default(),
325            vertex: vec![None],
326            parent: vec![0],
327            semi: vec![0],
328            idom: vec![0],
329            ancestor: vec![0],
330            label: vec![0],
331            bucket: vec![Vec::new()],
332            pred: vec![Vec::new()],
333        }
334    }
335
336    fn last_index(&self) -> usize {
337        self.vertex.len().saturating_sub(1)
338    }
339
340    fn push_vertex(&mut self, node: C::NodeId, parent: usize) -> usize {
341        let idx = self.vertex.len();
342        self.number.insert(node, idx);
343        self.vertex.push(Some(node));
344        self.parent.push(parent);
345        self.semi.push(idx);
346        self.idom.push(0);
347        self.ancestor.push(0);
348        self.label.push(idx);
349        self.bucket.push(Vec::new());
350        self.pred.push(Vec::new());
351        idx
352    }
353
354    fn dfs(&mut self, node: C::NodeId, parent: usize) {
355        if self.number.contains_key(&node) || !self.node_set.contains(&node) {
356            return;
357        }
358
359        let node_idx = self.push_vertex(node, parent);
360
361        let succs: Vec<C::NodeId> = self.graph.successors(node).collect();
362        for succ in succs {
363            if !self.node_set.contains(&succ) {
364                continue;
365            }
366
367            if !self.number.contains_key(&succ) {
368                self.dfs(succ, node_idx);
369            }
370
371            if let Some(&succ_idx) = self.number.get(&succ) {
372                self.pred[succ_idx].push(node_idx);
373            }
374        }
375    }
376
377    fn link(&mut self, parent: usize, child: usize) {
378        self.ancestor[child] = parent;
379    }
380
381    fn compress(&mut self, v: usize) {
382        let a = self.ancestor[v];
383        if a != 0 {
384            let aa = self.ancestor[a];
385            if aa != 0 {
386                self.compress(a);
387
388                if self.semi[self.label[a]] < self.semi[self.label[v]] {
389                    self.label[v] = self.label[a];
390                }
391
392                self.ancestor[v] = self.ancestor[a];
393            }
394        }
395    }
396
397    fn eval(&mut self, v: usize) -> usize {
398        if self.ancestor[v] == 0 {
399            return self.label[v];
400        }
401
402        self.compress(v);
403        let a = self.ancestor[v];
404        if a != 0 && self.semi[self.label[a]] < self.semi[self.label[v]] {
405            self.label[v] = self.label[a];
406        }
407
408        self.label[v]
409    }
410}
411
412// ---------------------------------------------------------------------------
413// Tests
414// ---------------------------------------------------------------------------
415
416#[cfg(test)]
417mod tests {
418    use std::collections::HashSet;
419
420    use jstd_derive::Identifier;
421
422    use super::{compute_dominators, reachable_from_root};
423    use crate::graph::owning::OwningGraph;
424
425    #[derive(Identifier)]
426    struct NodeId(usize);
427
428    #[derive(Identifier)]
429    struct EdgeId(usize);
430
431    type TestGraph = OwningGraph<NodeId, EdgeId, (), ()>;
432
433    #[test]
434    fn computes_reachability_from_root() {
435        let mut graph = TestGraph::default();
436
437        let a = graph.make_node(());
438        let b = graph.make_node(());
439        let c = graph.make_node(());
440        let d = graph.make_node(());
441        let x = graph.make_node(());
442
443        graph.make_edge(a, b, ());
444        graph.make_edge(b, c, ());
445        graph.make_edge(c, d, ());
446
447        let reachable: HashSet<_> = reachable_from_root(&graph, a).into_iter().collect();
448
449        assert!(reachable.contains(&a));
450        assert!(reachable.contains(&b));
451        assert!(reachable.contains(&c));
452        assert!(reachable.contains(&d));
453        assert!(!reachable.contains(&x));
454    }
455
456    #[test]
457    fn computes_dominators_on_diamond() {
458        // a -> b, a -> c, b -> d, c -> d, d -> e
459        let mut graph = TestGraph::default();
460        let a = graph.make_node(());
461        let b = graph.make_node(());
462        let c = graph.make_node(());
463        let d = graph.make_node(());
464        let e = graph.make_node(());
465        graph.make_edge(a, b, ());
466        graph.make_edge(a, c, ());
467        graph.make_edge(b, d, ());
468        graph.make_edge(c, d, ());
469        graph.make_edge(d, e, ());
470
471        let tree = compute_dominators(&graph, a);
472
473        assert_eq!(tree.dominator_set(a), Some(&HashSet::from_iter([a])));
474        assert!(tree.dominates(a, b) && tree.dominates(b, b));
475        assert!(tree.dominates(a, c) && tree.dominates(c, c));
476        assert!(tree.dominates(a, d));
477        assert!(!tree.dominates(b, d));
478        assert!(!tree.dominates(c, d));
479        assert!(tree.dominates(a, e) && tree.dominates(d, e) && tree.dominates(e, e));
480    }
481
482    #[test]
483    fn immediate_dominator_on_chain() {
484        // a -> b -> c -> d
485        let mut graph = TestGraph::default();
486        let a = graph.make_node(());
487        let b = graph.make_node(());
488        let c = graph.make_node(());
489        let d = graph.make_node(());
490        graph.make_edge(a, b, ());
491        graph.make_edge(b, c, ());
492        graph.make_edge(c, d, ());
493
494        let tree = compute_dominators(&graph, a);
495
496        assert_eq!(tree.immediate_dominator(b), Some(a));
497        assert_eq!(tree.immediate_dominator(c), Some(b));
498        assert_eq!(tree.immediate_dominator(d), Some(c));
499        assert_eq!(tree.immediate_dominator(a), None);
500    }
501
502    #[test]
503    fn immediate_dominator_on_diamond() {
504        // a -> b, a -> c, b -> d, c -> d
505        let mut graph = TestGraph::default();
506        let a = graph.make_node(());
507        let b = graph.make_node(());
508        let c = graph.make_node(());
509        let d = graph.make_node(());
510        graph.make_edge(a, b, ());
511        graph.make_edge(a, c, ());
512        graph.make_edge(b, d, ());
513        graph.make_edge(c, d, ());
514
515        let tree = compute_dominators(&graph, a);
516
517        assert_eq!(tree.immediate_dominator(b), Some(a));
518        assert_eq!(tree.immediate_dominator(c), Some(a));
519        assert_eq!(tree.immediate_dominator(d), Some(a));
520        assert_eq!(tree.immediate_dominator(a), None);
521    }
522
523    #[test]
524    fn dominator_frontier_on_diamond() {
525        // a -> b, a -> c, b -> d, c -> d
526        // idom: b=a, c=a, d=a
527        // DF(a)={}, DF(b)={d}, DF(c)={d}, DF(d)={}
528        let mut graph = TestGraph::default();
529        let a = graph.make_node(());
530        let b = graph.make_node(());
531        let c = graph.make_node(());
532        let d = graph.make_node(());
533        graph.make_edge(a, b, ());
534        graph.make_edge(a, c, ());
535        graph.make_edge(b, d, ());
536        graph.make_edge(c, d, ());
537
538        let tree = compute_dominators(&graph, a);
539        let df = tree.dominator_frontier();
540
541        assert_eq!(df[&a], HashSet::default());
542        assert_eq!(df[&b], HashSet::from_iter([d]));
543        assert_eq!(df[&c], HashSet::from_iter([d]));
544        assert_eq!(df[&d], HashSet::default());
545    }
546
547    #[test]
548    fn dominator_frontier_on_loop() {
549        // a -> b -> c -> b (back-edge), b -> d
550        // idom: b=a, c=b, d=b
551        // DF(b)={b} (b is its own frontier due to the back-edge), DF(c)={b}, DF(d)={}
552        let mut graph = TestGraph::default();
553        let a = graph.make_node(());
554        let b = graph.make_node(());
555        let c = graph.make_node(());
556        let d = graph.make_node(());
557        graph.make_edge(a, b, ());
558        graph.make_edge(b, c, ());
559        graph.make_edge(c, b, ());
560        graph.make_edge(b, d, ());
561
562        let tree = compute_dominators(&graph, a);
563        let df = tree.dominator_frontier();
564
565        assert_eq!(df[&a], HashSet::default());
566        assert_eq!(df[&b], HashSet::from_iter([b]));
567        assert_eq!(df[&c], HashSet::from_iter([b]));
568        assert_eq!(df[&d], HashSet::default());
569    }
570
571    #[test]
572    fn dominator_tree_children_on_diamond() {
573        // a -> b, a -> c, b -> d, c -> d  =>  idom(b)=a, idom(c)=a, idom(d)=a
574        let mut graph = TestGraph::default();
575        let a = graph.make_node(());
576        let b = graph.make_node(());
577        let c = graph.make_node(());
578        let d = graph.make_node(());
579        graph.make_edge(a, b, ());
580        graph.make_edge(a, c, ());
581        graph.make_edge(b, d, ());
582        graph.make_edge(c, d, ());
583
584        let tree = compute_dominators(&graph, a);
585
586        let mut a_children: Vec<_> = tree.children_of(a).to_vec();
587        a_children.sort_by_key(|id| Into::<usize>::into(*id));
588        assert_eq!(a_children, vec![b, c, d]);
589        assert!(tree.children_of(d).is_empty());
590    }
591}