Skip to main content

vyre_primitives/graph/exploded/
cpu_ref.rs

1#[cfg(any(test, feature = "cpu-parity"))]
2use super::validation::validate_ifds_csr_inputs;
3
4/// CPU-reference CSR builder for the exploded supergraph.
5///
6/// `intra_edges` are `(src_block, dst_block)` pairs **within** a
7/// procedure  -  the standard CFG. `inter_edges` are `(src_proc,
8/// src_block, dst_proc, dst_block)` call / return edges. Flow
9/// functions are encoded as per-block GEN / KILL bitsets over the
10/// fact domain.
11///
12/// Caller-owned workspace for exploded IFDS CPU-reference CSR construction.
13#[cfg(any(test, feature = "cpu-parity"))]
14#[derive(Debug, Default, Clone)]
15pub struct ExplodedIfdsCpuScratch {
16    /// Flat `(src, dst)` edge list before CSR compaction.
17    pub edges_flat: Vec<(u32, u32)>,
18    /// Per-dense-node KILL bitmap.
19    pub killed: Vec<bool>,
20    /// Per-block GEN prefix offsets.
21    pub gen_offsets: Vec<usize>,
22    /// Per-block GEN fill cursor.
23    pub gen_cursor: Vec<usize>,
24    /// Flat GEN fact table keyed by `gen_offsets`.
25    pub gen_facts: Vec<u32>,
26    /// Per-row CSR fill cursor.
27    pub cursor: Vec<usize>,
28}
29
30#[cfg(any(test, feature = "cpu-parity"))]
31impl ExplodedIfdsCpuScratch {
32    /// Create an empty reusable exploded-IFDS CPU workspace.
33    pub fn new() -> Self {
34        Self::default()
35    }
36}
37
38/// Returns `(row_ptr, col_idx)` in the **dense** index space
39/// `idx(p, b, f) = p * blocks * facts + b * facts + f`. This is
40/// the space every traversal kernel operates in  -  packing via
41/// [`crate::graph::exploded::encode_node`] is only used at the I/O boundary when the
42/// caller needs to report results as `(proc, block, fact)`
43/// triples. The two spaces coincide only in the degenerate case
44/// `blocks_per_proc == 1 << BLOCK_BITS` and `facts_per_proc == 1 << FACT_BITS`;
45/// the dense layout works for any dimensions that fit in
46/// 32-bit encoding.
47#[must_use]
48#[cfg(any(test, feature = "cpu-parity"))]
49pub fn build_cpu_reference(
50    num_procs: u32,
51    blocks_per_proc: u32,
52    facts_per_proc: u32,
53    intra_edges: &[(u32, u32, u32)], // (proc, src_block, dst_block)
54    inter_edges: &[(u32, u32, u32, u32)], // (src_proc, src_block, dst_proc, dst_block)
55    flow_gen: &[(u32, u32, u32)],    // (proc, block, fact)  -  GEN bits
56    flow_kill: &[(u32, u32, u32)],   // (proc, block, fact)  -  KILL bits
57) -> (Vec<u32>, Vec<u32>) {
58    try_build_cpu_reference(
59        num_procs,
60        blocks_per_proc,
61        facts_per_proc,
62        intra_edges,
63        inter_edges,
64        flow_gen,
65        flow_kill,
66    )
67    .unwrap_or_else(|err| panic!("exploded IFDS CPU reference received malformed input. {err}"))
68}
69
70/// Fallible CPU-reference CSR builder for the exploded supergraph.
71///
72/// This is the allocation-safe oracle entry point for fuzz, hostile-dimension,
73/// and parity harnesses. [`build_cpu_reference`] preserves the legacy panicking
74/// API by delegating here.
75#[cfg(any(test, feature = "cpu-parity"))]
76pub fn try_build_cpu_reference(
77    num_procs: u32,
78    blocks_per_proc: u32,
79    facts_per_proc: u32,
80    intra_edges: &[(u32, u32, u32)],
81    inter_edges: &[(u32, u32, u32, u32)],
82    flow_gen: &[(u32, u32, u32)],
83    flow_kill: &[(u32, u32, u32)],
84) -> Result<(Vec<u32>, Vec<u32>), String> {
85    let mut row_ptr = Vec::new();
86    let mut col_idx = Vec::new();
87    let mut scratch = ExplodedIfdsCpuScratch::default();
88    try_build_cpu_reference_into(
89        num_procs,
90        blocks_per_proc,
91        facts_per_proc,
92        intra_edges,
93        inter_edges,
94        flow_gen,
95        flow_kill,
96        &mut row_ptr,
97        &mut col_idx,
98        &mut scratch,
99    )?;
100    Ok((row_ptr, col_idx))
101}
102
103/// Fallible CPU-reference CSR builder into caller-owned output and scratch.
104///
105/// Validation happens before output and scratch storage are cleared. This keeps
106/// fuzz/parity diagnostics intact when a malformed IFDS domain or rule set is
107/// rejected before CSR construction begins.
108#[cfg(any(test, feature = "cpu-parity"))]
109#[allow(clippy::too_many_arguments)]
110pub fn try_build_cpu_reference_into(
111    num_procs: u32,
112    blocks_per_proc: u32,
113    facts_per_proc: u32,
114    intra_edges: &[(u32, u32, u32)],
115    inter_edges: &[(u32, u32, u32, u32)],
116    flow_gen: &[(u32, u32, u32)],
117    flow_kill: &[(u32, u32, u32)],
118    row_ptr: &mut Vec<u32>,
119    col_idx: &mut Vec<u32>,
120    scratch: &mut ExplodedIfdsCpuScratch,
121) -> Result<(), String> {
122    let layout = validate_ifds_csr_inputs(
123        num_procs,
124        blocks_per_proc,
125        facts_per_proc,
126        intra_edges,
127        inter_edges,
128        flow_gen,
129        flow_kill,
130    )?;
131    if layout.empty {
132        return Err(format!(
133            "exploded IFDS CPU reference dimensions must be nonzero, got procs={num_procs}, blocks={blocks_per_proc}, facts={facts_per_proc}. Fix: pass a real exploded-supergraph domain before parity comparison."
134        ));
135    }
136
137    // PHASE7_GRAPH C4: every multiply checked. The previous unchecked
138    // chain (`blocks * facts`, then `procs * slots`) wraps silently
139    // when the caller passes the maximum dimensions for each field
140    // (4096 × 1024 × 1024 = 2^32 = wraps to 0 on 32-bit usize and
141    // sits exactly at the overflow boundary on 64-bit). Either case
142    // produced a tiny `Vec<Vec<u32>>` and catastrophic OOB writes in
143    // the edge-emit loops below.
144    let slots_per_proc = layout.slots_per_proc as usize;
145    let total_nodes = layout.total_nodes as usize;
146    crate::graph::scratch::reserve_graph_items(
147        &mut scratch.edges_flat,
148        layout.max_col_count as usize,
149        "exploded IFDS CPU reference",
150        "flat exploded edge list",
151    )?;
152    scratch.edges_flat.clear();
153    let block_count = (num_procs as usize) * (blocks_per_proc as usize);
154
155    let idx = |p: u32, b: u32, f: u32| -> u32 {
156        ((p as usize) * slots_per_proc + (b as usize) * facts_per_proc as usize + f as usize) as u32
157    };
158    let block_idx =
159        |p: u32, b: u32| -> usize { (p as usize) * blocks_per_proc as usize + b as usize };
160    let in_space =
161        |p: u32, b: u32, f: u32| p < num_procs && b < blocks_per_proc && f < facts_per_proc;
162
163    crate::graph::scratch::reserve_graph_items(
164        &mut scratch.killed,
165        total_nodes,
166        "exploded IFDS CPU reference",
167        "kill bitmap",
168    )?;
169    scratch.killed.clear();
170    scratch.killed.resize(total_nodes, false);
171    for &(p, b, f) in flow_kill {
172        if in_space(p, b, f) {
173            scratch.killed[idx(p, b, f) as usize] = true;
174        }
175    }
176
177    let gen_offset_count = block_count
178        .checked_add(1)
179        .ok_or_else(|| "Fix: exploded IFDS block_count+1 overflows usize.".to_string())?;
180    crate::graph::scratch::reserve_graph_items(
181        &mut scratch.gen_offsets,
182        gen_offset_count,
183        "exploded IFDS CPU reference",
184        "GEN offsets",
185    )?;
186    scratch.gen_offsets.clear();
187    scratch.gen_offsets.resize(gen_offset_count, 0);
188    for &(p, b, f) in flow_gen {
189        if in_space(p, b, f) {
190            scratch.gen_offsets[block_idx(p, b) + 1] += 1;
191        }
192    }
193    for i in 1..scratch.gen_offsets.len() {
194        scratch.gen_offsets[i] += scratch.gen_offsets[i - 1];
195    }
196    crate::graph::scratch::reserve_graph_items(
197        &mut scratch.gen_cursor,
198        block_count,
199        "exploded IFDS CPU reference",
200        "GEN cursor",
201    )?;
202    scratch.gen_cursor.clear();
203    scratch
204        .gen_cursor
205        .extend_from_slice(&scratch.gen_offsets[..block_count]);
206    let gen_fact_count = scratch.gen_offsets[block_count];
207    crate::graph::scratch::reserve_graph_items(
208        &mut scratch.gen_facts,
209        gen_fact_count,
210        "exploded IFDS CPU reference",
211        "GEN fact table",
212    )?;
213    scratch.gen_facts.clear();
214    scratch.gen_facts.resize(gen_fact_count, 0);
215    for &(p, b, f) in flow_gen {
216        if in_space(p, b, f) {
217            let key = block_idx(p, b);
218            let slot = scratch.gen_cursor[key];
219            scratch.gen_facts[slot] = f;
220            scratch.gen_cursor[key] += 1;
221        }
222    }
223
224    // Intra-procedural CFG edges, cross-producted with fact-propagation:
225    // an edge (B_src -> B_dst) gives rise to an edge in the exploded
226    // supergraph between every pair (f, f) that survives the flow
227    // function at B_src (fact f propagates iff f is not killed).
228    for &(p, src_b, dst_b) in intra_edges {
229        if p >= num_procs || src_b >= blocks_per_proc || dst_b >= blocks_per_proc {
230            continue;
231        }
232        for f in 0..facts_per_proc {
233            if scratch.killed[idx(p, src_b, f) as usize] {
234                continue;
235            }
236            scratch
237                .edges_flat
238                .push((idx(p, src_b, f), idx(p, dst_b, f)));
239        }
240        // GEN edges: standard IFDS 0-fact encoding  -  fact 0 is the
241        // tautological "always present" fact. `GEN(src_b, gf)` emits
242        // edge `(src_b, 0) → (dst_b, gf)`, so seeding `(entry, 0)`
243        // triggers every GEN along the reachable CFG. Callers that
244        // don't use the 0-fact convention see GEN as a no-op.
245        let gen_key = block_idx(p, src_b);
246        for &gf in
247            &scratch.gen_facts[scratch.gen_offsets[gen_key]..scratch.gen_offsets[gen_key + 1]]
248        {
249            scratch
250                .edges_flat
251                .push((idx(p, src_b, 0), idx(p, dst_b, gf)));
252        }
253    }
254
255    // Inter-procedural call / return edges propagate every fact
256    // (IFDS handles parameter mapping via summary edges in the full
257    // algorithm; this CPU reference is the unfiltered
258    // "every-fact-flows" upper bound used for correctness tests).
259    for &(sp, sb, dp, db) in inter_edges {
260        if sp >= num_procs || dp >= num_procs || sb >= blocks_per_proc || db >= blocks_per_proc {
261            continue;
262        }
263        for f in 0..facts_per_proc {
264            scratch.edges_flat.push((idx(sp, sb, f), idx(dp, db, f)));
265        }
266    }
267
268    // Flatten into CSR  -  row_ptr has total_nodes+1 entries.
269    if scratch.edges_flat.len() > u32::MAX as usize {
270        return Err(format!(
271            "exploded IFDS CPU reference edge_count={} exceeds u32 CSR encoding. Fix: shard the IFDS graph before parity comparison.",
272            scratch.edges_flat.len()
273        ));
274    }
275    let row_ptr_len = layout.row_words;
276    crate::graph::scratch::reserve_graph_items(
277        row_ptr,
278        row_ptr_len,
279        "exploded IFDS CPU reference",
280        "CSR row_ptr",
281    )?;
282    row_ptr.clear();
283    row_ptr.resize(row_ptr_len, 0);
284    for &(src, _) in &scratch.edges_flat {
285        let row = src as usize;
286        row_ptr[row + 1] = row_ptr[row + 1].checked_add(1).ok_or_else(|| {
287            format!(
288                "exploded IFDS CPU reference row {row} edge count overflowed u32. Fix: shard the IFDS graph before parity comparison."
289            )
290        })?;
291    }
292    for row in 1..row_ptr.len() {
293        row_ptr[row] = row_ptr[row].checked_add(row_ptr[row - 1]).ok_or_else(|| {
294            format!(
295                "exploded IFDS CPU reference CSR prefix overflowed at row {row}. Fix: shard the IFDS graph before parity comparison."
296            )
297        })?;
298    }
299    crate::graph::scratch::reserve_graph_items(
300        &mut scratch.cursor,
301        total_nodes,
302        "exploded IFDS CPU reference",
303        "CSR cursor",
304    )?;
305    scratch.cursor.clear();
306    for &offset in &row_ptr[..total_nodes] {
307        scratch.cursor.push(offset as usize);
308    }
309    crate::graph::scratch::reserve_graph_items(
310        col_idx,
311        scratch.edges_flat.len(),
312        "exploded IFDS CPU reference",
313        "CSR col_idx",
314    )?;
315    col_idx.clear();
316    col_idx.resize(scratch.edges_flat.len(), 0);
317    for &(src, dst) in &scratch.edges_flat {
318        let row = src as usize;
319        let slot = scratch.cursor[row];
320        col_idx[slot] = dst;
321        scratch.cursor[row] += 1;
322    }
323    Ok(())
324}