Skip to main content

vyre_primitives/graph/dominator_tree/
lengauer_tarjan.rs

1// CPU reference oracles  (#[cfg(test)] / feature = "cpu-parity")
2// ------------------------------------------------------------------
3
4use super::alloc_helpers::{push_dominator_vec, resize_dominator_vec};
5
6/// Lengauer–Tarjan exact immediate dominators.
7///
8/// Returns `idom[v]` for every node `v`.  `idom[entry] == entry`.
9/// Unreachable nodes receive `None`.
10///
11/// CPU-only reference algorithm. Gated with the rest of the CPU oracle
12/// surface (`compress`, `eval`, `link`, `cpu_ref`) so default builds
13/// don't pull the implementation through. (Without this gate, default
14/// builds left the body of `lengauer_tarjan_idoms` referencing the
15/// gated-out `eval`/`link`/`compress` helpers and failed with three
16/// E0423/E0425 errors. Reproduced 2026-05-23.)
17#[must_use]
18#[cfg(any(test, feature = "cpu-parity"))]
19pub fn lengauer_tarjan_idoms(
20    node_count: u32,
21    entry: u32,
22    edges: &[(u32, u32)],
23) -> Vec<Option<u32>> {
24    try_lengauer_tarjan_idoms(node_count, entry, edges).unwrap_or_else(|error| panic!("{error}"))
25}
26
27/// Fallible Lengauer-Tarjan exact immediate dominators.
28#[cfg(any(test, feature = "cpu-parity"))]
29pub fn try_lengauer_tarjan_idoms(
30    node_count: u32,
31    entry: u32,
32    edges: &[(u32, u32)],
33) -> Result<Vec<Option<u32>>, String> {
34    let mut idom = Vec::new();
35    let mut scratch = DominatorTreeCpuScratch::default();
36    try_lengauer_tarjan_idoms_into(node_count, entry, edges, &mut idom, &mut scratch)?;
37    Ok(idom)
38}
39
40/// Reusable workspace for dominator-tree CPU oracles.
41#[cfg(any(test, feature = "cpu-parity"))]
42#[derive(Debug, Default)]
43pub struct DominatorTreeCpuScratch {
44    succ: Vec<Vec<usize>>,
45    pred: Vec<Vec<usize>>,
46    semi: Vec<usize>,
47    vertex: Vec<usize>,
48    parent: Vec<usize>,
49    dfs_stack: Vec<(usize, usize)>,
50    ancestor: Vec<usize>,
51    label: Vec<usize>,
52    bucket: Vec<Vec<usize>>,
53    compress_stack: Vec<usize>,
54}
55
56#[cfg(any(test, feature = "cpu-parity"))]
57impl DominatorTreeCpuScratch {
58    /// Construct empty dominator-tree CPU scratch.
59    #[must_use]
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Pre-reserve outer workspace vectors (unit tests for reuse invariants).
65    #[cfg(test)]
66    pub fn reserve_outer_for_test(&mut self, hint: usize, bucket_hint: usize) {
67        self.succ.reserve(hint);
68        self.pred.reserve(hint);
69        self.semi.reserve(hint);
70        self.vertex.reserve(hint.saturating_add(1));
71        self.parent.reserve(hint);
72        self.dfs_stack.reserve(hint);
73        self.ancestor.reserve(hint);
74        self.label.reserve(hint);
75        self.bucket.reserve(bucket_hint);
76        self.compress_stack.reserve(hint);
77    }
78
79    /// Snapshot outer-vector capacities (unit tests for reuse invariants).
80    #[cfg(test)]
81    #[must_use]
82    pub fn outer_capacities(&self) -> [usize; 10] {
83        [
84            self.succ.capacity(),
85            self.pred.capacity(),
86            self.semi.capacity(),
87            self.vertex.capacity(),
88            self.parent.capacity(),
89            self.dfs_stack.capacity(),
90            self.ancestor.capacity(),
91            self.label.capacity(),
92            self.bucket.capacity(),
93            self.compress_stack.capacity(),
94        ]
95    }
96
97    /// Successor adjacency row (unit tests only).
98    #[cfg(test)]
99    #[must_use]
100    pub fn test_succ_row(&self, node: usize) -> &[usize] {
101        &self.succ[node]
102    }
103
104    /// Predecessor adjacency row (unit tests only).
105    #[cfg(test)]
106    #[must_use]
107    pub fn test_pred_row(&self, node: usize) -> &[usize] {
108        &self.pred[node]
109    }
110
111    /// Successor row capacity after reuse (unit tests only).
112    #[cfg(test)]
113    #[must_use]
114    pub fn test_succ_row_capacity(&self, node: usize) -> usize {
115        self.succ[node].capacity()
116    }
117
118    /// Predecessor row capacity after reuse (unit tests only).
119    #[cfg(test)]
120    #[must_use]
121    pub fn test_pred_row_capacity(&self, node: usize) -> usize {
122        self.pred[node].capacity()
123    }
124}
125
126/// Fallible Lengauer-Tarjan exact immediate dominators using caller-owned output and scratch.
127#[cfg(any(test, feature = "cpu-parity"))]
128pub fn try_lengauer_tarjan_idoms_into(
129    node_count: u32,
130    entry: u32,
131    edges: &[(u32, u32)],
132    idom: &mut Vec<Option<u32>>,
133    scratch: &mut DominatorTreeCpuScratch,
134) -> Result<(), String> {
135    let n = node_count as usize;
136    let entry = entry as usize;
137    if n == 0 {
138        idom.clear();
139        return Ok(());
140    }
141    if entry >= n {
142        idom.clear();
143        resize_dominator_vec(idom, n, None, "dominator_tree entry-out-of-range idoms")?;
144        return Ok(());
145    }
146
147    // Build adjacency.
148    resize_dominator_vec(
149        &mut scratch.succ,
150        n,
151        Vec::new(),
152        "dominator_tree successor rows",
153    )?;
154    resize_dominator_vec(
155        &mut scratch.pred,
156        n,
157        Vec::new(),
158        "dominator_tree predecessor rows",
159    )?;
160    for row in scratch.succ.iter_mut().take(n) {
161        row.clear();
162    }
163    for row in scratch.pred.iter_mut().take(n) {
164        row.clear();
165    }
166    for &(u, v) in edges {
167        let u = u as usize;
168        let v = v as usize;
169        if u < n && v < n {
170            push_dominator_vec(&mut scratch.succ[u], v, "dominator_tree successor row")?;
171            push_dominator_vec(&mut scratch.pred[v], u, "dominator_tree predecessor row")?;
172        }
173    }
174
175    // DFS numbering.
176    scratch.semi.clear();
177    scratch.vertex.clear();
178    scratch.parent.clear();
179    resize_dominator_vec(&mut scratch.semi, n, 0usize, "dominator_tree semi numbers")?;
180    resize_dominator_vec(
181        &mut scratch.vertex,
182        n + 1,
183        0usize,
184        "dominator_tree DFS vertices",
185    )?;
186    resize_dominator_vec(&mut scratch.parent, n, 0usize, "dominator_tree DFS parents")?;
187    let mut dfs_num: usize = 0;
188
189    // Iterative DFS to avoid stack overflow on million-node chains.
190    scratch.dfs_stack.clear();
191    push_dominator_vec(
192        &mut scratch.dfs_stack,
193        (entry, 0usize),
194        "dominator_tree DFS stack",
195    )?;
196    while let Some((v, next_idx)) = scratch.dfs_stack.last_mut() {
197        let v = *v;
198        if *next_idx == 0 {
199            dfs_num += 1;
200            scratch.semi[v] = dfs_num;
201            scratch.vertex[dfs_num] = v;
202        }
203        if *next_idx < scratch.succ[v].len() {
204            let w = scratch.succ[v][*next_idx];
205            *next_idx += 1;
206            if scratch.semi[w] == 0 {
207                scratch.parent[w] = v;
208                push_dominator_vec(&mut scratch.dfs_stack, (w, 0), "dominator_tree DFS stack")?;
209            }
210        } else {
211            scratch.dfs_stack.pop();
212        }
213    }
214
215    if dfs_num == 0 {
216        idom.clear();
217        resize_dominator_vec(idom, n, None, "dominator_tree unreachable idoms")?;
218        return Ok(());
219    }
220
221    idom.clear();
222    scratch.ancestor.clear();
223    scratch.label.clear();
224    scratch.bucket.clear();
225    resize_dominator_vec(idom, n, None, "dominator_tree idoms")?;
226    resize_dominator_vec(&mut scratch.ancestor, n, 0usize, "dominator_tree ancestors")?;
227    crate::graph::scratch::reserve_graph_items(
228        &mut scratch.label,
229        n,
230        "dominator tree CPU oracle",
231        "dominator_tree labels",
232    )?;
233    scratch.label.extend(0..n);
234    resize_dominator_vec(
235        &mut scratch.bucket,
236        n + 1,
237        Vec::new(),
238        "dominator_tree buckets",
239    )?;
240    for row in scratch.bucket.iter_mut().take(n + 1) {
241        row.clear();
242    }
243    scratch.compress_stack.clear();
244
245    for i in (1..=dfs_num).rev() {
246        let w = scratch.vertex[i];
247
248        for &v in &scratch.pred[w] {
249            if scratch.semi[v] > 0 {
250                let u = try_eval_with_stack(
251                    v,
252                    &mut scratch.ancestor,
253                    &mut scratch.label,
254                    &scratch.semi,
255                    &mut scratch.compress_stack,
256                )?;
257                if scratch.semi[u] < scratch.semi[w] {
258                    scratch.semi[w] = scratch.semi[u];
259                }
260            }
261        }
262
263        push_dominator_vec(
264            &mut scratch.bucket[scratch.vertex[scratch.semi[w]]],
265            w,
266            "dominator_tree bucket row",
267        )?;
268
269        link(
270            scratch.parent[w],
271            w,
272            &mut scratch.ancestor,
273            &mut scratch.label,
274            &scratch.semi,
275        );
276
277        for &v in &scratch.bucket[scratch.parent[w]] {
278            let u = try_eval_with_stack(
279                v,
280                &mut scratch.ancestor,
281                &mut scratch.label,
282                &scratch.semi,
283                &mut scratch.compress_stack,
284            )?;
285            if scratch.semi[u] < scratch.semi[v] {
286                idom[v] = Some(u as u32);
287            } else {
288                idom[v] = Some(scratch.parent[w] as u32);
289            }
290        }
291        scratch.bucket[scratch.parent[w]].clear();
292    }
293
294    for i in 2..=dfs_num {
295        let w = scratch.vertex[i];
296        if idom[w].map(|x| x as usize) != Some(scratch.vertex[scratch.semi[w]]) {
297            idom[w] = idom[w]
298                .and_then(|parent| idom.get(parent as usize))
299                .copied()
300                .flatten();
301        }
302    }
303
304    idom[entry] = Some(entry as u32);
305
306    // unreachable nodes keep None
307    Ok(())
308}
309
310#[cfg(any(test, feature = "cpu-parity"))]
311fn try_compress(
312    v: usize,
313    ancestor: &mut [usize],
314    label: &mut [usize],
315    semi: &[usize],
316) -> Result<(), String> {
317    let mut stack = Vec::new();
318    try_compress_with_stack(v, ancestor, label, semi, &mut stack)
319}
320
321#[cfg(any(test, feature = "cpu-parity"))]
322fn try_compress_with_stack(
323    v: usize,
324    ancestor: &mut [usize],
325    label: &mut [usize],
326    semi: &[usize],
327    stack: &mut Vec<usize>,
328) -> Result<(), String> {
329    if ancestor[v] == 0 {
330        return Ok(());
331    }
332
333    // Iterative version of the recursive path-compression used in LT.
334    // We walk up the ancestor chain, pushing vertices that are at least
335    // two levels above the root.  When we hit a direct child of the root
336    // we process it in-place (label update, no splice) and then walk
337    // back down the stack, processing and splicing as we go.
338    stack.clear();
339    let mut u = v;
340    while ancestor[u] != 0 {
341        if ancestor[ancestor[u]] != 0 {
342            push_dominator_vec(stack, u, "dominator_tree compression stack")?;
343            u = ancestor[u];
344        } else {
345            // Direct child of the root – "else" branch of the recursive
346            // formulation.  Update label but do NOT splice ancestor.
347            if semi[label[ancestor[u]]] < semi[label[u]] {
348                label[u] = label[ancestor[u]];
349            }
350            break;
351        }
352    }
353
354    // Walk back down, using the freshly-updated labels of ancestors.
355    while let Some(w) = stack.pop() {
356        if semi[label[ancestor[w]]] < semi[label[w]] {
357            label[w] = label[ancestor[w]];
358        }
359        ancestor[w] = ancestor[ancestor[w]];
360    }
361    Ok(())
362}
363
364#[cfg(any(test, feature = "cpu-parity"))]
365fn try_eval(
366    v: usize,
367    ancestor: &mut [usize],
368    label: &mut [usize],
369    semi: &[usize],
370) -> Result<usize, String> {
371    let mut stack = Vec::new();
372    try_eval_with_stack(v, ancestor, label, semi, &mut stack)
373}
374
375#[cfg(any(test, feature = "cpu-parity"))]
376fn try_eval_with_stack(
377    v: usize,
378    ancestor: &mut [usize],
379    label: &mut [usize],
380    semi: &[usize],
381    stack: &mut Vec<usize>,
382) -> Result<usize, String> {
383    if ancestor[v] == 0 {
384        Ok(v)
385    } else {
386        try_compress_with_stack(v, ancestor, label, semi, stack)?;
387        Ok(label[v])
388    }
389}
390
391#[cfg(any(test, feature = "cpu-parity"))]
392fn link(v: usize, w: usize, ancestor: &mut [usize], label: &mut [usize], _semi: &[usize]) {
393    ancestor[w] = v;
394    label[w] = w;
395}