Skip to main content

weir/test_harness/
ifds_builder.rs

1//! Deterministic IFDS problem fixtures for Weir oracle tests.
2//!
3//! The builder computes `expected_reached` with a small worklist interpreter
4//! over IFDS triples instead of calling the CPU oracle under test. That keeps
5//! builder parity tests independent of the CSR construction path they verify.
6
7/// Complete IFDS problem fixture consumed by oracle and resident tests.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct IfdsProblemFixture {
10    pub num_procs: u32,
11    pub blocks_per_proc: u32,
12    pub facts_per_proc: u32,
13    pub intra_edges: Vec<(u32, u32, u32)>,
14    pub inter_edges: Vec<(u32, u32, u32, u32)>,
15    pub flow_gen: Vec<(u32, u32, u32)>,
16    pub flow_kill: Vec<(u32, u32, u32)>,
17    pub seed_facts: Vec<(u32, u32, u32)>,
18    pub expected_reached: Vec<u32>,
19}
20
21/// Factory for deterministic IFDS fixtures.
22pub struct IfdsProblemBuilder;
23
24impl IfdsProblemBuilder {
25    #[allow(clippy::too_many_arguments)]
26    pub fn random_valid(
27        num_procs: u32,
28        blocks_per_proc: u32,
29        facts_per_proc: u32,
30        intra_edge_count: u32,
31        seed_count: u32,
32        summary_edge_count: u32,
33        seed: u64,
34    ) -> IfdsProblemFixture {
35        assert!(
36            num_procs > 0 && blocks_per_proc > 0 && facts_per_proc > 0,
37            "IfdsProblemBuilder::random_valid requires non-zero dimensions"
38        );
39        let _ = checked_total_nodes(num_procs, blocks_per_proc, facts_per_proc);
40        let mut rng = SplitMix64::new(
41            seed ^ ((num_procs as u64) << 42)
42                ^ ((blocks_per_proc as u64) << 21)
43                ^ facts_per_proc as u64,
44        );
45
46        let mut intra_edges = Vec::with_capacity(intra_edge_count as usize);
47        for edge_idx in 0..intra_edge_count {
48            let proc_id = if edge_idx < num_procs {
49                edge_idx
50            } else {
51                rng.below(num_procs)
52            };
53            let src_block = if blocks_per_proc <= 1 {
54                0
55            } else if edge_idx < blocks_per_proc - 1 {
56                edge_idx
57            } else {
58                rng.below(blocks_per_proc)
59            };
60            let mut dst_block = if blocks_per_proc <= 1 {
61                0
62            } else if edge_idx < blocks_per_proc - 1 {
63                src_block + 1
64            } else {
65                rng.below(blocks_per_proc)
66            };
67            if blocks_per_proc > 1 && dst_block == src_block {
68                dst_block = (dst_block + 1) % blocks_per_proc;
69            }
70            intra_edges.push((proc_id % num_procs, src_block, dst_block));
71        }
72
73        let mut inter_edges = Vec::with_capacity(summary_edge_count as usize);
74        for _ in 0..summary_edge_count {
75            let src_proc = rng.below(num_procs);
76            let mut dst_proc = rng.below(num_procs);
77            if num_procs > 1 && dst_proc == src_proc {
78                dst_proc = (dst_proc + 1) % num_procs;
79            }
80            inter_edges.push((
81                src_proc,
82                rng.below(blocks_per_proc),
83                dst_proc,
84                rng.below(blocks_per_proc),
85            ));
86        }
87
88        let mut flow_gen = Vec::with_capacity(summary_edge_count as usize);
89        let mut flow_kill = Vec::with_capacity((summary_edge_count / 2) as usize);
90        for rule_idx in 0..summary_edge_count {
91            let proc_id = rng.below(num_procs);
92            let block_id = rng.below(blocks_per_proc);
93            let gen_fact = rng.below(facts_per_proc);
94            flow_gen.push((proc_id, block_id, gen_fact));
95            if rule_idx % 2 == 1 {
96                let kill_fact = if facts_per_proc > 1 {
97                    1 + rng.below(facts_per_proc - 1)
98                } else {
99                    0
100                };
101                flow_kill.push((proc_id, block_id, kill_fact));
102            }
103        }
104
105        let mut seed_facts = Vec::with_capacity(seed_count as usize);
106        for seed_idx in 0..seed_count {
107            let proc_id = if seed_idx < num_procs {
108                seed_idx
109            } else {
110                rng.below(num_procs)
111            };
112            let block_id = if blocks_per_proc > 1 && seed_idx == 0 {
113                0
114            } else {
115                rng.below(blocks_per_proc)
116            };
117            let fact_id = if facts_per_proc > 1 && seed_idx == 0 {
118                0
119            } else {
120                rng.below(facts_per_proc)
121            };
122            seed_facts.push((proc_id % num_procs, block_id, fact_id));
123        }
124
125        let expected_reached = expected_reachability(
126            num_procs,
127            blocks_per_proc,
128            facts_per_proc,
129            &intra_edges,
130            &inter_edges,
131            &flow_gen,
132            &flow_kill,
133            &seed_facts,
134        );
135
136        IfdsProblemFixture {
137            num_procs,
138            blocks_per_proc,
139            facts_per_proc,
140            intra_edges,
141            inter_edges,
142            flow_gen,
143            flow_kill,
144            seed_facts,
145            expected_reached,
146        }
147    }
148
149    pub fn adversarial_zero_dimensions() -> IfdsProblemFixture {
150        IfdsProblemFixture {
151            num_procs: 0,
152            blocks_per_proc: 1,
153            facts_per_proc: 1,
154            intra_edges: Vec::new(),
155            inter_edges: Vec::new(),
156            flow_gen: Vec::new(),
157            flow_kill: Vec::new(),
158            seed_facts: Vec::new(),
159            expected_reached: Vec::new(),
160        }
161    }
162
163    pub fn adversarial_overflow_shape() -> IfdsProblemFixture {
164        IfdsProblemFixture {
165            num_procs: 4097,
166            blocks_per_proc: 1,
167            facts_per_proc: 1,
168            intra_edges: Vec::new(),
169            inter_edges: Vec::new(),
170            flow_gen: Vec::new(),
171            flow_kill: Vec::new(),
172            seed_facts: Vec::new(),
173            expected_reached: Vec::new(),
174        }
175    }
176}
177
178#[allow(clippy::too_many_arguments)]
179fn expected_reachability(
180    num_procs: u32,
181    blocks_per_proc: u32,
182    facts_per_proc: u32,
183    intra_edges: &[(u32, u32, u32)],
184    inter_edges: &[(u32, u32, u32, u32)],
185    flow_gen: &[(u32, u32, u32)],
186    flow_kill: &[(u32, u32, u32)],
187    seed_facts: &[(u32, u32, u32)],
188) -> Vec<u32> {
189    let total_nodes = checked_total_nodes(num_procs, blocks_per_proc, facts_per_proc);
190    let mut killed = vec![false; total_nodes];
191    for &(proc_id, block_id, fact_id) in flow_kill {
192        killed[dense_index(proc_id, block_id, fact_id, blocks_per_proc, facts_per_proc)] = true;
193    }
194
195    let mut visited = vec![false; total_nodes];
196    let mut queue = Vec::with_capacity(seed_facts.len());
197    for &(proc_id, block_id, fact_id) in seed_facts {
198        mark_reached(
199            proc_id,
200            block_id,
201            fact_id,
202            blocks_per_proc,
203            facts_per_proc,
204            &mut visited,
205            &mut queue,
206        );
207    }
208
209    let mut head = 0usize;
210    while head < queue.len() {
211        let (proc_id, block_id, fact_id) = queue[head];
212        head += 1;
213
214        for &(edge_proc, src_block, dst_block) in intra_edges {
215            if edge_proc != proc_id || src_block != block_id {
216                continue;
217            }
218            let current_idx =
219                dense_index(proc_id, block_id, fact_id, blocks_per_proc, facts_per_proc);
220            if !killed[current_idx] {
221                mark_reached(
222                    proc_id,
223                    dst_block,
224                    fact_id,
225                    blocks_per_proc,
226                    facts_per_proc,
227                    &mut visited,
228                    &mut queue,
229                );
230            }
231            if fact_id == 0 {
232                for &(gen_proc, gen_block, gen_fact) in flow_gen {
233                    if gen_proc == proc_id && gen_block == block_id {
234                        mark_reached(
235                            proc_id,
236                            dst_block,
237                            gen_fact,
238                            blocks_per_proc,
239                            facts_per_proc,
240                            &mut visited,
241                            &mut queue,
242                        );
243                    }
244                }
245            }
246        }
247
248        for &(src_proc, src_block, dst_proc, dst_block) in inter_edges {
249            if src_proc == proc_id && src_block == block_id {
250                mark_reached(
251                    dst_proc,
252                    dst_block,
253                    fact_id,
254                    blocks_per_proc,
255                    facts_per_proc,
256                    &mut visited,
257                    &mut queue,
258                );
259            }
260        }
261    }
262
263    visited
264        .into_iter()
265        .enumerate()
266        .filter_map(|(dense_id, reached)| reached.then_some(dense_id as u32))
267        .collect()
268}
269
270fn mark_reached(
271    proc_id: u32,
272    block_id: u32,
273    fact_id: u32,
274    blocks_per_proc: u32,
275    facts_per_proc: u32,
276    visited: &mut [bool],
277    queue: &mut Vec<(u32, u32, u32)>,
278) {
279    let idx = dense_index(proc_id, block_id, fact_id, blocks_per_proc, facts_per_proc);
280    if !visited[idx] {
281        visited[idx] = true;
282        queue.push((proc_id, block_id, fact_id));
283    }
284}
285
286fn checked_total_nodes(num_procs: u32, blocks_per_proc: u32, facts_per_proc: u32) -> usize {
287    let total = (num_procs as u64)
288        .checked_mul(blocks_per_proc as u64)
289        .and_then(|value| value.checked_mul(facts_per_proc as u64))
290        ;
291    assert!(
292        total.is_some(),
293        "IFDS fixture node count overflows u64"
294    );
295    let total = total.unwrap_or(u64::MAX);
296    assert!(
297        total <= u32::MAX as u64,
298        "IFDS fixture node count {total} exceeds u32 dense domain"
299    );
300    assert!(
301        total <= usize::MAX as u64,
302        "IFDS fixture node count {total} does not fit usize"
303    );
304    total as usize
305}
306
307fn dense_index(
308    proc_id: u32,
309    block_id: u32,
310    fact_id: u32,
311    blocks_per_proc: u32,
312    facts_per_proc: u32,
313) -> usize {
314    let idx = (proc_id as u64)
315        .checked_mul(blocks_per_proc as u64)
316        .and_then(|value| value.checked_mul(facts_per_proc as u64))
317        .and_then(|value| {
318            (block_id as u64)
319                .checked_mul(facts_per_proc as u64)
320                .and_then(|block_offset| value.checked_add(block_offset))
321        })
322        .and_then(|value| value.checked_add(fact_id as u64))
323        ;
324    assert!(idx.is_some(), "IFDS fixture dense index overflows u64");
325    let idx = idx.unwrap_or(u64::MAX);
326    assert!(
327        idx <= usize::MAX as u64,
328        "IFDS fixture dense index {idx} does not fit usize"
329    );
330    idx as usize
331}
332
333#[derive(Debug, Clone, Copy)]
334struct SplitMix64 {
335    state: u64,
336}
337
338impl SplitMix64 {
339    fn new(seed: u64) -> Self {
340        Self {
341            state: seed ^ 0x9e37_79b9_7f4a_7c15,
342        }
343    }
344
345    fn next(&mut self) -> u64 {
346        self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15);
347        let mut z = self.state;
348        z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
349        z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
350        z ^ (z >> 31)
351    }
352
353    fn below(&mut self, upper: u32) -> u32 {
354        if upper == 0 {
355            return 0;
356        }
357        (self.next() % upper as u64) as u32
358    }
359}