1use super::padded_u32_slice_fingerprint as csr_bidirectional_padded_slice_fingerprint;
6use vyre_foundation::execution_plan::fusion::fuse_programs;
7use vyre_foundation::ir::{DataType, Program};
8
9use crate::graph::csr_backward_traverse::csr_backward_traverse;
10use crate::graph::csr_forward_traverse::{bitset_words, csr_forward_traverse};
11use crate::graph::csr_frontier_step::csr_frontier_step_dispatch_grid;
12use crate::graph::program_graph::ProgramGraphShape;
13
14pub const OP_ID: &str = "vyre-primitives::graph::csr_bidirectional";
16pub const CSR_BIDIRECTIONAL_NODES_BUFFER: &str = "csr_bidirectional nodes";
18pub const CSR_BIDIRECTIONAL_OFFSETS_BUFFER: &str = "csr_bidirectional edge_offsets";
20pub const CSR_BIDIRECTIONAL_TARGETS_BUFFER: &str = "csr_bidirectional edge_targets";
22pub const CSR_BIDIRECTIONAL_EDGE_KIND_BUFFER: &str = "csr_bidirectional edge_kind_mask";
24pub const CSR_BIDIRECTIONAL_NODE_TAGS_BUFFER: &str = "csr_bidirectional node_tags";
26pub const CSR_BIDIRECTIONAL_FRONTIER_IN_BUFFER: &str = "csr_bidirectional frontier_in";
28pub const CSR_BIDIRECTIONAL_FRONTIER_OUT_BUFFER: &str = "csr_bidirectional frontier_out";
30
31#[must_use]
35pub fn csr_bidirectional(
36 shape: ProgramGraphShape,
37 frontier_in: &str,
38 frontier_out: &str,
39 edge_kind_mask: u32,
40) -> Program {
41 let fwd = csr_forward_traverse(shape, frontier_in, frontier_out, edge_kind_mask);
42 let bwd = csr_backward_traverse(shape, frontier_in, frontier_out, edge_kind_mask);
43 fuse_programs(&[fwd, bwd]).unwrap_or_else(|error| {
44 crate::invalid_output_program(
45 OP_ID,
46 frontier_out,
47 DataType::U32,
48 format!("Fix: csr_bidirectional forward+backward fusion failed: {error}"),
49 )
50 })
51}
52
53#[must_use]
55#[cfg(any(test, feature = "cpu-parity"))]
56pub fn cpu_ref(
57 node_count: u32,
58 edge_offsets: &[u32],
59 edge_targets: &[u32],
60 edge_kind_mask: &[u32],
61 frontier_in: &[u32],
62 allow_mask: u32,
63) -> Vec<u32> {
64 try_cpu_ref(
65 node_count,
66 edge_offsets,
67 edge_targets,
68 edge_kind_mask,
69 frontier_in,
70 allow_mask,
71 )
72 .unwrap_or_else(|err| panic!("csr_bidirectional CPU oracle received malformed input. {err}"))
73}
74
75#[cfg(any(test, feature = "cpu-parity"))]
81pub fn try_cpu_ref(
82 node_count: u32,
83 edge_offsets: &[u32],
84 edge_targets: &[u32],
85 edge_kind_mask: &[u32],
86 frontier_in: &[u32],
87 allow_mask: u32,
88) -> Result<Vec<u32>, String> {
89 let mut out = Vec::new();
90 try_cpu_ref_into(
91 node_count,
92 edge_offsets,
93 edge_targets,
94 edge_kind_mask,
95 frontier_in,
96 allow_mask,
97 &mut out,
98 )?;
99 Ok(out)
100}
101
102#[cfg(any(test, feature = "cpu-parity"))]
104pub fn cpu_ref_into(
105 node_count: u32,
106 edge_offsets: &[u32],
107 edge_targets: &[u32],
108 edge_kind_mask: &[u32],
109 frontier_in: &[u32],
110 allow_mask: u32,
111 out: &mut Vec<u32>,
112) {
113 try_cpu_ref_into(
114 node_count,
115 edge_offsets,
116 edge_targets,
117 edge_kind_mask,
118 frontier_in,
119 allow_mask,
120 out,
121 )
122 .unwrap_or_else(|err| panic!("csr_bidirectional CPU oracle received malformed input. {err}"));
123}
124
125#[cfg(any(test, feature = "cpu-parity"))]
130pub fn try_cpu_ref_into(
131 node_count: u32,
132 edge_offsets: &[u32],
133 edge_targets: &[u32],
134 edge_kind_mask: &[u32],
135 frontier_in: &[u32],
136 allow_mask: u32,
137 out: &mut Vec<u32>,
138) -> Result<(), String> {
139 let layout = validate_csr_inputs(
140 node_count,
141 edge_offsets,
142 edge_targets,
143 edge_kind_mask,
144 frontier_in,
145 )?;
146 crate::graph::scratch::reserve_graph_items_with(
147 out,
148 layout.words,
149 "csr_bidirectional CPU oracle",
150 "bidirectional step output",
151 |message| message,
152 )?;
153 cpu_ref_into_validated(
154 layout,
155 edge_offsets,
156 edge_targets,
157 edge_kind_mask,
158 frontier_in,
159 allow_mask,
160 out,
161 )
162}
163
164#[cfg(any(test, feature = "cpu-parity"))]
165fn cpu_ref_into_validated(
166 layout: CsrBidirectionalLayout,
167 edge_offsets: &[u32],
168 edge_targets: &[u32],
169 edge_kind_mask: &[u32],
170 frontier_in: &[u32],
171 allow_mask: u32,
172 out: &mut Vec<u32>,
173) -> Result<(), String> {
174 out.clear();
175 out.resize(layout.words, 0);
176 for src in 0..layout.node_words {
177 let src_word = src / 32;
178 let src_bit = 1u32 << (src % 32);
179 let src_in_frontier =
180 src_word < frontier_in.len() && (frontier_in[src_word] & src_bit) != 0;
181 let edge_start = csr_bidir_u32_to_usize(edge_offsets[src], "edge start offset")?;
182 let edge_end = csr_bidir_u32_to_usize(edge_offsets[src + 1], "edge end offset")?;
183 let mut backward_hit = false;
184 for edge in edge_start..edge_end.min(edge_targets.len()).min(edge_kind_mask.len()) {
185 if edge_kind_mask[edge] & allow_mask == 0 {
186 continue;
187 }
188 let dst = csr_bidir_u32_to_usize(edge_targets[edge], "edge target")?;
189 let dst_word = dst / 32;
190 let dst_bit = 1u32 << (dst % 32);
191 if src_in_frontier && dst < layout.node_words {
192 out[dst_word] |= dst_bit;
193 }
194 if dst_word < frontier_in.len() && (frontier_in[dst_word] & dst_bit) != 0 {
195 backward_hit = true;
196 }
197 }
198 if backward_hit && src_word < out.len() {
199 out[src_word] |= src_bit;
200 }
201 }
202 Ok(())
203}
204
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
210pub struct CsrBidirectionalLayout {
211 pub node_count: u32,
213 pub words: usize,
215 pub node_words: usize,
217 pub edge_count: u32,
219 pub edge_storage_words: usize,
221}
222
223#[derive(Clone, Debug, Eq, PartialEq)]
225pub struct CsrBidirectionalDispatchPlan {
226 pub layout: CsrBidirectionalLayout,
228 pub allow_mask: u32,
230 pub grid: [u32; 3],
232 pub node_words: usize,
234 pub edge_storage_words: usize,
236 pub frontier_words: usize,
238}
239
240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242pub struct CsrBidirectionalProgramKey {
243 pub layout: CsrBidirectionalLayout,
245 pub allow_mask: u32,
247}
248
249#[derive(Clone, Copy, Debug, Eq, PartialEq)]
256pub struct CsrBidirectionalStaticInputKey {
257 pub program_key: CsrBidirectionalProgramKey,
259 pub edge_offset_words: usize,
261 pub edge_storage_words: usize,
263 pub edge_offsets_hash: u64,
265 pub edge_targets_hash: u64,
267 pub edge_kind_mask_hash: u64,
269}
270
271impl CsrBidirectionalDispatchPlan {
272 #[must_use]
274 pub const fn program_key(&self) -> CsrBidirectionalProgramKey {
275 CsrBidirectionalProgramKey {
276 layout: self.layout,
277 allow_mask: self.allow_mask,
278 }
279 }
280
281 #[must_use]
283 pub fn program(&self) -> Program {
284 csr_bidirectional(
285 ProgramGraphShape::new(self.layout.node_count, self.layout.edge_count.max(1)),
286 CSR_BIDIRECTIONAL_FRONTIER_IN_BUFFER,
287 CSR_BIDIRECTIONAL_FRONTIER_OUT_BUFFER,
288 self.allow_mask,
289 )
290 }
291
292 #[must_use]
296 pub const fn edge_buffers_can_dispatch_unpadded(
297 &self,
298 edge_targets_len: usize,
299 edge_kind_mask_len: usize,
300 ) -> bool {
301 can_dispatch_edge_buffers_without_padding(
302 edge_targets_len,
303 edge_kind_mask_len,
304 self.edge_storage_words,
305 )
306 }
307
308 pub fn static_input_key(
315 &self,
316 edge_offsets: &[u32],
317 edge_targets: &[u32],
318 edge_kind_mask: &[u32],
319 ) -> Result<CsrBidirectionalStaticInputKey, String> {
320 let expected_offsets = self.layout.node_words.checked_add(1).ok_or_else(|| {
321 format!(
322 "Fix: csr_bidirectional static key node_words + 1 overflows usize for node_words={}.",
323 self.layout.node_words
324 )
325 })?;
326 if edge_offsets.len() != expected_offsets {
327 return Err(format!(
328 "Fix: csr_bidirectional static key expected {expected_offsets} offset word(s), got {}.",
329 edge_offsets.len()
330 ));
331 }
332 let expected_edges = self.layout.edge_count as usize;
333 if edge_targets.len() != expected_edges {
334 return Err(format!(
335 "Fix: csr_bidirectional static key expected {expected_edges} edge target word(s), got {}.",
336 edge_targets.len()
337 ));
338 }
339 if edge_kind_mask.len() != expected_edges {
340 return Err(format!(
341 "Fix: csr_bidirectional static key expected {expected_edges} edge kind word(s), got {}.",
342 edge_kind_mask.len()
343 ));
344 }
345 Ok(CsrBidirectionalStaticInputKey {
346 program_key: self.program_key(),
347 edge_offset_words: expected_offsets,
348 edge_storage_words: self.edge_storage_words,
349 edge_offsets_hash: csr_bidirectional_padded_slice_fingerprint(
350 edge_offsets,
351 expected_offsets,
352 ),
353 edge_targets_hash: csr_bidirectional_padded_slice_fingerprint(
354 edge_targets,
355 self.edge_storage_words,
356 ),
357 edge_kind_mask_hash: csr_bidirectional_padded_slice_fingerprint(
358 edge_kind_mask,
359 self.edge_storage_words,
360 ),
361 })
362 }
363}
364
365#[must_use]
372pub const fn can_dispatch_edge_buffers_without_padding(
373 edge_targets_len: usize,
374 edge_kind_mask_len: usize,
375 edge_storage_words: usize,
376) -> bool {
377 edge_targets_len == edge_storage_words && edge_kind_mask_len == edge_storage_words
378}
379
380pub fn validate_csr_inputs(
391 node_count: u32,
392 edge_offsets: &[u32],
393 edge_targets: &[u32],
394 edge_kind_mask: &[u32],
395 frontier_in: &[u32],
396) -> Result<CsrBidirectionalLayout, String> {
397 let expected_offsets = (node_count as usize).checked_add(1).ok_or_else(|| {
398 format!(
399 "Fix: csr_bidirectional node_count + 1 overflows usize for node_count={node_count}."
400 )
401 })?;
402 if edge_offsets.len() != expected_offsets {
403 return Err(format!(
404 "Fix: csr_bidirectional requires edge_offsets.len() == node_count + 1, got len={}, node_count={node_count}.",
405 edge_offsets.len()
406 ));
407 }
408
409 let expected_frontier_words = bitset_words(node_count) as usize;
410 if frontier_in.len() != expected_frontier_words {
411 return Err(format!(
412 "Fix: csr_bidirectional expected frontier length {expected_frontier_words} words for {node_count} nodes, got {}.",
413 frontier_in.len()
414 ));
415 }
416
417 if edge_targets.len() != edge_kind_mask.len() {
418 return Err(format!(
419 "Fix: csr_bidirectional requires edge_targets.len() == edge_kind_mask.len(), got {} vs {}.",
420 edge_targets.len(),
421 edge_kind_mask.len()
422 ));
423 }
424
425 if let Some(&first) = edge_offsets.first() {
426 if first != 0 {
427 return Err(format!(
428 "Fix: csr_bidirectional requires edge_offsets[0] == 0, got {first}."
429 ));
430 }
431 }
432 for (index, pair) in edge_offsets.windows(2).enumerate() {
433 if pair[0] > pair[1] {
434 return Err(format!(
435 "Fix: csr_bidirectional offsets must be monotonic; offsets[{index}]={} > offsets[{}]={}.",
436 pair[0],
437 index + 1,
438 pair[1]
439 ));
440 }
441 }
442
443 let edge_count = edge_offsets[expected_offsets - 1] as usize;
444 if edge_targets.len() != edge_count {
445 return Err(format!(
446 "Fix: csr_bidirectional final offset declares edge_count={edge_count}, but targets_len={} and kind_mask_len={}.",
447 edge_targets.len(),
448 edge_kind_mask.len()
449 ));
450 }
451 for (index, &target) in edge_targets.iter().enumerate() {
452 if target >= node_count {
453 return Err(format!(
454 "Fix: csr_bidirectional edge_targets[{index}]={target} is outside node_count {node_count}."
455 ));
456 }
457 }
458 let edge_count = u32::try_from(edge_count).map_err(|_| {
459 format!("Fix: csr_bidirectional edge count {edge_count} exceeds u32 index space.")
460 })?;
461 Ok(CsrBidirectionalLayout {
462 node_count,
463 words: expected_frontier_words,
464 node_words: node_count as usize,
465 edge_count,
466 edge_storage_words: edge_targets.len().max(1),
467 })
468}
469
470pub fn plan_csr_bidirectional_step(
472 node_count: u32,
473 edge_offsets: &[u32],
474 edge_targets: &[u32],
475 edge_kind_mask: &[u32],
476 frontier_in: &[u32],
477 allow_mask: u32,
478) -> Result<CsrBidirectionalDispatchPlan, String> {
479 let layout = validate_csr_inputs(
480 node_count,
481 edge_offsets,
482 edge_targets,
483 edge_kind_mask,
484 frontier_in,
485 )?;
486 Ok(CsrBidirectionalDispatchPlan {
487 node_words: layout.node_words,
488 edge_storage_words: layout.edge_storage_words,
489 frontier_words: layout.words,
490 grid: csr_frontier_step_dispatch_grid(layout.node_count),
491 allow_mask,
492 layout,
493 })
494}
495
496#[allow(clippy::too_many_arguments)]
508pub fn run_csr_bidirectional_closure_plan_with_step<E, MapError, Step>(
509 plan: &CsrBidirectionalDispatchPlan,
510 seed: &[u32],
511 max_iters: u32,
512 current: &mut Vec<u32>,
513 next: &mut Vec<u32>,
514 mut map_error: MapError,
515 mut step: Step,
516) -> Result<(), E>
517where
518 MapError: FnMut(String) -> E,
519 Step: FnMut(&[u32], &mut Vec<u32>) -> Result<(), E>,
520{
521 if seed.len() != plan.frontier_words {
522 return Err(map_error(format!(
523 "Fix: csr_bidirectional closure expected seed length {} words for {} nodes, got {}.",
524 plan.frontier_words,
525 plan.layout.node_count,
526 seed.len()
527 )));
528 }
529 crate::graph::scratch::reserve_graph_items_with(
530 current,
531 plan.frontier_words,
532 "csr_bidirectional closure runner",
533 "current frontier",
534 |message| map_error(message),
535 )?;
536 crate::graph::scratch::reserve_graph_items_with(
537 next,
538 plan.frontier_words,
539 "csr_bidirectional closure runner",
540 "next frontier",
541 |message| map_error(message),
542 )?;
543
544 current.clear();
545 current.extend_from_slice(seed);
546 next.clear();
547 if plan.layout.node_count == 0 || max_iters == 0 {
548 return Ok(());
549 }
550
551 for _ in 0..max_iters {
552 next.clear();
553 step(current, next)?;
554 if !try_merge_frontier_or_changed(current, next).map_err(&mut map_error)? {
555 return Ok(());
556 }
557 }
558 Ok(())
559}
560
561#[cfg(test)]
562mod dispatch_plan_tests {
563 use super::*;
564
565 #[test]
566 fn dispatch_plan_owns_buffer_sizes_grid_and_mask() {
567 let plan = plan_csr_bidirectional_step(
568 4,
569 &[0, 1, 2, 3, 3],
570 &[1, 2, 3],
571 &[1, 1, 1],
572 &[0b0010],
573 0x55AA_00FF,
574 )
575 .expect("Fix: valid bidirectional CSR step should produce dispatch plan");
576
577 assert_eq!(plan.grid, [1, 1, 1]);
579 assert_eq!(plan.node_words, 4);
580 assert_eq!(plan.edge_storage_words, 3);
581 assert_eq!(plan.frontier_words, 1);
582 assert_eq!(plan.allow_mask, 0x55AA_00FF);
583 assert_eq!(plan.layout.edge_count, 3);
584 }
585
586 #[test]
592 fn grid_x_is_ceil_node_count_div_workgroup_size_not_node_count() {
593 let plan_small = plan_csr_bidirectional_step(
595 4,
596 &[0, 1, 2, 3, 3],
597 &[1, 2, 3],
598 &[1, 1, 1],
599 &[0b0010],
600 u32::MAX,
601 )
602 .expect("Fix: valid 4-node bidirectional CSR step should produce dispatch plan");
603 assert_eq!(
604 plan_small.grid,
605 [1, 1, 1],
606 "4 nodes: expected ceil(4/256)=1 block, got {:?}",
607 plan_small.grid
608 );
609
610 let mut offsets = Vec::with_capacity(258);
613 offsets.push(0u32);
614 for i in 0..256u32 {
615 offsets.push(i + 1);
616 }
617 offsets.push(256u32); let targets: Vec<u32> = (1u32..=256).collect();
619 let kinds: Vec<u32> = vec![1u32; 256];
620 let frontier = vec![0u32; crate::bitset::bitset_words(257) as usize];
621
622 let plan_large =
623 plan_csr_bidirectional_step(257, &offsets, &targets, &kinds, &frontier, u32::MAX)
624 .expect("Fix: valid 257-node bidirectional CSR step should produce dispatch plan");
625 assert_eq!(
626 plan_large.grid,
627 [2, 1, 1],
628 "257 nodes: expected ceil(257/256)=2 blocks, got {:?}",
629 plan_large.grid
630 );
631 }
632
633 #[test]
634 fn dispatch_plan_pads_empty_edges_without_zero_sized_buffers() {
635 let plan = plan_csr_bidirectional_step(1, &[0, 0], &[], &[], &[0], u32::MAX)
636 .expect("Fix: edgeless one-node graph should still have dispatch buffers");
637
638 assert_eq!(plan.grid, [1, 1, 1]);
639 assert_eq!(plan.edge_storage_words, 1);
640 assert_eq!(plan.frontier_words, 1);
641 assert_eq!(plan.layout.edge_count, 0);
642 assert!(!plan.edge_buffers_can_dispatch_unpadded(0, 0));
643 }
644
645 #[test]
646 fn edge_buffer_unpadded_policy_is_primitive_owned() {
647 assert!(can_dispatch_edge_buffers_without_padding(3, 3, 3));
648 assert!(!can_dispatch_edge_buffers_without_padding(0, 0, 1));
649 assert!(!can_dispatch_edge_buffers_without_padding(3, 2, 3));
650 assert!(!can_dispatch_edge_buffers_without_padding(2, 3, 3));
651 }
652
653 #[test]
654 fn static_input_key_tracks_graph_content_and_padded_edge_storage() {
655 let plan = plan_csr_bidirectional_step(
656 4,
657 &[0, 1, 2, 3, 3],
658 &[1, 2, 3],
659 &[1, 1, 1],
660 &[0b0010],
661 0x55AA_00FF,
662 )
663 .expect("Fix: valid bidirectional CSR step should produce dispatch plan");
664
665 let first = plan
666 .static_input_key(&[0, 1, 2, 3, 3], &[1, 2, 3], &[1, 1, 1])
667 .expect("Fix: matching static CSR slices should key");
668 let same = plan
669 .static_input_key(&[0, 1, 2, 3, 3], &[1, 2, 3], &[1, 1, 1])
670 .expect("Fix: matching static CSR slices should key");
671 let changed_targets = plan
672 .static_input_key(&[0, 1, 2, 3, 3], &[2, 3, 0], &[1, 1, 1])
673 .expect("Fix: same-shape target content should key");
674 let changed_kind = plan
675 .static_input_key(&[0, 1, 2, 3, 3], &[1, 2, 3], &[1, 2, 1])
676 .expect("Fix: same-shape kind content should key");
677
678 assert_eq!(first, same);
679 assert_ne!(first, changed_targets);
680 assert_ne!(first, changed_kind);
681 assert_eq!(first.program_key, plan.program_key());
682 assert_eq!(first.edge_offset_words, 5);
683 assert_eq!(first.edge_storage_words, 3);
684 }
685
686 #[test]
687 fn static_input_key_normalizes_empty_edges_to_padded_upload() {
688 let plan = plan_csr_bidirectional_step(1, &[0, 0], &[], &[], &[0], u32::MAX)
689 .expect("Fix: edgeless one-node graph should still have dispatch buffers");
690 let key = plan
691 .static_input_key(&[0, 0], &[], &[])
692 .expect("Fix: empty edge buffers should key through padded primitive storage");
693
694 assert_eq!(key.edge_offset_words, 2);
695 assert_eq!(key.edge_storage_words, 1);
696 }
697
698 #[test]
699 fn static_input_key_rejects_shape_drift() {
700 let plan = plan_csr_bidirectional_step(
701 4,
702 &[0, 1, 2, 3, 3],
703 &[1, 2, 3],
704 &[1, 1, 1],
705 &[0b0010],
706 u32::MAX,
707 )
708 .expect("Fix: valid bidirectional CSR step should produce dispatch plan");
709
710 let err = plan
711 .static_input_key(&[0, 1, 2, 3], &[1, 2, 3], &[1, 1, 1])
712 .unwrap_err();
713 assert!(err.contains("expected 5 offset word"));
714
715 let err = plan
716 .static_input_key(&[0, 1, 2, 3, 3], &[1, 2], &[1, 1, 1])
717 .unwrap_err();
718 assert!(err.contains("expected 3 edge target"));
719
720 let err = plan
721 .static_input_key(&[0, 1, 2, 3, 3], &[1, 2, 3], &[1, 1])
722 .unwrap_err();
723 assert!(err.contains("expected 3 edge kind"));
724 }
725
726 #[test]
727 fn closure_runner_stops_after_fixpoint_and_reuses_buffers() {
728 let plan = plan_csr_bidirectional_step(4, &[0, 0, 0, 0, 0], &[], &[], &[0b0001], u32::MAX)
729 .expect("Fix: valid empty-edge CSR plan should build");
730 let mut current = Vec::with_capacity(4);
731 let mut next = Vec::with_capacity(4);
732 let mut calls = 0usize;
733
734 run_csr_bidirectional_closure_plan_with_step(
735 &plan,
736 &[0b0001],
737 9,
738 &mut current,
739 &mut next,
740 |message| message,
741 |_frontier, out| {
742 calls += 1;
743 out.extend_from_slice(&[0]);
744 Ok(())
745 },
746 )
747 .expect("Fix: closure runner should accept matching frontier shapes");
748
749 assert_eq!(calls, 1);
750 assert_eq!(current, vec![0b0001]);
751 assert!(current.capacity() >= 4);
752 assert!(next.capacity() >= 4);
753 }
754
755 #[test]
756 fn closure_runner_rejects_seed_width_drift_without_clobbering_buffers() {
757 let plan = plan_csr_bidirectional_step(4, &[0, 0, 0, 0, 0], &[], &[], &[0], u32::MAX)
758 .expect("Fix: valid empty-edge CSR plan should build");
759 let mut current = vec![0xAA55_AA55];
760 let mut next = vec![0x55AA_55AA];
761
762 let err = run_csr_bidirectional_closure_plan_with_step(
763 &plan,
764 &[0, 1],
765 1,
766 &mut current,
767 &mut next,
768 |message| message,
769 |_frontier, _out| Ok(()),
770 )
771 .expect_err("seed width drift must be rejected before mutation");
772
773 assert!(err.contains("expected seed length"));
774 assert_eq!(current, vec![0xAA55_AA55]);
775 assert_eq!(next, vec![0x55AA_55AA]);
776 }
777}
778
779#[must_use]
785#[cfg(any(test, feature = "cpu-parity"))]
786pub fn cpu_ref_closure(
787 node_count: u32,
788 edge_offsets: &[u32],
789 edge_targets: &[u32],
790 edge_kind_mask: &[u32],
791 seed: &[u32],
792 allow_mask: u32,
793 max_iters: u32,
794) -> Vec<u32> {
795 try_cpu_ref_closure(
796 node_count,
797 edge_offsets,
798 edge_targets,
799 edge_kind_mask,
800 seed,
801 allow_mask,
802 max_iters,
803 )
804 .unwrap_or_else(|err| {
805 panic!("csr_bidirectional closure CPU oracle received malformed input. {err}")
806 })
807}
808
809#[cfg(any(test, feature = "cpu-parity"))]
811pub fn try_cpu_ref_closure(
812 node_count: u32,
813 edge_offsets: &[u32],
814 edge_targets: &[u32],
815 edge_kind_mask: &[u32],
816 seed: &[u32],
817 allow_mask: u32,
818 max_iters: u32,
819) -> Result<Vec<u32>, String> {
820 let mut current = Vec::new();
821 let mut next = Vec::new();
822 try_cpu_ref_closure_into(
823 node_count,
824 edge_offsets,
825 edge_targets,
826 edge_kind_mask,
827 seed,
828 allow_mask,
829 max_iters,
830 &mut current,
831 &mut next,
832 )?;
833 Ok(current)
834}
835
836#[allow(clippy::too_many_arguments)]
838#[cfg(any(test, feature = "cpu-parity"))]
839pub fn cpu_ref_closure_into(
840 node_count: u32,
841 edge_offsets: &[u32],
842 edge_targets: &[u32],
843 edge_kind_mask: &[u32],
844 seed: &[u32],
845 allow_mask: u32,
846 max_iters: u32,
847 current: &mut Vec<u32>,
848 next: &mut Vec<u32>,
849) {
850 try_cpu_ref_closure_into(
851 node_count,
852 edge_offsets,
853 edge_targets,
854 edge_kind_mask,
855 seed,
856 allow_mask,
857 max_iters,
858 current,
859 next,
860 )
861 .unwrap_or_else(|err| {
862 panic!("csr_bidirectional closure CPU oracle received malformed input. {err}")
863 });
864}
865
866#[allow(clippy::too_many_arguments)]
868#[cfg(any(test, feature = "cpu-parity"))]
869pub fn try_cpu_ref_closure_into(
870 node_count: u32,
871 edge_offsets: &[u32],
872 edge_targets: &[u32],
873 edge_kind_mask: &[u32],
874 seed: &[u32],
875 allow_mask: u32,
876 max_iters: u32,
877 current: &mut Vec<u32>,
878 next: &mut Vec<u32>,
879) -> Result<(), String> {
880 try_cpu_ref_closure_into_with_step_hook(
881 node_count,
882 edge_offsets,
883 edge_targets,
884 edge_kind_mask,
885 seed,
886 allow_mask,
887 max_iters,
888 current,
889 next,
890 || {},
891 )
892}
893
894#[allow(clippy::too_many_arguments)]
899#[cfg(any(test, feature = "cpu-parity"))]
900pub fn cpu_ref_closure_into_with_step_hook<F>(
901 node_count: u32,
902 edge_offsets: &[u32],
903 edge_targets: &[u32],
904 edge_kind_mask: &[u32],
905 seed: &[u32],
906 allow_mask: u32,
907 max_iters: u32,
908 current: &mut Vec<u32>,
909 next: &mut Vec<u32>,
910 mut on_step: F,
911) where
912 F: FnMut(),
913{
914 try_cpu_ref_closure_into_with_step_hook(
915 node_count,
916 edge_offsets,
917 edge_targets,
918 edge_kind_mask,
919 seed,
920 allow_mask,
921 max_iters,
922 current,
923 next,
924 &mut on_step,
925 )
926 .unwrap_or_else(|err| {
927 panic!("csr_bidirectional closure CPU oracle received malformed input. {err}")
928 });
929}
930
931#[allow(clippy::too_many_arguments)]
933#[cfg(any(test, feature = "cpu-parity"))]
934pub fn try_cpu_ref_closure_into_with_step_hook<F>(
935 node_count: u32,
936 edge_offsets: &[u32],
937 edge_targets: &[u32],
938 edge_kind_mask: &[u32],
939 seed: &[u32],
940 allow_mask: u32,
941 max_iters: u32,
942 current: &mut Vec<u32>,
943 next: &mut Vec<u32>,
944 mut on_step: F,
945) -> Result<(), String>
946where
947 F: FnMut(),
948{
949 let plan = plan_csr_bidirectional_step(
950 node_count,
951 edge_offsets,
952 edge_targets,
953 edge_kind_mask,
954 seed,
955 allow_mask,
956 )?;
957 run_csr_bidirectional_closure_plan_with_step(
958 &plan,
959 seed,
960 max_iters,
961 current,
962 next,
963 |message| message,
964 |frontier, out| {
965 on_step();
966 cpu_ref_into_validated(
967 plan.layout,
968 edge_offsets,
969 edge_targets,
970 edge_kind_mask,
971 frontier,
972 allow_mask,
973 out,
974 )
975 },
976 )
977}
978
979#[must_use]
989pub fn merge_frontier_or_changed(current: &mut [u32], next: &[u32]) -> bool {
990 try_merge_frontier_or_changed(current, next).unwrap_or_else(|error| panic!("{error}"))
995}
996
997pub fn try_merge_frontier_or_changed(current: &mut [u32], next: &[u32]) -> Result<bool, String> {
999 if current.len() != next.len() {
1000 return Err(format!(
1001 "Fix: bidirectional frontier merge requires equal bitset word counts, got current={} next={}.",
1002 current.len(),
1003 next.len()
1004 ));
1005 }
1006 let mut changed = false;
1007 for (dst, src) in current.iter_mut().zip(next.iter()) {
1008 let merged = *dst | *src;
1009 changed |= merged != *dst;
1010 *dst = merged;
1011 }
1012 Ok(changed)
1013}
1014
1015fn csr_bidir_u32_to_usize(value: u32, label: &'static str) -> Result<usize, String> {
1016 usize::try_from(value).map_err(|source| {
1017 format!("Fix: csr_bidirectional {label} value {value} cannot fit host usize: {source}.")
1018 })
1019}
1020
1021#[cfg(test)]
1022mod tests {
1023 use super::*;
1024
1025 fn linear_graph() -> (Vec<u32>, Vec<u32>, Vec<u32>) {
1026 (vec![0, 1, 2, 3, 3], vec![1, 2, 3], vec![1, 1, 1])
1028 }
1029
1030 #[test]
1031 fn forward_step_propagates() {
1032 let (off, tgt, msk) = linear_graph();
1033 let out = cpu_ref(4, &off, &tgt, &msk, &[0b0001], 0xFFFF_FFFF);
1034 assert!(out[0] & 0b0010 != 0);
1036 }
1037
1038 #[test]
1039 fn empty_seed_yields_empty_step() {
1040 let (off, tgt, msk) = linear_graph();
1041 let out = cpu_ref(4, &off, &tgt, &msk, &[0], 0xFFFF_FFFF);
1042 assert_eq!(out, vec![0]);
1043 }
1044
1045 #[test]
1046 fn allow_mask_zero_blocks_all() {
1047 let (off, tgt, msk) = linear_graph();
1048 let out = cpu_ref(4, &off, &tgt, &msk, &[0b0001], 0);
1049 assert_eq!(out, vec![0]);
1050 }
1051
1052 #[test]
1053 fn bidirectional_includes_both_directions() {
1054 let (off, tgt, msk) = linear_graph();
1055 let out = cpu_ref(4, &off, &tgt, &msk, &[0b0010], 0xFFFF_FFFF);
1057 assert!(out[0] & 0b0001 != 0, "bwd should reach node 0");
1058 assert!(out[0] & 0b0100 != 0, "fwd should reach node 2");
1059 }
1060
1061 #[test]
1062 fn closure_reaches_full_linear_component() {
1063 let (off, tgt, msk) = linear_graph();
1064 let out = cpu_ref_closure(4, &off, &tgt, &msk, &[0b0001], 0xFFFF_FFFF, 5);
1065 assert_eq!(out, vec![0b1111]);
1066 }
1067
1068 #[test]
1069 fn closure_into_reuses_caller_buffers() {
1070 let (off, tgt, msk) = linear_graph();
1071 let mut current = Vec::with_capacity(8);
1072 let mut next = Vec::with_capacity(8);
1073 cpu_ref_closure_into(
1074 4,
1075 &off,
1076 &tgt,
1077 &msk,
1078 &[0b0001],
1079 0xFFFF_FFFF,
1080 5,
1081 &mut current,
1082 &mut next,
1083 );
1084 assert_eq!(current, vec![0b1111]);
1085 assert_eq!(current.capacity(), 8);
1086 assert_eq!(next.capacity(), 8);
1087 }
1088
1089 #[test]
1090 fn merge_frontier_reports_change_and_or_merges_words() {
1091 let mut current = [0b0001u32, 0b1000];
1092 let next = [0b0110u32, 0b1000];
1093 assert!(merge_frontier_or_changed(&mut current, &next));
1094 assert_eq!(current, [0b0111, 0b1000]);
1095 assert!(!merge_frontier_or_changed(&mut current, &next));
1096 }
1097
1098 #[test]
1099 fn try_merge_frontier_rejects_mismatched_word_counts_without_panic() {
1100 let mut current = [0u32];
1101 let next = [1u32, 2];
1102 let err = try_merge_frontier_or_changed(&mut current, &next)
1103 .expect_err("mismatched frontier word counts must be a typed error");
1104 assert!(err.contains("equal bitset word counts"));
1105 assert_eq!(current, [0u32]);
1106 }
1107
1108 #[test]
1109 #[should_panic(
1110 expected = "Fix: bidirectional frontier merge requires equal bitset word counts"
1111 )]
1112 fn merge_frontier_rejects_mismatched_word_counts() {
1113 let mut current = [0u32];
1114 let next = [1u32, 2];
1115 let _ = merge_frontier_or_changed(&mut current, &next);
1116 }
1117
1118 #[test]
1119 fn validate_csr_inputs_accepts_empty_and_canonical_graphs() {
1120 assert_eq!(
1121 validate_csr_inputs(0, &[0], &[], &[], &[]).unwrap(),
1122 CsrBidirectionalLayout {
1123 node_count: 0,
1124 words: 0,
1125 node_words: 0,
1126 edge_count: 0,
1127 edge_storage_words: 1,
1128 }
1129 );
1130
1131 let (off, tgt, msk) = linear_graph();
1132 assert_eq!(
1133 validate_csr_inputs(4, &off, &tgt, &msk, &[0]).unwrap(),
1134 CsrBidirectionalLayout {
1135 node_count: 4,
1136 words: 1,
1137 node_words: 4,
1138 edge_count: 3,
1139 edge_storage_words: 3,
1140 }
1141 );
1142 }
1143
1144 #[test]
1145 fn validate_csr_inputs_rejects_frontier_and_csr_contract_violations() {
1146 let err = validate_csr_inputs(2, &[0, 1, 1], &[1], &[1], &[]).unwrap_err();
1147 assert!(err.contains("expected frontier length"));
1148
1149 let err = validate_csr_inputs(2, &[0, 1, 1], &[1], &[], &[0]).unwrap_err();
1150 assert!(err.contains("edge_targets.len() == edge_kind_mask.len()"));
1151
1152 let err = validate_csr_inputs(2, &[0, 2, 1], &[1], &[1], &[0]).unwrap_err();
1153 assert!(err.contains("offsets must be monotonic"));
1154
1155 let err = validate_csr_inputs(2, &[0, 1, 1], &[5], &[1], &[0]).unwrap_err();
1156 assert!(err.contains("outside node_count"));
1157 }
1158
1159 #[test]
1160 fn try_cpu_ref_into_rejects_bad_csr_without_clobbering_output() {
1161 let mut out = vec![0xCAFE_BABEu32];
1162 let capacity = out.capacity();
1163 let err = try_cpu_ref_into(2, &[0, 1, 1], &[1], &[], &[0], u32::MAX, &mut out)
1164 .expect_err("mismatched edge arrays must return an error");
1165 assert!(err.contains("edge_targets.len() == edge_kind_mask.len()"));
1166 assert_eq!(out, vec![0xCAFE_BABEu32]);
1167 assert_eq!(out.capacity(), capacity);
1168 }
1169
1170 #[test]
1171 fn try_cpu_ref_closure_rejects_bad_seed_without_clobbering_buffers() {
1172 let (off, tgt, msk) = linear_graph();
1173 let mut current = vec![0xCAFE_BABEu32];
1174 let mut next = vec![0xDEAD_BEEFu32];
1175 let current_capacity = current.capacity();
1176 let next_capacity = next.capacity();
1177 let err = try_cpu_ref_closure_into(
1178 4,
1179 &off,
1180 &tgt,
1181 &msk,
1182 &[],
1183 u32::MAX,
1184 4,
1185 &mut current,
1186 &mut next,
1187 )
1188 .expect_err("bad seed width must be rejected");
1189 assert!(err.contains("expected frontier length"));
1190 assert_eq!(current, vec![0xCAFE_BABEu32]);
1191 assert_eq!(next, vec![0xDEAD_BEEFu32]);
1192 assert_eq!(current.capacity(), current_capacity);
1193 assert_eq!(next.capacity(), next_capacity);
1194 }
1195
1196 #[test]
1197 fn fallible_cpu_reference_matches_compatibility_wrappers() {
1198 let (off, tgt, msk) = linear_graph();
1199 let step = try_cpu_ref(4, &off, &tgt, &msk, &[0b0010], u32::MAX)
1200 .expect("Fix: operation must return Err on failure; tests may use expect only with Fix: recovery text - valid step should succeed");
1201 assert_eq!(step, cpu_ref(4, &off, &tgt, &msk, &[0b0010], u32::MAX));
1202
1203 let closure = try_cpu_ref_closure(4, &off, &tgt, &msk, &[0b0001], u32::MAX, 5)
1204 .expect("Fix: operation must return Err on failure; tests may use expect only with Fix: recovery text - valid closure should succeed");
1205 assert_eq!(
1206 closure,
1207 cpu_ref_closure(4, &off, &tgt, &msk, &[0b0001], u32::MAX, 5)
1208 );
1209 }
1210
1211 #[test]
1212 fn cpu_ref_into_validates_before_resizing_output() {
1213 let mut out = vec![0xCAFE_BABEu32];
1214 let original_capacity = out.capacity();
1215
1216 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1217 cpu_ref_into(u32::MAX, &[0], &[], &[], &[], u32::MAX, &mut out);
1218 }));
1219
1220 assert!(result.is_err(), "malformed CSR must still be rejected");
1221 assert_eq!(
1222 out,
1223 vec![0xCAFE_BABEu32],
1224 "invalid input must not clear or resize caller output before validation"
1225 );
1226 assert_eq!(
1227 out.capacity(),
1228 original_capacity,
1229 "invalid input must not allocate based on hostile node_count"
1230 );
1231 }
1232}