Skip to main content

oxicuda_graph/analysis/
dominance.rs

1//! Dominator tree computation using Cooper et al.'s algorithm.
2//!
3//! A node `d` **dominates** node `n` if every path from the graph's entry
4//! (virtual root) to `n` passes through `d`. The dominator tree encodes
5//! this relationship compactly: `d` is the parent of `n` in the tree iff
6//! `d` is the **immediate dominator** of `n` (the closest dominator).
7//!
8//! # Why dominators matter for GPU graphs
9//!
10//! * **Operator fusion** — two nodes can be fused only if one dominates the
11//!   other *and* there are no side-effecting nodes between them.
12//! * **Stream partitioning** — nodes in the same dominator subtree that are
13//!   independent of each other are candidates for concurrent execution.
14//! * **Memory lifetime shortening** — a buffer's live range can be shortened
15//!   to the subtree dominated by its definition.
16//!
17//! # Algorithm
18//!
19//! We implement Cooper, Harvey, and Kennedy (2001): "A Simple, Fast Dominance
20//! Algorithm". The graph is rooted at a virtual super-source that has edges
21//! to all real source nodes, giving a single entry point.
22//!
23//! 1. Compute a reverse-post-order (RPO) numbering via iterative DFS from
24//!    the virtual root.
25//! 2. Initialise `idom[entry] = entry`; all others undefined.
26//! 3. Repeat until convergence: for each node `b` in RPO order (excluding
27//!    entry), set `new_idom` to the intersect of all processed predecessors;
28//!    update `idom[b]` if it changed.
29//! 4. Strip the virtual root from the output idom array.
30//!
31//! Time complexity: O(n²) worst-case, O(n) in practice for structured graphs.
32
33use crate::error::{GraphError, GraphResult};
34use crate::graph::ComputeGraph;
35use crate::node::NodeId;
36
37// ---------------------------------------------------------------------------
38// DomTree — the dominator tree
39// ---------------------------------------------------------------------------
40
41/// The dominator tree of a computation graph.
42///
43/// The tree is rooted at a virtual **root node** that is not part of the
44/// original graph. Every real node has a unique immediate dominator, except
45/// source nodes whose only dominator is the virtual root (represented by
46/// `idom(n) == None`).
47#[derive(Debug, Clone)]
48pub struct DomTree {
49    /// Immediate dominator of each node (indexed by NodeId.0).
50    /// `None` means the node is dominated only by the virtual root.
51    idom: Vec<Option<NodeId>>,
52    /// Children in the dominator tree (indexed by NodeId.0 for real nodes).
53    children: Vec<Vec<NodeId>>,
54    /// Number of real nodes.
55    n_nodes: usize,
56}
57
58impl DomTree {
59    /// Returns the immediate dominator of `node`, or `None` if it is a root.
60    #[must_use]
61    pub fn idom(&self, node: NodeId) -> Option<NodeId> {
62        self.idom.get(node.0 as usize).copied().flatten()
63    }
64
65    /// Returns the children of `node` in the dominator tree.
66    pub fn children(&self, node: NodeId) -> &[NodeId] {
67        self.children
68            .get(node.0 as usize)
69            .map(|v| v.as_slice())
70            .unwrap_or(&[])
71    }
72
73    /// Returns all root nodes (nodes with no immediate dominator in the real graph).
74    pub fn roots(&self) -> Vec<NodeId> {
75        (0..self.n_nodes)
76            .filter(|&i| self.idom[i].is_none())
77            .map(|i| NodeId(i as u32))
78            .collect()
79    }
80
81    /// Returns `true` if `dominator` dominates `node`.
82    ///
83    /// A node always dominates itself.
84    #[must_use]
85    pub fn dominates(&self, dominator: NodeId, node: NodeId) -> bool {
86        if dominator == node {
87            return true;
88        }
89        let mut cur = node;
90        loop {
91            match self.idom(cur) {
92                None => return false,
93                Some(p) if p == dominator => return true,
94                Some(p) => cur = p,
95            }
96        }
97    }
98
99    /// Returns the set of nodes dominated by `root` (its subtree), including `root` itself.
100    pub fn dominated_by(&self, root: NodeId) -> Vec<NodeId> {
101        let mut result = Vec::new();
102        let mut stack = vec![root];
103        while let Some(n) = stack.pop() {
104            result.push(n);
105            for &child in self.children(n) {
106                stack.push(child);
107            }
108        }
109        result
110    }
111
112    /// Returns the depth of `node` in the dominator tree (root = 0).
113    #[must_use]
114    pub fn depth(&self, node: NodeId) -> usize {
115        let mut d = 0usize;
116        let mut cur = node;
117        loop {
118            match self.idom(cur) {
119                None => return d,
120                Some(p) => {
121                    d += 1;
122                    cur = p;
123                }
124            }
125        }
126    }
127
128    /// Returns the lowest common ancestor of two nodes in the dominator tree.
129    ///
130    /// If one node dominates the other, it is the LCA.
131    #[must_use]
132    pub fn lca(&self, a: NodeId, b: NodeId) -> NodeId {
133        let da = self.depth(a);
134        let db = self.depth(b);
135        let mut x = a;
136        let mut y = b;
137        // Bring the deeper one up to the same level.
138        let (shallow, deep, diff) = if da <= db {
139            (x, y, db - da)
140        } else {
141            (y, x, da - db)
142        };
143        let mut deep = deep;
144        for _ in 0..diff {
145            deep = self.idom(deep).unwrap_or(deep);
146        }
147        x = shallow;
148        y = deep;
149        // Walk up together until they meet.
150        let mut guard = self.n_nodes + 1;
151        while x != y {
152            x = self.idom(x).unwrap_or(x);
153            y = self.idom(y).unwrap_or(y);
154            if guard == 0 {
155                break; // safety: disconnected components
156            }
157            guard -= 1;
158        }
159        x
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Cooper's "intersect" helper
165// ---------------------------------------------------------------------------
166
167/// Finds the common dominator of two nodes using the idom chain.
168///
169/// `rpo[v]` is the reverse-post-order position of node `v` (lower = earlier).
170/// `idom_raw[v]` = raw immediate dominator index (virtual-root-inclusive).
171/// The virtual root is at index `vroot` with `idom_raw[vroot] = vroot`.
172fn intersect(mut b1: usize, mut b2: usize, idom_raw: &[usize], rpo: &[usize]) -> usize {
173    while b1 != b2 {
174        while rpo[b1] > rpo[b2] {
175            b1 = idom_raw[b1];
176        }
177        while rpo[b2] > rpo[b1] {
178            b2 = idom_raw[b2];
179        }
180    }
181    b1
182}
183
184// ---------------------------------------------------------------------------
185// analyse — entry point
186// ---------------------------------------------------------------------------
187
188/// Computes the dominator tree of `graph` using Cooper's algorithm.
189///
190/// A virtual super-source is introduced with edges to all source nodes so
191/// the graph has a single entry point. The resulting tree's roots are the
192/// real source nodes (those with `idom == None`).
193///
194/// # Errors
195///
196/// Returns [`GraphError::EmptyGraph`] if there are no nodes.
197pub fn analyse(graph: &ComputeGraph) -> GraphResult<DomTree> {
198    if graph.is_empty() {
199        return Err(GraphError::EmptyGraph);
200    }
201
202    let n_real = graph.node_count();
203    let vroot = n_real; // virtual root index (beyond real nodes)
204    let total = n_real + 1;
205
206    // Build adjacency lists augmented with the virtual root.
207    let mut succ: Vec<Vec<usize>> = vec![Vec::new(); total];
208    let mut pred: Vec<Vec<usize>> = vec![Vec::new(); total];
209
210    for (from, to) in graph.edges() {
211        let f = from.0 as usize;
212        let t = to.0 as usize;
213        succ[f].push(t);
214        pred[t].push(f);
215    }
216    for src in graph.sources() {
217        succ[vroot].push(src.0 as usize);
218        pred[src.0 as usize].push(vroot);
219    }
220
221    // ---- Compute reverse-post-order (RPO) via iterative DFS from vroot ----
222    //
223    // We push nodes onto `rpo_order` when we finish processing them
224    // (post-order), then reverse to get RPO.
225    let mut rpo_order: Vec<usize> = Vec::with_capacity(total);
226    let mut visited = vec![false; total];
227    // Stack: (node, next_successor_index).
228    let mut dfs_stack: Vec<(usize, usize)> = vec![(vroot, 0)];
229    visited[vroot] = true;
230
231    while let Some((node, idx)) = dfs_stack.last_mut() {
232        if *idx < succ[*node].len() {
233            let child = succ[*node][*idx];
234            *idx += 1;
235            if !visited[child] {
236                visited[child] = true;
237                dfs_stack.push((child, 0));
238            }
239        } else {
240            // All successors processed: emit node in post-order.
241            let n = *node;
242            dfs_stack.pop();
243            rpo_order.push(n);
244        }
245    }
246
247    // Reverse post-order: entry (vroot) gets rpo=0.
248    rpo_order.reverse();
249
250    // rpo[node] = position in RPO (smaller = earlier = closer to entry).
251    let mut rpo = vec![usize::MAX; total];
252    for (i, &node) in rpo_order.iter().enumerate() {
253        rpo[node] = i;
254    }
255
256    // ---- Cooper's dominance algorithm ----------------------------------------
257
258    const UNDEF: usize = usize::MAX;
259    // idom_raw uses raw indices (including vroot).
260    let mut idom_raw = vec![UNDEF; total];
261    idom_raw[vroot] = vroot; // virtual root is its own immediate dominator
262
263    let mut changed = true;
264    while changed {
265        changed = false;
266        // Process all nodes in RPO order, skip the virtual root (rpo[vroot]=0).
267        for &b in &rpo_order[1..] {
268            // Find a "new_idom" that is the intersection of all processed predecessors.
269            let mut new_idom = UNDEF;
270            for &p in &pred[b] {
271                if idom_raw[p] == UNDEF {
272                    continue; // predecessor not yet processed
273                }
274                if new_idom == UNDEF {
275                    new_idom = p;
276                } else {
277                    new_idom = intersect(p, new_idom, &idom_raw, &rpo);
278                }
279            }
280            if new_idom != UNDEF && idom_raw[b] != new_idom {
281                idom_raw[b] = new_idom;
282                changed = true;
283            }
284        }
285    }
286
287    // ---- Extract real-node idom (virtual root → None) ------------------------
288    let mut idom_out: Vec<Option<NodeId>> = vec![None; n_real];
289    for i in 0..n_real {
290        let raw = idom_raw[i];
291        if raw == UNDEF || raw == vroot {
292            idom_out[i] = None; // root in the dominator tree
293        } else {
294            idom_out[i] = Some(NodeId(raw as u32));
295        }
296    }
297
298    // ---- Build children map ---------------------------------------------------
299    let mut children: Vec<Vec<NodeId>> = vec![Vec::new(); n_real];
300    for (i, &idom_opt) in idom_out.iter().enumerate() {
301        if let Some(parent) = idom_opt {
302            children[parent.0 as usize].push(NodeId(i as u32));
303        }
304    }
305
306    Ok(DomTree {
307        idom: idom_out,
308        children,
309        n_nodes: n_real,
310    })
311}
312
313// ---------------------------------------------------------------------------
314// Tests
315// ---------------------------------------------------------------------------
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use crate::builder::GraphBuilder;
321
322    fn make_chain(n: usize) -> (ComputeGraph, Vec<NodeId>) {
323        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
324        let ids: Vec<NodeId> = (0..n).map(|_| b.add_barrier("x")).collect();
325        for w in ids.windows(2) {
326            b.dep(w[0], w[1]);
327        }
328        let g = b.build().unwrap();
329        (g, ids)
330    }
331
332    #[test]
333    fn dominance_empty_graph_error() {
334        let g = ComputeGraph::new();
335        assert!(matches!(analyse(&g), Err(GraphError::EmptyGraph)));
336    }
337
338    #[test]
339    fn dominance_single_node_is_root() {
340        let (g, ids) = make_chain(1);
341        let dt = analyse(&g).unwrap();
342        assert!(dt.idom(ids[0]).is_none());
343        assert_eq!(dt.roots(), vec![ids[0]]);
344    }
345
346    #[test]
347    fn dominance_linear_chain() {
348        // a→b→c→d: each node has the previous as idom.
349        let (g, ids) = make_chain(4);
350        let dt = analyse(&g).unwrap();
351        assert!(dt.idom(ids[0]).is_none()); // source
352        assert_eq!(dt.idom(ids[1]), Some(ids[0]));
353        assert_eq!(dt.idom(ids[2]), Some(ids[1]));
354        assert_eq!(dt.idom(ids[3]), Some(ids[2]));
355    }
356
357    #[test]
358    fn dominance_diamond() {
359        // a→b, a→c, b→d, c→d
360        // idom(b)=a, idom(c)=a, idom(d)=a (a is common dominator of d)
361        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
362        let a = b.add_barrier("a");
363        let bnode = b.add_barrier("b");
364        let c = b.add_barrier("c");
365        let d = b.add_barrier("d");
366        b.dep(a, bnode).dep(a, c).dep(bnode, d).dep(c, d);
367        let g = b.build().unwrap();
368        let dt = analyse(&g).unwrap();
369        assert!(dt.idom(a).is_none());
370        assert_eq!(dt.idom(bnode), Some(a));
371        assert_eq!(dt.idom(c), Some(a));
372        // d is reachable from a via two paths (b and c); idom(d) = a.
373        assert_eq!(dt.idom(d), Some(a));
374    }
375
376    #[test]
377    fn dominance_dominates_reflexive() {
378        let (g, ids) = make_chain(3);
379        let dt = analyse(&g).unwrap();
380        assert!(dt.dominates(ids[0], ids[0]));
381        assert!(dt.dominates(ids[1], ids[1]));
382    }
383
384    #[test]
385    fn dominance_dominates_transitive() {
386        let (g, ids) = make_chain(4);
387        let dt = analyse(&g).unwrap();
388        // ids[0] dominates all subsequent nodes in a chain.
389        assert!(dt.dominates(ids[0], ids[1]));
390        assert!(dt.dominates(ids[0], ids[2]));
391        assert!(dt.dominates(ids[0], ids[3]));
392        assert!(!dt.dominates(ids[3], ids[0]));
393    }
394
395    #[test]
396    fn dominance_dominated_by_subtree() {
397        let (g, ids) = make_chain(4);
398        let dt = analyse(&g).unwrap();
399        // ids[1] dominates ids[2] and ids[3] (the rest of the chain).
400        let sub = dt.dominated_by(ids[1]);
401        assert!(sub.contains(&ids[1]));
402        assert!(sub.contains(&ids[2]));
403        assert!(sub.contains(&ids[3]));
404        assert!(!sub.contains(&ids[0]));
405    }
406
407    #[test]
408    fn dominance_depth_linear() {
409        let (g, ids) = make_chain(4);
410        let dt = analyse(&g).unwrap();
411        assert_eq!(dt.depth(ids[0]), 0);
412        assert_eq!(dt.depth(ids[1]), 1);
413        assert_eq!(dt.depth(ids[2]), 2);
414        assert_eq!(dt.depth(ids[3]), 3);
415    }
416
417    #[test]
418    fn dominance_lca_same_node() {
419        let (g, ids) = make_chain(3);
420        let dt = analyse(&g).unwrap();
421        assert_eq!(dt.lca(ids[1], ids[1]), ids[1]);
422    }
423
424    #[test]
425    fn dominance_lca_diamond() {
426        // Diamond: a→b, a→c, b→d, c→d
427        // lca(b, c) should be a (they share idom a)
428        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
429        let a = b.add_barrier("a");
430        let bnode = b.add_barrier("b");
431        let c = b.add_barrier("c");
432        let d = b.add_barrier("d");
433        b.dep(a, bnode).dep(a, c).dep(bnode, d).dep(c, d);
434        let g = b.build().unwrap();
435        let dt = analyse(&g).unwrap();
436        // b and c both have idom = a, so lca(b,c) should be a.
437        let result = dt.lca(bnode, c);
438        // LCA of two siblings in domtree is their common idom = a.
439        assert_eq!(result, a);
440        // lca of a node with its ancestor:
441        assert_eq!(dt.lca(bnode, d), a); // idom(d)=a, so a dominates both
442    }
443
444    #[test]
445    fn dominance_children() {
446        let (g, ids) = make_chain(3);
447        let dt = analyse(&g).unwrap();
448        // In a linear chain: children of ids[0] = [ids[1]].
449        assert_eq!(dt.children(ids[0]), &[ids[1]]);
450        assert_eq!(dt.children(ids[1]), &[ids[2]]);
451        assert!(dt.children(ids[2]).is_empty());
452    }
453
454    #[test]
455    fn dominance_fork_join_children() {
456        // a→b, a→c, a→d; b→e, c→e, d→e
457        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
458        let a = b.add_barrier("a");
459        let b1 = b.add_barrier("b");
460        let c = b.add_barrier("c");
461        let d = b.add_barrier("d");
462        let e = b.add_barrier("e");
463        b.fan_out(a, &[b1, c, d]);
464        b.fan_in(&[b1, c, d], e);
465        let g = b.build().unwrap();
466        let dt = analyse(&g).unwrap();
467        // a dominates all; e is dominated by a (convergence point).
468        assert!(dt.dominates(a, e));
469        // b, c, d each dominated by a.
470        assert_eq!(dt.idom(b1), Some(a));
471        assert_eq!(dt.idom(c), Some(a));
472        assert_eq!(dt.idom(d), Some(a));
473    }
474
475    #[test]
476    fn dominance_two_independent_nodes() {
477        // Two completely isolated nodes — both are roots.
478        let mut b = GraphBuilder::new().with_auto_infer_edges(false);
479        let a = b.add_barrier("a");
480        let bnode = b.add_barrier("b");
481        let g = b.build().unwrap();
482        let dt = analyse(&g).unwrap();
483        assert!(dt.idom(a).is_none());
484        assert!(dt.idom(bnode).is_none());
485        let roots = dt.roots();
486        assert_eq!(roots.len(), 2);
487    }
488
489    #[test]
490    fn dominance_longer_chain_all_dominated() {
491        let (g, ids) = make_chain(8);
492        let dt = analyse(&g).unwrap();
493        // ids[0] dominates all others.
494        for &id in &ids[1..] {
495            assert!(dt.dominates(ids[0], id));
496        }
497        // ids[7] (last) does not dominate any predecessor.
498        for &id in &ids[..7] {
499            assert!(!dt.dominates(ids[7], id));
500        }
501    }
502
503    #[test]
504    fn dominance_dominated_by_includes_self() {
505        let (g, ids) = make_chain(3);
506        let dt = analyse(&g).unwrap();
507        let sub = dt.dominated_by(ids[0]);
508        assert!(sub.contains(&ids[0]));
509    }
510}