Skip to main content

vyre_self_substrate/
exploded.rs

1//! Exploded-supergraph (IFDS encoding) substrate consumer.
2//!
3//! Wires `vyre_primitives::graph::exploded::build_cpu_reference` (zero
4//! prior consumers) into the substrate so the optimizer can build
5//! interprocedural-dataflow graphs directly. The IFDS encoding packs
6//! `(proc_id, block_id, fact_id)` into a u32 node id, then composes
7//! intra-/inter-procedural edges + GEN/KILL flow into a CSR ready for
8//! reachability/closure analysis.
9
10use vyre_primitives::graph::exploded::{build_cpu_reference, dense_to_encoded, encoded_to_dense};
11
12/// Build an exploded supergraph and return its CSR `(row_ptr, col_idx)`.
13/// Inputs match the underlying primitive's contract; the wrapper bumps
14/// the dataflow-fixpoint observability counter so dispatch-time IFDS
15/// graph builds are visible in dashboards.
16#[must_use]
17pub fn build_ifds_csr(
18    num_procs: u32,
19    blocks_per_proc: u32,
20    facts_per_proc: u32,
21    intra_edges: &[(u32, u32, u32)],
22    inter_edges: &[(u32, u32, u32, u32)],
23    flow_gen: &[(u32, u32, u32)],
24    flow_kill: &[(u32, u32, u32)],
25) -> (Vec<u32>, Vec<u32>) {
26    use crate::observability::{bump, dataflow_fixpoint_calls};
27    bump(&dataflow_fixpoint_calls);
28    build_cpu_reference(
29        num_procs,
30        blocks_per_proc,
31        facts_per_proc,
32        intra_edges,
33        inter_edges,
34        flow_gen,
35        flow_kill,
36    )
37}
38
39/// Total node count of the exploded supergraph for the given
40/// dimensions. Equivalent to `row_ptr.len() - 1` after the CSR is
41/// built; useful when the caller needs to size frontier bitsets
42/// before invoking [`build_ifds_csr`].
43#[must_use]
44pub fn ifds_node_count(num_procs: u32, blocks_per_proc: u32, facts_per_proc: u32) -> u32 {
45    num_procs
46        .saturating_mul(blocks_per_proc)
47        .saturating_mul(facts_per_proc)
48}
49
50/// Helper: round-trip a dense index through the packed encoding and
51/// back. Used by callers that emit findings keyed on the packed id
52/// but operate on dense indices internally.
53#[must_use]
54pub fn round_trip_dense(dense: u32, blocks_per_proc: u32, facts_per_proc: u32) -> Option<u32> {
55    let encoded = dense_to_encoded(dense, blocks_per_proc, facts_per_proc)?;
56    encoded_to_dense(encoded, blocks_per_proc, facts_per_proc)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    /// Two procs, 2 blocks each, 2 facts each. One intra edge per
64    /// proc, no inter, no flow. The CSR row count must equal the
65    /// total node count.
66    #[test]
67    fn csr_row_count_matches_node_count() {
68        let (row_ptr, _) = build_ifds_csr(2, 2, 2, &[(0, 0, 1), (1, 0, 1)], &[], &[], &[]);
69        // Total = 2 * 2 * 2 = 8.
70        assert_eq!(row_ptr.len(), 9);
71        assert_eq!(ifds_node_count(2, 2, 2), 8);
72    }
73
74    /// Closure-bar: substrate output equals primitive output.
75    #[test]
76    fn matches_primitive_directly() {
77        let intra = vec![(0, 0, 1), (1, 0, 1)];
78        let inter = vec![(0, 1, 1, 0)];
79        let gen = vec![(0, 0, 1)];
80        let kill = vec![(1, 0, 0)];
81        let via_substrate = build_ifds_csr(2, 2, 2, &intra, &inter, &gen, &kill);
82        let via_primitive = build_cpu_reference(2, 2, 2, &intra, &inter, &gen, &kill);
83        assert_eq!(via_substrate, via_primitive);
84    }
85
86    /// Empty graph (no procs) is degenerate; CSR is `(vec![0], vec![])`.
87    /// Common bug: emit `vec![]` instead of `vec![0]` for row_ptr,
88    /// breaking downstream CSR walkers that assume `row_ptr[0] = 0`.
89    #[test]
90    fn empty_graph_yields_singleton_row_ptr() {
91        let (row_ptr, col_idx) = build_ifds_csr(0, 0, 0, &[], &[], &[], &[]);
92        assert_eq!(row_ptr, vec![0u32]);
93        assert!(col_idx.is_empty());
94    }
95
96    /// Adversarial: KILL must suppress fact propagation along an
97    /// intra edge. (proc 0, block 0, fact 1) is killed → no edge
98    /// emitted from (0, 0, 1) to (0, 1, 1).
99    #[test]
100    fn kill_suppresses_fact_propagation() {
101        let intra = vec![(0, 0, 1)];
102        let kill = vec![(0, 0, 1)];
103        let (row_ptr, col_idx) = build_ifds_csr(1, 2, 2, &intra, &[], &[], &kill);
104        // Node (0, 0, 1) is at dense index 0 * 4 + 0 * 2 + 1 = 1.
105        let src = 1usize;
106        let row_start = row_ptr[src] as usize;
107        let row_end = row_ptr[src + 1] as usize;
108        let neighbors = &col_idx[row_start..row_end];
109        // Should have NO edge to (0, 1, 1) (= dense 0*4 + 1*2 + 1 = 3).
110        assert!(!neighbors.contains(&3), "killed fact must not propagate");
111    }
112
113    /// Adversarial: GEN must inject the new fact along the intra
114    /// edge. (proc 0, block 0, gen fact 1) → edge from (0, 0, 0)
115    /// (the 0-fact) to (0, 1, 1).
116    #[test]
117    fn gen_injects_new_fact() {
118        let intra = vec![(0, 0, 1)];
119        let gen = vec![(0, 0, 1)];
120        let (row_ptr, col_idx) = build_ifds_csr(1, 2, 2, &intra, &[], &gen, &[]);
121        // 0-fact at (0, 0, 0) → dense index 0.
122        let row_start = row_ptr[0] as usize;
123        let row_end = row_ptr[1] as usize;
124        let neighbors = &col_idx[row_start..row_end];
125        // Edge to (0, 1, 1) → dense 3.
126        assert!(neighbors.contains(&3), "gen must emit edge to new fact");
127    }
128
129    /// Round-trip dense ↔ encoded must be identity for valid indices.
130    #[test]
131    fn round_trip_dense_is_identity() {
132        let blocks_per_proc = 4;
133        let facts_per_proc = 8;
134        for dense in 0..32 {
135            assert_eq!(
136                round_trip_dense(dense, blocks_per_proc, facts_per_proc),
137                Some(dense)
138            );
139        }
140    }
141
142    /// Adversarial: inter-procedural edge propagates EVERY fact
143    /// (IFDS upper bound). For 2 facts, expect 2 edges from
144    /// (sp, sb, *) to (dp, db, *).
145    #[test]
146    fn inter_edge_propagates_every_fact() {
147        let inter = vec![(0, 0, 1, 1)];
148        let (row_ptr, col_idx) = build_ifds_csr(2, 2, 2, &[], &inter, &[], &[]);
149        let dense_src_f0 = 0; // (0, 0, 0)
150        let dense_src_f1 = 1; // (0, 0, 1)
151        let row0 = &col_idx[row_ptr[dense_src_f0] as usize..row_ptr[dense_src_f0 + 1] as usize];
152        let row1 = &col_idx[row_ptr[dense_src_f1] as usize..row_ptr[dense_src_f1 + 1] as usize];
153        // (1, 1, 0) = 1*4 + 1*2 + 0 = 6
154        // (1, 1, 1) = 1*4 + 1*2 + 1 = 7
155        assert!(row0.contains(&6), "fact 0 must propagate via inter edge");
156        assert!(row1.contains(&7), "fact 1 must propagate via inter edge");
157    }
158}