1use std::sync::Arc;
14
15use vyre_foundation::ir::model::expr::Ident;
16use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
17
18use crate::graph::csr_forward_traverse::bitset_words;
19
20pub const OP_ID: &str = "vyre-primitives::graph::dominator_frontier";
22
23pub const DOMINATOR_FRONTIER_DOM_OFFSETS_BUFFER: u32 = 0;
25pub const DOMINATOR_FRONTIER_DOM_TARGETS_BUFFER: u32 = 1;
27pub const DOMINATOR_FRONTIER_PRED_OFFSETS_BUFFER: u32 = 2;
29pub const DOMINATOR_FRONTIER_PRED_TARGETS_BUFFER: u32 = 3;
31pub const DOMINATOR_FRONTIER_SEED_BUFFER: u32 = 4;
33pub const DOMINATOR_FRONTIER_OUT_BUFFER: u32 = 5;
35pub const DOMINATOR_FRONTIER_WORKGROUP_SIZE: [u32; 3] = [256, 1, 1];
37
38#[must_use]
40pub const fn dominator_frontier_dispatch_grid(node_count: u32) -> [u32; 3] {
41 if node_count == 0 {
42 [0, 1, 1]
43 } else {
44 [
45 node_count.div_ceil(DOMINATOR_FRONTIER_WORKGROUP_SIZE[0]),
46 1,
47 1,
48 ]
49 }
50}
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub struct DominatorFrontierLayout {
55 pub words: usize,
57 pub dom_edge_count: u32,
59 pub pred_edge_count: u32,
61}
62
63#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct DominatorFrontierProgramShape {
66 pub node_count: u32,
68 pub dom_edge_count: u32,
70 pub pred_edge_count: u32,
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct DominatorFrontierSliceFingerprint {
77 len: usize,
78 first: u32,
79 last: u32,
80 xor: u32,
81 sum: u64,
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct DominatorFrontierStaticInputKey {
91 shape: DominatorFrontierProgramShape,
92 layout: DominatorFrontierLayout,
93 dom_target_words: usize,
94 pred_target_words: usize,
95 frontier_words: usize,
96 dom_offsets: DominatorFrontierSliceFingerprint,
97 dom_targets: DominatorFrontierSliceFingerprint,
98 pred_offsets: DominatorFrontierSliceFingerprint,
99 pred_targets: DominatorFrontierSliceFingerprint,
100}
101
102#[must_use]
104pub fn dominator_frontier_slice_fingerprint(words: &[u32]) -> DominatorFrontierSliceFingerprint {
105 let mut xor = 0u32;
106 let mut sum = 0u64;
107 for &word in words {
108 xor ^= word;
109 sum = sum.wrapping_add(u64::from(word));
110 }
111 DominatorFrontierSliceFingerprint {
112 len: words.len(),
113 first: words.first().copied().unwrap_or(0),
114 last: words.last().copied().unwrap_or(0),
115 xor,
116 sum,
117 }
118}
119
120#[cfg(test)]
121mod static_input_key_tests {
122 use super::*;
123
124 #[test]
125 fn slice_fingerprint_tracks_interior_content_not_only_len_edges() {
126 let baseline = dominator_frontier_slice_fingerprint(&[7, 11, 13, 17]);
127 let changed = dominator_frontier_slice_fingerprint(&[7, 11, 19, 17]);
128
129 assert_ne!(baseline, changed);
130 }
131
132 #[test]
133 fn static_input_key_tracks_graph_content_but_not_dynamic_seed_bits() {
134 let plan_a = plan_dominator_frontier_launch(
135 4,
136 &[0, 4, 5, 6, 7],
137 &[0, 1, 2, 3, 1, 2, 3],
138 &[0, 0, 1, 2, 4],
139 &[0, 0, 1, 2],
140 &[0b0010],
141 )
142 .expect("Fix: valid dominator-frontier launch plan should build");
143 let plan_b = plan_dominator_frontier_launch(
144 4,
145 &[0, 4, 5, 6, 7],
146 &[0, 1, 2, 3, 1, 2, 3],
147 &[0, 0, 1, 2, 4],
148 &[0, 0, 1, 2],
149 &[0b0100],
150 )
151 .expect("Fix: seed-only changes should keep the same static launch shape");
152
153 let baseline = plan_a.static_input_key(
154 &[0, 4, 5, 6, 7],
155 &[0, 1, 2, 3, 1, 2, 3],
156 &[0, 0, 1, 2, 4],
157 &[0, 0, 1, 2],
158 );
159 let seed_only_change = plan_b.static_input_key(
160 &[0, 4, 5, 6, 7],
161 &[0, 1, 2, 3, 1, 2, 3],
162 &[0, 0, 1, 2, 4],
163 &[0, 0, 1, 2],
164 );
165 let graph_content_change = plan_a.static_input_key(
166 &[0, 4, 5, 6, 7],
167 &[0, 1, 2, 2, 1, 2, 3],
168 &[0, 0, 1, 2, 4],
169 &[0, 0, 1, 2],
170 );
171
172 assert_eq!(baseline, seed_only_change);
173 assert_ne!(baseline, graph_content_change);
174 }
175}
176
177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
179pub struct DominatorFrontierLaunchPlan {
180 layout: DominatorFrontierLayout,
181 shape: DominatorFrontierProgramShape,
182 dispatch_grid: [u32; 3],
183}
184
185impl DominatorFrontierLaunchPlan {
186 #[must_use]
188 pub const fn layout(&self) -> DominatorFrontierLayout {
189 self.layout
190 }
191
192 #[must_use]
194 pub const fn shape(&self) -> DominatorFrontierProgramShape {
195 self.shape
196 }
197
198 #[must_use]
200 pub const fn dispatch_grid(&self) -> [u32; 3] {
201 self.dispatch_grid
202 }
203
204 #[must_use]
206 pub const fn frontier_words(&self) -> usize {
207 self.layout.words
208 }
209
210 #[must_use]
212 pub const fn dom_target_words(&self) -> usize {
213 if self.layout.dom_edge_count == 0 {
214 1
215 } else {
216 self.layout.dom_edge_count as usize
217 }
218 }
219
220 #[must_use]
222 pub const fn pred_target_words(&self) -> usize {
223 if self.layout.pred_edge_count == 0 {
224 1
225 } else {
226 self.layout.pred_edge_count as usize
227 }
228 }
229
230 #[must_use]
232 pub fn static_input_key(
233 &self,
234 dom_offsets: &[u32],
235 dom_targets: &[u32],
236 pred_offsets: &[u32],
237 pred_targets: &[u32],
238 ) -> DominatorFrontierStaticInputKey {
239 DominatorFrontierStaticInputKey {
240 shape: self.shape,
241 layout: self.layout,
242 dom_target_words: self.dom_target_words(),
243 pred_target_words: self.pred_target_words(),
244 frontier_words: self.frontier_words(),
245 dom_offsets: dominator_frontier_slice_fingerprint(dom_offsets),
246 dom_targets: dominator_frontier_slice_fingerprint(dom_targets),
247 pred_offsets: dominator_frontier_slice_fingerprint(pred_offsets),
248 pred_targets: dominator_frontier_slice_fingerprint(pred_targets),
249 }
250 }
251
252 pub fn program(&self, seed_buffer: &str, out_buffer: &str) -> Result<Program, String> {
254 try_dominator_frontier(
255 self.shape.node_count,
256 self.shape.dom_edge_count,
257 self.shape.pred_edge_count,
258 seed_buffer,
259 out_buffer,
260 )
261 }
262}
263
264pub struct DominatorFrontierDispatchPlan {
266 launch: DominatorFrontierLaunchPlan,
267 program: Program,
268}
269
270impl DominatorFrontierDispatchPlan {
271 #[must_use]
273 pub const fn layout(&self) -> DominatorFrontierLayout {
274 self.launch.layout()
275 }
276
277 #[must_use]
279 pub const fn shape(&self) -> DominatorFrontierProgramShape {
280 self.launch.shape()
281 }
282
283 #[must_use]
285 pub const fn program(&self) -> &Program {
286 &self.program
287 }
288
289 #[must_use]
291 pub const fn dispatch_grid(&self) -> [u32; 3] {
292 self.launch.dispatch_grid()
293 }
294
295 #[must_use]
297 pub const fn frontier_words(&self) -> usize {
298 self.launch.frontier_words()
299 }
300
301 #[must_use]
303 pub const fn dom_target_words(&self) -> usize {
304 self.launch.dom_target_words()
305 }
306
307 #[must_use]
309 pub const fn pred_target_words(&self) -> usize {
310 self.launch.pred_target_words()
311 }
312}
313
314pub fn plan_dominator_frontier_launch(
323 node_count: u32,
324 dom_offsets: &[u32],
325 dom_targets: &[u32],
326 pred_offsets: &[u32],
327 pred_targets: &[u32],
328 seed: &[u32],
329) -> Result<DominatorFrontierLaunchPlan, String> {
330 let layout = validate_dominator_frontier_inputs(
331 node_count,
332 dom_offsets,
333 dom_targets,
334 pred_offsets,
335 pred_targets,
336 seed,
337 )?;
338 let _offset_count = node_count.checked_add(1).ok_or_else(|| {
339 format!(
340 "dominator_frontier node_count={node_count} overflows CSR offset buffer count. Fix: shard the graph before GPU dispatch."
341 )
342 })?;
343
344 Ok(DominatorFrontierLaunchPlan {
345 layout,
346 shape: DominatorFrontierProgramShape {
347 node_count,
348 dom_edge_count: layout.dom_edge_count,
349 pred_edge_count: layout.pred_edge_count,
350 },
351 dispatch_grid: dominator_frontier_dispatch_grid(node_count),
352 })
353}
354
355pub fn plan_dominator_frontier_dispatch(
363 node_count: u32,
364 dom_offsets: &[u32],
365 dom_targets: &[u32],
366 pred_offsets: &[u32],
367 pred_targets: &[u32],
368 seed: &[u32],
369 seed_buffer: &str,
370 out_buffer: &str,
371) -> Result<DominatorFrontierDispatchPlan, String> {
372 let launch = plan_dominator_frontier_launch(
373 node_count,
374 dom_offsets,
375 dom_targets,
376 pred_offsets,
377 pred_targets,
378 seed,
379 )?;
380 let program = launch.program(seed_buffer, out_buffer)?;
381
382 Ok(DominatorFrontierDispatchPlan { launch, program })
383}
384
385#[must_use]
399pub fn dominator_frontier(
400 node_count: u32,
401 dom_edge_count: u32,
402 pred_edge_count: u32,
403 seed: &str,
404 out: &str,
405) -> Program {
406 try_dominator_frontier(node_count, dom_edge_count, pred_edge_count, seed, out)
410 .unwrap_or_else(|error| panic!("{error}"))
411}
412
413pub fn try_dominator_frontier(
416 node_count: u32,
417 dom_edge_count: u32,
418 pred_edge_count: u32,
419 seed: &str,
420 out: &str,
421) -> Result<Program, String> {
422 let words = bitset_words(node_count).max(1);
423 let offset_count = node_count.checked_add(1).ok_or_else(|| {
424 format!(
425 "dominator_frontier node_count={node_count} overflows CSR offset buffer count. Fix: shard the graph before GPU dispatch."
426 )
427 });
428 let offset_count = offset_count?;
429 let t = Expr::InvocationId { axis: 0 };
430 let dominator_is_seed = vec![
431 Node::let_bind(
432 "seed_word",
433 Expr::load(seed, Expr::shr(Expr::var("n"), Expr::u32(5))),
434 ),
435 Node::let_bind(
436 "seed_bit",
437 Expr::shl(Expr::u32(1), Expr::bitand(Expr::var("n"), Expr::u32(31))),
438 ),
439 Node::if_then(
440 Expr::ne(
441 Expr::bitand(Expr::var("seed_word"), Expr::var("seed_bit")),
442 Expr::u32(0),
443 ),
444 vec![
445 Node::let_bind(
446 "pred_start",
447 Expr::load("pred_offsets", Expr::var("candidate")),
448 ),
449 Node::let_bind(
450 "pred_end",
451 Expr::load(
452 "pred_offsets",
453 Expr::add(Expr::var("candidate"), Expr::u32(1)),
454 ),
455 ),
456 Node::let_bind("dominates_a_predecessor", Expr::u32(0)),
457 Node::loop_for(
458 "p",
459 Expr::var("pred_start"),
460 Expr::var("pred_end"),
461 vec![Node::if_then(
462 Expr::eq(Expr::var("dominates_a_predecessor"), Expr::u32(0)),
463 vec![
464 Node::let_bind("pred", Expr::load("pred_targets", Expr::var("p"))),
465 Node::let_bind(
466 "dom_start_pred",
467 Expr::load("dom_offsets", Expr::var("n")),
468 ),
469 Node::let_bind(
470 "dom_end_pred",
471 Expr::load("dom_offsets", Expr::add(Expr::var("n"), Expr::u32(1))),
472 ),
473 Node::loop_for(
474 "d_pred",
475 Expr::var("dom_start_pred"),
476 Expr::var("dom_end_pred"),
477 vec![Node::if_then(
478 Expr::eq(
479 Expr::load("dom_targets", Expr::var("d_pred")),
480 Expr::var("pred"),
481 ),
482 vec![Node::assign("dominates_a_predecessor", Expr::u32(1))],
483 )],
484 ),
485 ],
486 )],
487 ),
488 Node::let_bind("dominates_candidate", Expr::u32(0)),
489 Node::let_bind(
490 "dom_start_candidate",
491 Expr::load("dom_offsets", Expr::var("n")),
492 ),
493 Node::let_bind(
494 "dom_end_candidate",
495 Expr::load("dom_offsets", Expr::add(Expr::var("n"), Expr::u32(1))),
496 ),
497 Node::loop_for(
498 "d_candidate",
499 Expr::var("dom_start_candidate"),
500 Expr::var("dom_end_candidate"),
501 vec![Node::if_then(
502 Expr::eq(
503 Expr::load("dom_targets", Expr::var("d_candidate")),
504 Expr::var("candidate"),
505 ),
506 vec![Node::assign("dominates_candidate", Expr::u32(1))],
507 )],
508 ),
509 Node::let_bind("strictly_dominates_candidate", Expr::u32(0)),
510 Node::if_then(
511 Expr::and(
512 Expr::eq(Expr::var("dominates_candidate"), Expr::u32(1)),
513 Expr::ne(Expr::var("n"), Expr::var("candidate")),
514 ),
515 vec![Node::assign("strictly_dominates_candidate", Expr::u32(1))],
516 ),
517 Node::if_then(
518 Expr::and(
519 Expr::eq(Expr::var("dominates_a_predecessor"), Expr::u32(1)),
520 Expr::eq(Expr::var("strictly_dominates_candidate"), Expr::u32(0)),
521 ),
522 vec![
523 Node::let_bind(
524 "candidate_word",
525 Expr::shr(Expr::var("candidate"), Expr::u32(5)),
526 ),
527 Node::let_bind(
528 "candidate_bit",
529 Expr::shl(
530 Expr::u32(1),
531 Expr::bitand(Expr::var("candidate"), Expr::u32(31)),
532 ),
533 ),
534 Node::let_bind(
535 "_prev",
536 Expr::atomic_or(
537 out,
538 Expr::var("candidate_word"),
539 Expr::var("candidate_bit"),
540 ),
541 ),
542 ],
543 ),
544 ],
545 ),
546 ];
547 Ok(Program::wrapped(
548 vec![
549 BufferDecl::storage(
550 "dom_offsets",
551 DOMINATOR_FRONTIER_DOM_OFFSETS_BUFFER,
552 BufferAccess::ReadOnly,
553 DataType::U32,
554 )
555 .with_count(offset_count),
556 BufferDecl::storage(
557 "dom_targets",
558 DOMINATOR_FRONTIER_DOM_TARGETS_BUFFER,
559 BufferAccess::ReadOnly,
560 DataType::U32,
561 )
562 .with_count(dom_edge_count.max(1)),
563 BufferDecl::storage(
564 "pred_offsets",
565 DOMINATOR_FRONTIER_PRED_OFFSETS_BUFFER,
566 BufferAccess::ReadOnly,
567 DataType::U32,
568 )
569 .with_count(offset_count),
570 BufferDecl::storage(
571 "pred_targets",
572 DOMINATOR_FRONTIER_PRED_TARGETS_BUFFER,
573 BufferAccess::ReadOnly,
574 DataType::U32,
575 )
576 .with_count(pred_edge_count.max(1)),
577 BufferDecl::storage(
578 seed,
579 DOMINATOR_FRONTIER_SEED_BUFFER,
580 BufferAccess::ReadOnly,
581 DataType::U32,
582 )
583 .with_count(words),
584 BufferDecl::storage(
585 out,
586 DOMINATOR_FRONTIER_OUT_BUFFER,
587 BufferAccess::ReadWrite,
588 DataType::U32,
589 )
590 .with_count(words),
591 ],
592 DOMINATOR_FRONTIER_WORKGROUP_SIZE,
593 vec![Node::Region {
594 generator: Ident::from(OP_ID),
595 source_region: None,
596 body: Arc::new(vec![Node::if_then(
597 Expr::lt(t.clone(), Expr::u32(node_count)),
598 vec![
599 Node::let_bind("candidate", t),
600 Node::loop_for("n", Expr::u32(0), Expr::u32(node_count), dominator_is_seed),
601 ],
602 )]),
603 }],
604 ))
605}
606
607#[must_use]
612#[cfg(any(test, feature = "cpu-parity"))]
613pub fn cpu_ref(
614 node_count: u32,
615 dom_offsets: &[u32],
616 dom_targets: &[u32],
617 pred_offsets: &[u32],
618 pred_targets: &[u32],
619 seed: &[u32],
620) -> Vec<u32> {
621 try_cpu_ref(
622 node_count,
623 dom_offsets,
624 dom_targets,
625 pred_offsets,
626 pred_targets,
627 seed,
628 )
629 .expect("Fix: reject malformed oracle input via try_* APIs; do not call panicking wrappers on hostile data - dominator_frontier CPU oracle received malformed input or could not reserve output")
630}
631
632#[cfg(any(test, feature = "cpu-parity"))]
637pub fn try_cpu_ref(
638 node_count: u32,
639 dom_offsets: &[u32],
640 dom_targets: &[u32],
641 pred_offsets: &[u32],
642 pred_targets: &[u32],
643 seed: &[u32],
644) -> Result<Vec<u32>, String> {
645 let mut frontier = Vec::new();
646 try_cpu_ref_into(
647 node_count,
648 dom_offsets,
649 dom_targets,
650 pred_offsets,
651 pred_targets,
652 seed,
653 &mut frontier,
654 )?;
655 Ok(frontier)
656}
657
658#[cfg(any(test, feature = "cpu-parity"))]
663pub fn cpu_ref_into(
664 node_count: u32,
665 dom_offsets: &[u32],
666 dom_targets: &[u32],
667 pred_offsets: &[u32],
668 pred_targets: &[u32],
669 seed: &[u32],
670 frontier: &mut Vec<u32>,
671) {
672 try_cpu_ref_into(
673 node_count,
674 dom_offsets,
675 dom_targets,
676 pred_offsets,
677 pred_targets,
678 seed,
679 frontier,
680 )
681 .expect("Fix: reject malformed oracle input via try_* APIs; do not call panicking wrappers on hostile data - dominator_frontier CPU oracle received malformed input or could not reserve output")
682}
683
684#[cfg(any(test, feature = "cpu-parity"))]
690pub fn try_cpu_ref_into(
691 node_count: u32,
692 dom_offsets: &[u32],
693 dom_targets: &[u32],
694 pred_offsets: &[u32],
695 pred_targets: &[u32],
696 seed: &[u32],
697 frontier: &mut Vec<u32>,
698) -> Result<(), String> {
699 let layout = validate_dominator_frontier_inputs(
700 node_count,
701 dom_offsets,
702 dom_targets,
703 pred_offsets,
704 pred_targets,
705 seed,
706 )?;
707 let words = layout.words;
708 crate::graph::scratch::reserve_graph_items(
709 frontier,
710 words,
711 "dominator frontier CPU oracle",
712 "frontier output",
713 )?;
714 frontier.clear();
715 frontier.resize(words, 0);
716 for n in 0..node_count {
717 let n_word = (n / 32) as usize;
718 let n_bit = 1u32 << (n % 32);
719 if seed[n_word] & n_bit == 0 {
720 continue;
721 }
722 for m in 0..node_count {
723 let pred_start = pred_offsets[m as usize] as usize;
724 let pred_end = pred_offsets[m as usize + 1] as usize;
725 let dominates_a_predecessor = pred_targets[pred_start..pred_end]
726 .iter()
727 .copied()
728 .any(|pred| dominates(dom_offsets, dom_targets, n, pred));
729 let strictly_dominates_m = n != m && dominates(dom_offsets, dom_targets, n, m);
730 if dominates_a_predecessor && !strictly_dominates_m {
731 let m_word = (m / 32) as usize;
732 let m_bit = 1u32 << (m % 32);
733 frontier[m_word] |= m_bit;
734 }
735 }
736 }
737 Ok(())
738}
739
740#[must_use]
742pub fn frontier_size(frontier: &[u32]) -> u32 {
743 frontier.iter().map(|word| word.count_ones()).sum()
744}
745
746pub fn validate_csr_shape(
754 label: &str,
755 node_count: u32,
756 offsets: &[u32],
757 targets: &[u32],
758) -> Result<u32, String> {
759 let expected_offsets = (node_count as usize).checked_add(1).ok_or_else(|| {
760 format!(
761 "Fix: dominator_frontier {label} node_count + 1 overflows usize for node_count={node_count}."
762 )
763 })?;
764 if offsets.len() != expected_offsets {
765 return Err(format!(
766 "Fix: dominator_frontier {label} offsets length must be {expected_offsets}, got {}.",
767 offsets.len()
768 ));
769 }
770 let mut previous = 0u32;
771 for (idx, &offset) in offsets.iter().enumerate() {
772 if idx > 0 && offset < previous {
773 return Err(format!(
774 "Fix: dominator_frontier {label} offsets must be monotonic; offsets[{idx}]={offset} after {previous}."
775 ));
776 }
777 previous = offset;
778 }
779 if offsets.last().copied().unwrap_or(0) as usize != targets.len() {
780 return Err(format!(
781 "Fix: dominator_frontier {label} final offset must equal target count {}, got {}.",
782 targets.len(),
783 offsets.last().copied().unwrap_or(0)
784 ));
785 }
786 for (idx, &target) in targets.iter().enumerate() {
787 if target >= node_count {
788 return Err(format!(
789 "Fix: dominator_frontier {label} target[{idx}]={target} is outside node_count {node_count}."
790 ));
791 }
792 }
793 u32::try_from(targets.len()).map_err(|_| {
794 format!(
795 "Fix: dominator_frontier {label} target count {} exceeds u32 index space.",
796 targets.len()
797 )
798 })
799}
800
801pub fn validate_dominator_frontier_inputs(
808 node_count: u32,
809 dom_offsets: &[u32],
810 dom_targets: &[u32],
811 pred_offsets: &[u32],
812 pred_targets: &[u32],
813 seed: &[u32],
814) -> Result<DominatorFrontierLayout, String> {
815 let words = bitset_words(node_count) as usize;
816 if seed.len() != words {
817 return Err(format!(
818 "Fix: dominator_frontier expected seed length {words} words for {node_count} nodes, got {}.",
819 seed.len()
820 ));
821 }
822 let dom_edge_count = validate_csr_shape("dominance", node_count, dom_offsets, dom_targets)?;
823 let pred_edge_count =
824 validate_csr_shape("predecessor", node_count, pred_offsets, pred_targets)?;
825 Ok(DominatorFrontierLayout {
826 words,
827 dom_edge_count,
828 pred_edge_count,
829 })
830}
831
832#[cfg(any(test, feature = "cpu-parity"))]
833fn dominates(dom_offsets: &[u32], dom_targets: &[u32], dominator: u32, node: u32) -> bool {
834 let start = dom_offsets[dominator as usize] as usize;
835 let end = dom_offsets[dominator as usize + 1] as usize;
836 dom_targets[start..end].contains(&node)
837}
838
839#[cfg(feature = "inventory-registry")]
840inventory::submit! {
841 vyre_foundation::operation::OperationRegistration::primitive(
842 OP_ID,
843 || dominator_frontier(4, 4, 4, "idom", "df"),
844 Some(|| {
845 vec![vec![
846 crate::wire::pack_u32_slice(&[0, 1, 2, 3, 4]),
847 crate::wire::pack_u32_slice(&[0, 1, 2, 3]),
848 crate::wire::pack_u32_slice(&[0, 0, 1, 2, 3]),
849 crate::wire::pack_u32_slice(&[0, 1, 2, 0]),
850 crate::wire::pack_u32_slice(&[0]),
851 crate::wire::pack_u32_slice(&[0]),
852 ]]
853 }),
854 Some(|| {
855 vec![vec![crate::wire::pack_u32_slice(&[0])]]
856 }),
857 )
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn empty_seed_yields_empty_frontier() {
866 let out = cpu_ref(4, &[0, 0, 0, 0, 0], &[], &[0, 0, 0, 0, 0], &[], &[0]);
867 assert_eq!(out, vec![0]);
868 }
869
870 #[test]
871 fn single_node_with_no_predecessors_has_empty_frontier() {
872 let out = cpu_ref(2, &[0, 0, 0], &[], &[0, 0, 0], &[], &[0b01]);
874 assert_eq!(out, vec![0]);
875 }
876
877 #[test]
878 fn dom_frontier_picks_up_join_node() {
879 let pred_offsets = vec![0u32, 0, 1, 2, 4];
884 let pred_targets = vec![0u32, 0, 1, 2];
885 let dom_offsets = vec![0u32, 4, 5, 6, 7];
888 let dom_targets = vec![0u32, 1, 2, 3, 1, 2, 3];
889 let out = cpu_ref(
890 4,
891 &dom_offsets,
892 &dom_targets,
893 &pred_offsets,
894 &pred_targets,
895 &[0b0010],
896 );
897 assert_eq!(out, vec![0b1000]);
898 }
899
900 #[test]
901 fn cpu_ref_into_reuses_frontier_storage() {
902 let mut out = Vec::with_capacity(8);
903 let dom_offsets = vec![0u32, 4, 5, 6, 7];
904 let dom_targets = vec![0u32, 1, 2, 3, 1, 2, 3];
905 let pred_offsets = vec![0u32, 0, 1, 2, 4];
906 let pred_targets = vec![0u32, 0, 1, 2];
907 cpu_ref_into(
908 4,
909 &dom_offsets,
910 &dom_targets,
911 &pred_offsets,
912 &pred_targets,
913 &[0b0010],
914 &mut out,
915 );
916 let capacity = out.capacity();
917 assert_eq!(out, vec![0b1000]);
918
919 cpu_ref_into(
920 4,
921 &dom_offsets,
922 &dom_targets,
923 &pred_offsets,
924 &pred_targets,
925 &[0],
926 &mut out,
927 );
928 assert_eq!(out.capacity(), capacity);
929 assert_eq!(out, vec![0]);
930 }
931
932 #[test]
933 fn cpu_ref_into_uses_shared_validation_before_output_mutation() {
934 let mut out = vec![0xCAFE_BABEu32];
935 let ptr = out.as_ptr();
936 let err = try_cpu_ref_into(
937 4,
938 &[0, 4, 5, 6, 7],
939 &[0, 1, 2, 3, 1, 2, 3],
940 &[0, 0, 1, 2, 4],
941 &[0, 1, 2, 3],
942 &[0b0010, 0],
943 &mut out,
944 );
945
946 assert!(err.is_err(), "extra seed word must be rejected");
947 assert_eq!(
948 out,
949 vec![0xCAFE_BABEu32],
950 "Fix: shared validation must reject malformed input before clearing caller output."
951 );
952 assert_eq!(out.as_ptr(), ptr);
953 }
954
955 #[test]
956 fn fallible_cpu_ref_matches_compatibility_oracle_on_generated_diamonds() {
957 for diamond_count in [1_u32, 2, 7, 16, 33, 64] {
958 let node_count = diamond_count
959 .checked_mul(4)
960 .expect("Fix: generated diamond node count should fit u32");
961 let words = bitset_words(node_count) as usize;
962 let mut dom_offsets = Vec::with_capacity(node_count as usize + 1);
963 let mut dom_targets = Vec::new();
964 let mut pred_offsets = Vec::with_capacity(node_count as usize + 1);
965 let mut pred_targets = Vec::new();
966 dom_offsets.push(0);
967 pred_offsets.push(0);
968 for diamond in 0..diamond_count {
969 let base = diamond * 4;
970 dom_targets.extend_from_slice(&[base, base + 1, base + 2, base + 3]);
971 dom_offsets.push(dom_targets.len() as u32);
972 dom_targets.push(base + 1);
973 dom_offsets.push(dom_targets.len() as u32);
974 dom_targets.push(base + 2);
975 dom_offsets.push(dom_targets.len() as u32);
976 dom_targets.push(base + 3);
977 dom_offsets.push(dom_targets.len() as u32);
978
979 pred_offsets.push(pred_targets.len() as u32);
980 pred_targets.push(base);
981 pred_offsets.push(pred_targets.len() as u32);
982 pred_targets.push(base);
983 pred_offsets.push(pred_targets.len() as u32);
984 pred_targets.extend_from_slice(&[base + 1, base + 2]);
985 pred_offsets.push(pred_targets.len() as u32);
986 }
987 let mut seed = vec![0; words];
988 for diamond in 0..diamond_count {
989 let node = diamond * 4 + 1;
990 seed[(node / 32) as usize] |= 1_u32 << (node % 32);
991 }
992
993 let expected = cpu_ref(
994 node_count,
995 &dom_offsets,
996 &dom_targets,
997 &pred_offsets,
998 &pred_targets,
999 &seed,
1000 );
1001 let actual = try_cpu_ref(
1002 node_count,
1003 &dom_offsets,
1004 &dom_targets,
1005 &pred_offsets,
1006 &pred_targets,
1007 &seed,
1008 )
1009 .expect("Fix: generated dominance-frontier diamonds should run fallibly");
1010 assert_eq!(actual, expected, "diamond_count={diamond_count}");
1011 }
1012 }
1013
1014 #[test]
1015 fn reusable_validation_rejects_bad_csr_and_seed() {
1016 let err = validate_dominator_frontier_inputs(2, &[0, 1, 1], &[1], &[0, 1, 0], &[0], &[1])
1017 .unwrap_err();
1018 assert!(err.contains("predecessor offsets must be monotonic"));
1019
1020 let err =
1021 validate_dominator_frontier_inputs(33, &[0; 34], &[], &[0; 34], &[], &[1]).unwrap_err();
1022 assert!(err.contains("expected seed length 2 words"));
1023 }
1024
1025 #[test]
1026 fn reusable_validation_returns_dispatch_layout() {
1027 let layout = validate_dominator_frontier_inputs(
1028 4,
1029 &[0, 4, 5, 6, 7],
1030 &[0, 1, 2, 3, 1, 2, 3],
1031 &[0, 0, 1, 2, 4],
1032 &[0, 1, 2, 3],
1033 &[0b0010],
1034 )
1035 .expect("Fix: canonical dominance-frontier input should validate");
1036
1037 assert_eq!(
1038 layout,
1039 DominatorFrontierLayout {
1040 words: 1,
1041 dom_edge_count: 7,
1042 pred_edge_count: 4,
1043 }
1044 );
1045 }
1046
1047 #[test]
1048 fn launch_plan_validates_layout_without_eager_program_materialization() {
1049 let plan = plan_dominator_frontier_launch(
1050 4,
1051 &[0, 4, 5, 6, 7],
1052 &[0, 1, 2, 3, 1, 2, 3],
1053 &[0, 0, 1, 2, 4],
1054 &[0, 1, 2, 3],
1055 &[0b0010],
1056 )
1057 .expect("Fix: canonical dominance-frontier launch plan should validate");
1058
1059 assert_eq!(plan.dispatch_grid(), [1, 1, 1]);
1060 assert_eq!(plan.frontier_words(), 1);
1061 assert_eq!(plan.dom_target_words(), 7);
1062 assert_eq!(plan.pred_target_words(), 4);
1063 assert_eq!(
1064 plan.shape(),
1065 DominatorFrontierProgramShape {
1066 node_count: 4,
1067 dom_edge_count: 7,
1068 pred_edge_count: 4,
1069 }
1070 );
1071 assert_eq!(
1072 plan.program("seed", "frontier_out")
1073 .expect("Fix: validated launch plan should materialize IR")
1074 .workgroup_size,
1075 DOMINATOR_FRONTIER_WORKGROUP_SIZE
1076 );
1077 }
1078
1079 #[test]
1080 fn launch_plan_packs_candidate_lanes_into_blocks() {
1081 assert_eq!(dominator_frontier_dispatch_grid(0), [0, 1, 1]);
1082 assert_eq!(dominator_frontier_dispatch_grid(1), [1, 1, 1]);
1083 assert_eq!(dominator_frontier_dispatch_grid(256), [1, 1, 1]);
1084 assert_eq!(dominator_frontier_dispatch_grid(257), [2, 1, 1]);
1085 assert_eq!(dominator_frontier_dispatch_grid(513), [3, 1, 1]);
1086 }
1087
1088 #[test]
1089 fn generated_launch_grid_covers_candidate_shapes_to_8192() {
1090 for node_count in 1..=8_192 {
1091 let grid = dominator_frontier_dispatch_grid(node_count);
1092 assert_eq!(
1093 grid[1], 1,
1094 "Fix: dominator-frontier grid y dimension drifted at node_count={node_count}"
1095 );
1096 assert_eq!(
1097 grid[2], 1,
1098 "Fix: dominator-frontier grid z dimension drifted at node_count={node_count}"
1099 );
1100 assert!(
1101 grid[0] * DOMINATOR_FRONTIER_WORKGROUP_SIZE[0] >= node_count,
1102 "Fix: dominator-frontier grid under-covers node_count={node_count}"
1103 );
1104 assert!(
1105 grid[0] == 1 || (grid[0] - 1) * DOMINATOR_FRONTIER_WORKGROUP_SIZE[0] < node_count,
1106 "Fix: dominator-frontier grid over-launches an avoidable extra block at node_count={node_count}"
1107 );
1108 }
1109 }
1110
1111 #[test]
1112 fn dispatch_plan_owns_buffer_slots_grid_and_readback_shape() {
1113 let plan = plan_dominator_frontier_dispatch(
1114 4,
1115 &[0, 4, 5, 6, 7],
1116 &[0, 1, 2, 3, 1, 2, 3],
1117 &[0, 0, 1, 2, 4],
1118 &[0, 1, 2, 3],
1119 &[0b0010],
1120 "seed",
1121 "frontier_out",
1122 )
1123 .expect("Fix: canonical dominance-frontier plan should validate");
1124
1125 assert_eq!(plan.dispatch_grid(), [1, 1, 1]);
1126 assert_eq!(plan.frontier_words(), 1);
1127 assert_eq!(plan.dom_target_words(), 7);
1128 assert_eq!(plan.pred_target_words(), 4);
1129 assert_eq!(
1130 plan.program().workgroup_size,
1131 DOMINATOR_FRONTIER_WORKGROUP_SIZE
1132 );
1133 let bindings = plan
1134 .program()
1135 .buffers
1136 .iter()
1137 .map(|buffer| buffer.binding)
1138 .collect::<Vec<_>>();
1139 assert_eq!(
1140 bindings,
1141 vec![
1142 DOMINATOR_FRONTIER_DOM_OFFSETS_BUFFER,
1143 DOMINATOR_FRONTIER_DOM_TARGETS_BUFFER,
1144 DOMINATOR_FRONTIER_PRED_OFFSETS_BUFFER,
1145 DOMINATOR_FRONTIER_PRED_TARGETS_BUFFER,
1146 DOMINATOR_FRONTIER_SEED_BUFFER,
1147 DOMINATOR_FRONTIER_OUT_BUFFER,
1148 ]
1149 );
1150 }
1151
1152 #[test]
1153 fn dispatch_plan_pads_empty_target_buffers_without_hiding_empty_offsets() {
1154 let plan = plan_dominator_frontier_dispatch(
1155 1,
1156 &[0, 0],
1157 &[],
1158 &[0, 0],
1159 &[],
1160 &[1],
1161 "seed",
1162 "frontier_out",
1163 )
1164 .expect("Fix: empty edge sets are valid CSR inputs");
1165
1166 assert_eq!(plan.layout().dom_edge_count, 0);
1167 assert_eq!(plan.layout().pred_edge_count, 0);
1168 assert_eq!(plan.dom_target_words(), 1);
1169 assert_eq!(plan.pred_target_words(), 1);
1170 }
1171
1172 #[test]
1173 fn frontier_size_counts_set_bits() {
1174 assert_eq!(frontier_size(&[0]), 0);
1175 assert_eq!(frontier_size(&[0b1011]), 3);
1176 assert_eq!(frontier_size(&[u32::MAX, 1]), 33);
1177 }
1178
1179 #[test]
1180 fn checked_builder_rejects_offset_count_overflow() {
1181 let error = try_dominator_frontier(u32::MAX, 0, 0, "seed", "out")
1182 .expect_err("checked dominator-frontier builder must reject CSR offset overflow");
1183
1184 assert!(
1185 error.contains("overflows CSR offset buffer count"),
1186 "error should describe the CSR offset overflow: {error}"
1187 );
1188 }
1189
1190 #[test]
1191 fn legacy_builder_fails_fast_on_offset_count_overflow() {
1192 let panic = std::panic::catch_unwind(|| {
1193 let _ = dominator_frontier(u32::MAX, 0, 0, "seed", "out");
1194 })
1195 .expect_err("legacy dominator-frontier builder must fail fast on CSR offset overflow");
1196
1197 let message = panic_payload_message(panic);
1198 assert!(
1199 message.contains("overflows CSR offset buffer count"),
1200 "error should describe the CSR offset overflow: {message}"
1201 );
1202 }
1203
1204 fn panic_payload_message(payload: Box<dyn std::any::Any + Send>) -> String {
1205 if let Some(message) = payload.downcast_ref::<&str>() {
1206 message.to_string()
1207 } else if let Some(message) = payload.downcast_ref::<String>() {
1208 message.clone()
1209 } else {
1210 format!("{payload:?}")
1211 }
1212 }
1213
1214 #[test]
1215 fn missing_seed_word_fails_loudly() {
1216 let previous_hook = std::panic::take_hook();
1217 std::panic::set_hook(Box::new(|_| {}));
1218 let err = std::panic::catch_unwind(|| {
1219 let _ = cpu_ref(2, &[0, 0, 0], &[], &[0, 0, 0], &[], &[]);
1220 });
1221 std::panic::set_hook(previous_hook);
1222
1223 let payload = err.expect_err("missing seed word must fail loudly");
1224 let message = payload
1225 .downcast_ref::<String>()
1226 .map(String::as_str)
1227 .or_else(|| payload.downcast_ref::<&str>().copied())
1228 .unwrap_or("<non-string panic>");
1229 assert!(
1230 message.contains("expected seed length"),
1231 "Fix: missing seed panic should explain the exact seed length mismatch, got: {message}"
1232 );
1233 }
1234}