Skip to main content

vyre_primitives/graph/dominator_tree/
program.rs

1//! `dominator_tree`  -  exact immediate-dominator primitive.
2//!
3//! Computes the immediate dominator (`idom`) of every reachable node in a
4//! control-flow graph with a single entry.  The primitive ships both a
5//! Lengauer–Tarjan CPU reference oracle and a serial lane-0 GPU `Program`
6//! builder that implements the Cooper–Harvey–Kennedy iterative fixpoint
7//! using parent-pointer LCA on the idom tree.
8//!
9//! # Wire shape
10//!
11//! ```text
12//! pg_edge_offsets : u32[node_count + 1]   // forward CSR
13//! pg_edge_targets : u32[edge_count]       // forward CSR
14//! pred_offsets    : u32[node_count + 1]   // predecessor CSR
15//! pred_targets    : u32[pred_edge_count]  // predecessor CSR
16//! idom_out        : u32[node_count]       // output idoms; NONE = unreachable
17//! ```
18//!
19//! `idom_out[entry] == entry` for the entry block.  Unreachable nodes keep
20//! the sentinel `NONE` (== `node_count`).
21//!
22//! # Soundness
23//!
24//! Exact for every reducible and irreducible single-entry CFG.  Multi-entry
25//! graphs (no path from entry to some node that has predecessors) are not
26//! rejected explicitly, but the resulting idom tree is undefined for the
27//! disconnected component; callers should run `reachable` first if they need
28//! strict guarantees.
29
30use std::sync::Arc;
31
32use vyre_foundation::ir::model::expr::{GeneratorRef, Ident};
33use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
34
35/// Canonical op id.
36pub const OP_ID: &str = "vyre-primitives::graph::dominator_tree";
37const INIT_PHASE_OP_ID: &str = "vyre-primitives::graph::dominator_tree::init_state";
38const DEPTH_PHASE_OP_ID: &str = "vyre-primitives::graph::dominator_tree::recompute_depth";
39const INTERSECT_PHASE_OP_ID: &str =
40    "vyre-primitives::graph::dominator_tree::intersect_predecessors";
41
42/// Sentinel stored in `idom_out` for unreachable nodes.
43pub const IDOM_NONE: u32 = u32::MAX;
44
45/// Errors from dominator-tree construction.
46#[derive(Debug, Clone, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum DominatorTreeError {
49    /// The requested entry node is outside `0..node_count`.
50    EntryOutOfRange {
51        /// Supplied entry.
52        entry: u32,
53        /// Declared node count.
54        node_count: u32,
55    },
56    /// CSR offset buffer length is inconsistent with `node_count`.
57    BadOffsets {
58        /// Actionable diagnostic.
59        message: String,
60    },
61    /// CSR target buffer references a node outside the valid range.
62    TargetOutOfRange {
63        /// Offending target index.
64        index: usize,
65        /// Offending value.
66        target: u32,
67        /// Declared node count.
68        node_count: u32,
69    },
70    /// Monotonicity violation in CSR offsets.
71    NonMonotonicOffsets {
72        /// Index of first violation.
73        index: usize,
74    },
75}
76
77/// Validated dispatch layout for the dominator-tree primitive.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct DominatorTreeLayout {
80    /// Number of nodes.
81    pub node_count: u32,
82    /// Number of forward edges.
83    pub edge_count: u32,
84    /// Number of predecessor edges.
85    pub pred_edge_count: u32,
86    /// Words in the `idom_out` buffer.
87    pub idom_words: usize,
88    /// Words in the `depth` scratch buffer.
89    pub depth_words: usize,
90}
91
92/// Build a serial lane-0 `Program` that computes exact immediate dominators.
93///
94/// The kernel runs the Cooper–Harvey–Kennedy iterative fixpoint over the
95/// idom tree using predecessor-list LCA.  Workgroup size is `[1, 1, 1]`;
96/// only invocation 0 performs work.
97///
98/// # Panics
99///
100/// Returns an inert early-return trap when `try_dominator_tree_program` rejects
101/// the shape (e.g. `node_count == u32::MAX`, which collides with `IDOM_NONE`).
102#[must_use]
103pub fn dominator_tree_program(
104    node_count: u32,
105    edge_count: u32,
106    pred_edge_count: u32,
107    idom_out: &str,
108) -> Program {
109    match try_dominator_tree_program(node_count, edge_count, pred_edge_count, idom_out) {
110        Ok(p) => p,
111        Err(_) => inert_dominator_tree_program(idom_out),
112    }
113}
114
115/// Checked builder.  Returns an actionable diagnostic instead of a trap
116/// program when the shape overflows buffer counts.
117pub fn try_dominator_tree_program(
118    node_count: u32,
119    edge_count: u32,
120    pred_edge_count: u32,
121    idom_out: &str,
122) -> Result<Program, String> {
123    if node_count == u32::MAX {
124        return Err(
125            "dominator_tree node_count == u32::MAX collides with IDOM_NONE sentinel. \
126             Fix: shard the graph before dispatch."
127                .to_string(),
128        );
129    }
130
131    let offset_count = node_count.checked_add(1).ok_or_else(|| {
132        format!(
133            "dominator_tree node_count={node_count} overflows CSR offset buffer count. \
134                 Fix: shard the graph before GPU dispatch."
135        )
136    })?;
137
138    let lane0 = Expr::eq(Expr::InvocationId { axis: 0 }, Expr::u32(0));
139
140    // ------------------------------------------------------------------
141    // Serial CHK-by-LCA kernel on lane 0.
142    // ------------------------------------------------------------------
143
144    // depth[0] = 0; depth[v] = 0 for all others (will be fixed on first update)
145    let depth_buf = "dt_depth";
146    let init_state = child_phase(
147        INIT_PHASE_OP_ID,
148        vec![
149            // idom_out[v] = NONE for all v
150            Node::loop_for(
151                "i",
152                Expr::u32(0),
153                Expr::u32(node_count),
154                vec![Node::store(idom_out, Expr::var("i"), Expr::u32(IDOM_NONE))],
155            ),
156            // idom_out[0] = 0  (entry dominates itself)
157            Node::store(idom_out, Expr::u32(0), Expr::u32(0)),
158            Node::loop_for(
159                "i",
160                Expr::u32(0),
161                Expr::u32(node_count),
162                vec![Node::store(depth_buf, Expr::var("i"), Expr::u32(0))],
163            ),
164        ],
165    );
166
167    // Outer fixpoint: at most node_count iterations.
168    // Each step: recompute depths, then for each v != entry intersect preds via LCA.
169    let recompute_depth = child_phase(
170        DEPTH_PHASE_OP_ID,
171        vec![Node::loop_for(
172            "v",
173            Expr::u32(0),
174            Expr::u32(node_count),
175            vec![
176                Node::let_bind("d", Expr::u32(0)),
177                Node::let_bind("cur", Expr::var("v")),
178                Node::loop_for(
179                    "depth_step",
180                    Expr::u32(0),
181                    Expr::u32(node_count),
182                    vec![Node::if_then(
183                        Expr::ne(Expr::var("cur"), Expr::u32(0)),
184                        vec![
185                            Node::let_bind("parent", Expr::load(idom_out, Expr::var("cur"))),
186                            Node::if_then(
187                                Expr::and(
188                                    Expr::ne(Expr::var("parent"), Expr::var("cur")),
189                                    Expr::ne(Expr::var("parent"), Expr::u32(IDOM_NONE)),
190                                ),
191                                vec![
192                                    Node::assign("d", Expr::add(Expr::var("d"), Expr::u32(1))),
193                                    Node::assign("cur", Expr::var("parent")),
194                                ],
195                            ),
196                        ],
197                    )],
198                ),
199                Node::store(depth_buf, Expr::var("v"), Expr::var("d")),
200            ],
201        )],
202    );
203
204    let body = vec![
205        // changed = 0
206        Node::let_bind("changed", Expr::u32(0)),
207        // recompute all depths from current idom tree
208        recompute_depth.clone(),
209        // for v in 0..node_count
210        child_phase(
211            INTERSECT_PHASE_OP_ID,
212            vec![Node::loop_for(
213                "v",
214                Expr::u32(0),
215                Expr::u32(node_count),
216                vec![Node::if_then(
217                    Expr::ne(Expr::var("v"), Expr::u32(0)),
218                    vec![
219                        // new_idom = NONE
220                        Node::let_bind("new_idom", Expr::u32(IDOM_NONE)),
221                        // walk predecessors
222                        Node::let_bind("p_start", Expr::load("pred_offsets", Expr::var("v"))),
223                        Node::let_bind(
224                            "p_end",
225                            Expr::load("pred_offsets", Expr::add(Expr::var("v"), Expr::u32(1))),
226                        ),
227                        Node::loop_for(
228                            "p_idx",
229                            Expr::var("p_start"),
230                            Expr::var("p_end"),
231                            vec![
232                                Node::let_bind("p", Expr::load("pred_targets", Expr::var("p_idx"))),
233                                // if idom[p] != NONE
234                                Node::if_then(
235                                    Expr::ne(
236                                        Expr::load(idom_out, Expr::var("p")),
237                                        Expr::u32(IDOM_NONE),
238                                    ),
239                                    vec![Node::if_then_else(
240                                        Expr::eq(Expr::var("new_idom"), Expr::u32(IDOM_NONE)),
241                                        // first reachable predecessor
242                                        vec![Node::assign("new_idom", Expr::var("p"))],
243                                        // else LCA(new_idom, p)
244                                        vec![
245                                            Node::let_bind("a", Expr::var("new_idom")),
246                                            Node::let_bind("b", Expr::var("p")),
247                                            Node::loop_for(
248                                                "lca_step",
249                                                Expr::u32(0),
250                                                Expr::u32(node_count),
251                                                vec![Node::if_then(
252                                                    Expr::ne(Expr::var("a"), Expr::var("b")),
253                                                    vec![
254                                                        Node::let_bind(
255                                                            "da",
256                                                            Expr::load(depth_buf, Expr::var("a")),
257                                                        ),
258                                                        Node::let_bind(
259                                                            "db",
260                                                            Expr::load(depth_buf, Expr::var("b")),
261                                                        ),
262                                                        Node::if_then_else(
263                                                            Expr::gt(
264                                                                Expr::var("da"),
265                                                                Expr::var("db"),
266                                                            ),
267                                                            vec![Node::assign(
268                                                                "a",
269                                                                Expr::load(
270                                                                    idom_out,
271                                                                    Expr::var("a"),
272                                                                ),
273                                                            )],
274                                                            vec![Node::assign(
275                                                                "b",
276                                                                Expr::load(
277                                                                    idom_out,
278                                                                    Expr::var("b"),
279                                                                ),
280                                                            )],
281                                                        ),
282                                                    ],
283                                                )],
284                                            ),
285                                            Node::assign("new_idom", Expr::var("a")),
286                                        ],
287                                    )],
288                                ),
289                            ],
290                        ),
291                        // if new_idom changed, write it and set changed flag
292                        Node::if_then(
293                            Expr::and(
294                                Expr::ne(Expr::var("new_idom"), Expr::u32(IDOM_NONE)),
295                                Expr::ne(
296                                    Expr::var("new_idom"),
297                                    Expr::load(idom_out, Expr::var("v")),
298                                ),
299                            ),
300                            vec![
301                                Node::store(idom_out, Expr::var("v"), Expr::var("new_idom")),
302                                Node::assign("changed", Expr::u32(1)),
303                            ],
304                        ),
305                    ],
306                )],
307            )],
308        ),
309    ];
310
311    let outer_loop = Node::loop_for("step", Expr::u32(0), Expr::u32(node_count), body);
312
313    let region_body = vec![Node::if_then(lane0, vec![init_state, outer_loop])];
314
315    Ok(Program::wrapped(
316        vec![
317            BufferDecl::storage("pg_edge_offsets", 0, BufferAccess::ReadOnly, DataType::U32)
318                .with_count(offset_count),
319            BufferDecl::storage("pg_edge_targets", 1, BufferAccess::ReadOnly, DataType::U32)
320                .with_count(edge_count.max(1)),
321            BufferDecl::storage("pred_offsets", 2, BufferAccess::ReadOnly, DataType::U32)
322                .with_count(offset_count),
323            BufferDecl::storage("pred_targets", 3, BufferAccess::ReadOnly, DataType::U32)
324                .with_count(pred_edge_count.max(1)),
325            BufferDecl::storage(idom_out, 4, BufferAccess::ReadWrite, DataType::U32)
326                .with_count(node_count.max(1)),
327            BufferDecl::storage(depth_buf, 5, BufferAccess::ReadWrite, DataType::U32)
328                .with_count(node_count.max(1)),
329        ],
330        [1, 1, 1],
331        vec![Node::Region {
332            generator: Ident::from(OP_ID),
333            source_region: None,
334            body: Arc::new(region_body),
335        }],
336    ))
337}
338
339fn child_phase(generator: &'static str, body: Vec<Node>) -> Node {
340    Node::Region {
341        generator: Ident::from(generator),
342        source_region: Some(GeneratorRef {
343            name: OP_ID.to_string(),
344        }),
345        body: Arc::new(body),
346    }
347}
348
349fn inert_dominator_tree_program(idom_out: &str) -> Program {
350    Program::wrapped(
351        vec![
352            BufferDecl::storage("pg_edge_offsets", 0, BufferAccess::ReadOnly, DataType::U32)
353                .with_count(1),
354            BufferDecl::storage("pg_edge_targets", 1, BufferAccess::ReadOnly, DataType::U32)
355                .with_count(1),
356            BufferDecl::storage("pred_offsets", 2, BufferAccess::ReadOnly, DataType::U32)
357                .with_count(1),
358            BufferDecl::storage("pred_targets", 3, BufferAccess::ReadOnly, DataType::U32)
359                .with_count(1),
360            BufferDecl::storage(idom_out, 4, BufferAccess::ReadWrite, DataType::U32).with_count(1),
361            BufferDecl::storage("dt_depth", 5, BufferAccess::ReadWrite, DataType::U32)
362                .with_count(1),
363        ],
364        [1, 1, 1],
365        vec![Node::Region {
366            generator: Ident::from(OP_ID),
367            source_region: None,
368            body: Arc::new(vec![Node::return_()]),
369        }],
370    )
371}
372
373/// Validate dominator-tree CSR inputs.
374///
375/// # Errors
376///
377/// Returns [`DominatorTreeError`] when offsets are malformed, targets point
378/// out of range, or the entry is invalid.
379pub fn validate_dominator_tree_inputs(
380    node_count: u32,
381    edge_offsets: &[u32],
382    edge_targets: &[u32],
383    pred_offsets: &[u32],
384    pred_targets: &[u32],
385) -> Result<DominatorTreeLayout, DominatorTreeError> {
386    let expected_offsets =
387        (node_count as usize)
388            .checked_add(1)
389            .ok_or_else(|| DominatorTreeError::BadOffsets {
390                message: format!(
391                "Fix: dominator_tree node_count + 1 overflows usize for node_count={node_count}."
392            ),
393            })?;
394
395    if edge_offsets.len() != expected_offsets {
396        return Err(DominatorTreeError::BadOffsets {
397            message: format!(
398                "Fix: dominator_tree edge_offsets.len() must be {expected_offsets}, got {}.",
399                edge_offsets.len()
400            ),
401        });
402    }
403    if pred_offsets.len() != expected_offsets {
404        return Err(DominatorTreeError::BadOffsets {
405            message: format!(
406                "Fix: dominator_tree pred_offsets.len() must be {expected_offsets}, got {}.",
407                pred_offsets.len()
408            ),
409        });
410    }
411
412    for (idx, pair) in edge_offsets.windows(2).enumerate() {
413        if pair[0] > pair[1] {
414            return Err(DominatorTreeError::NonMonotonicOffsets { index: idx });
415        }
416    }
417    for (idx, pair) in pred_offsets.windows(2).enumerate() {
418        if pair[0] > pair[1] {
419            return Err(DominatorTreeError::NonMonotonicOffsets { index: idx });
420        }
421    }
422
423    let edge_count = edge_offsets.last().copied().unwrap_or(0);
424    let pred_edge_count = pred_offsets.last().copied().unwrap_or(0);
425
426    if edge_targets.len() != edge_count as usize {
427        return Err(DominatorTreeError::BadOffsets {
428            message: format!(
429                "Fix: dominator_tree edge_targets.len()={} != edge_count={edge_count}.",
430                edge_targets.len()
431            ),
432        });
433    }
434    if pred_targets.len() != pred_edge_count as usize {
435        return Err(DominatorTreeError::BadOffsets {
436            message: format!(
437                "Fix: dominator_tree pred_targets.len()={} != pred_edge_count={pred_edge_count}.",
438                pred_targets.len()
439            ),
440        });
441    }
442
443    for (idx, &target) in edge_targets.iter().enumerate() {
444        if target >= node_count {
445            return Err(DominatorTreeError::TargetOutOfRange {
446                index: idx,
447                target,
448                node_count,
449            });
450        }
451    }
452    for (idx, &target) in pred_targets.iter().enumerate() {
453        if target >= node_count {
454            return Err(DominatorTreeError::TargetOutOfRange {
455                index: idx,
456                target,
457                node_count,
458            });
459        }
460    }
461
462    Ok(DominatorTreeLayout {
463        node_count,
464        edge_count,
465        pred_edge_count,
466        idom_words: node_count as usize,
467        depth_words: node_count as usize,
468    })
469}
470
471// ------------------------------------------------------------------