1use core::fmt;
4use std::sync::Arc;
5
6use crate::bitset::bitset_words;
7use vyre_foundation::ir::model::expr::Ident;
8use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program, UnOp};
9
10pub const ABSORB_NEW_BITS_OP_ID: &str = "vyre-primitives::bitset::frontier_absorb_new_bits";
12pub const ABSORB_NEW_BITS_NO_COUNTS_OP_ID: &str =
15 "vyre-primitives::bitset::frontier_absorb_new_bits_no_counts";
16
17#[derive(Clone, Debug, Eq, PartialEq)]
19#[non_exhaustive]
20pub enum FrontierError {
21 BadShape {
23 name: &'static str,
25 node_count: u32,
27 expected_words: usize,
29 actual_words: usize,
31 },
32 PopcountOverflow {
34 frontier_words: usize,
36 },
37 Allocation {
39 name: &'static str,
41 requested_words: usize,
43 source: String,
45 },
46 QueueCapacity {
48 node_count: u32,
50 capacity: usize,
52 required: u32,
54 },
55 QueueCountMismatch {
57 node_count: u32,
59 expected: u32,
61 observed: u32,
63 },
64}
65
66impl fmt::Display for FrontierError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 match self {
69 Self::BadShape {
70 name,
71 node_count,
72 expected_words,
73 actual_words,
74 } => write!(
75 f,
76 "{name} frontier for {node_count} nodes requires {expected_words} u32 words, got {actual_words}."
77 ),
78 Self::PopcountOverflow { frontier_words } => write!(
79 f,
80 "frontier popcount exceeds u32::MAX for {frontier_words} frontier words."
81 ),
82 Self::Allocation {
83 name,
84 requested_words,
85 source,
86 } => write!(
87 f,
88 "{name} frontier could not reserve {requested_words} u32 words: {source}."
89 ),
90 Self::QueueCapacity {
91 node_count,
92 capacity,
93 required,
94 } => write!(
95 f,
96 "frontier queue for {node_count} nodes requires {required} slots, got capacity {capacity}."
97 ),
98 Self::QueueCountMismatch {
99 node_count,
100 expected,
101 observed,
102 } => write!(
103 f,
104 "frontier queue for {node_count} nodes expected {expected} active bits, observed {observed}."
105 ),
106 }
107 }
108}
109
110impl std::error::Error for FrontierError {}
111
112#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
114pub struct FrontierAbsorbSummary {
115 pub added_any: bool,
117 pub added_popcount: u32,
119}
120
121#[must_use]
123pub const fn frontier_words(node_count: u32) -> usize {
124 bitset_words(node_count) as usize
125}
126
127#[must_use]
129pub const fn frontier_tail_mask(node_count: u32) -> u32 {
130 let tail_bits = node_count % u32::BITS;
131 if tail_bits == 0 {
132 u32::MAX
133 } else {
134 (1u32 << tail_bits) - 1
135 }
136}
137
138#[must_use]
140pub fn frontier_absorb_new_bits_program(
141 visited: &str,
142 neighbors: &str,
143 next_wave: &str,
144 added_counts: &str,
145 words: u32,
146 final_word_mask: u32,
147) -> Program {
148 let body = frontier_absorb_new_bits_body_prefixed(
149 visited,
150 neighbors,
151 next_wave,
152 Some(added_counts),
153 words,
154 final_word_mask,
155 "frontier_absorb",
156 );
157
158 Program::wrapped(
159 vec![
160 BufferDecl::storage(visited, 0, BufferAccess::ReadWrite, DataType::U32)
161 .with_count(words),
162 BufferDecl::storage(neighbors, 1, BufferAccess::ReadOnly, DataType::U32)
163 .with_count(words),
164 BufferDecl::storage(next_wave, 2, BufferAccess::ReadWrite, DataType::U32)
165 .with_count(words),
166 BufferDecl::storage(added_counts, 3, BufferAccess::ReadWrite, DataType::U32)
167 .with_count(words),
168 ],
169 [256, 1, 1],
170 vec![Node::Region {
171 generator: Ident::from(ABSORB_NEW_BITS_OP_ID),
172 source_region: None,
173 body: Arc::new(body),
174 }],
175 )
176}
177
178#[must_use]
184pub(crate) fn frontier_absorb_new_bits_body_prefixed(
185 visited: &str,
186 neighbors: &str,
187 next_wave: &str,
188 added_counts: Option<&str>,
189 words: u32,
190 final_word_mask: u32,
191 local_prefix: &str,
192) -> Vec<Node> {
193 frontier_absorb_new_bits_body_prefixed_with_flag(
194 visited,
195 neighbors,
196 next_wave,
197 added_counts,
198 None,
199 words,
200 final_word_mask,
201 local_prefix,
202 )
203}
204
205#[must_use]
208pub(crate) fn frontier_absorb_new_bits_body_prefixed_with_flag(
209 visited: &str,
210 neighbors: &str,
211 next_wave: &str,
212 added_counts: Option<&str>,
213 added_any_flag: Option<(&str, Expr)>,
214 words: u32,
215 final_word_mask: u32,
216 local_prefix: &str,
217) -> Vec<Node> {
218 let local = |name: &str| -> String {
219 if local_prefix.is_empty() {
220 name.to_string()
221 } else {
222 format!("{local_prefix}_{name}")
223 }
224 };
225 let t = Expr::InvocationId { axis: 0 };
226 let final_word = words.saturating_sub(1);
227 let old_visited = local("old_visited");
228 let neighbor_word = local("neighbors");
229 let domain_mask = local("domain_mask");
230 let in_domain_neighbors = local("in_domain_neighbors");
231 let new_bits = local("new_bits");
232 let changed_old = local("changed_old");
233
234 let mut body = vec![
235 Node::let_bind(old_visited.as_str(), Expr::load(visited, t.clone())),
236 Node::let_bind(neighbor_word.as_str(), Expr::load(neighbors, t.clone())),
237 Node::let_bind(
238 domain_mask.as_str(),
239 Expr::select(
240 Expr::eq(t.clone(), Expr::u32(final_word)),
241 Expr::u32(final_word_mask),
242 Expr::u32(u32::MAX),
243 ),
244 ),
245 Node::let_bind(
246 in_domain_neighbors.as_str(),
247 Expr::bitand(
248 Expr::var(neighbor_word.as_str()),
249 Expr::var(domain_mask.as_str()),
250 ),
251 ),
252 Node::let_bind(
253 new_bits.as_str(),
254 Expr::bitand(
255 Expr::var(in_domain_neighbors.as_str()),
256 Expr::bitnot(Expr::var(old_visited.as_str())),
257 ),
258 ),
259 Node::store(next_wave, t.clone(), Expr::var(new_bits.as_str())),
260 Node::store(
261 visited,
262 t.clone(),
263 Expr::bitor(
264 Expr::var(old_visited.as_str()),
265 Expr::var(new_bits.as_str()),
266 ),
267 ),
268 ];
269 if let Some(added_counts) = added_counts {
270 body.push(Node::store(
271 added_counts,
272 t.clone(),
273 Expr::UnOp {
274 op: UnOp::Popcount,
275 operand: Box::new(Expr::var(new_bits.as_str())),
276 },
277 ));
278 }
279 if let Some((flag_buffer, flag_index)) = added_any_flag {
280 body.push(Node::if_then(
281 Expr::ne(Expr::var(new_bits.as_str()), Expr::u32(0)),
282 vec![Node::let_bind(
283 changed_old.as_str(),
284 Expr::atomic_or(flag_buffer, flag_index, Expr::u32(1)),
285 )],
286 ));
287 }
288
289 vec![Node::if_then(Expr::lt(t, Expr::u32(words)), body)]
290}
291
292#[must_use]
295pub fn frontier_absorb_new_bits_no_counts_program(
296 visited: &str,
297 neighbors: &str,
298 next_wave: &str,
299 words: u32,
300 final_word_mask: u32,
301) -> Program {
302 Program::wrapped(
303 vec![
304 BufferDecl::storage(visited, 0, BufferAccess::ReadWrite, DataType::U32)
305 .with_count(words),
306 BufferDecl::storage(neighbors, 1, BufferAccess::ReadOnly, DataType::U32)
307 .with_count(words),
308 BufferDecl::storage(next_wave, 2, BufferAccess::ReadWrite, DataType::U32)
309 .with_count(words),
310 ],
311 [256, 1, 1],
312 vec![Node::Region {
313 generator: Ident::from(ABSORB_NEW_BITS_NO_COUNTS_OP_ID),
314 source_region: None,
315 body: Arc::new(frontier_absorb_new_bits_body_prefixed(
316 visited,
317 neighbors,
318 next_wave,
319 None,
320 words,
321 final_word_mask,
322 "frontier_absorb_no_counts",
323 )),
324 }],
325 )
326}
327
328#[must_use]
330pub fn frontier_absorb_new_bits_for_node_count_program(
331 visited: &str,
332 neighbors: &str,
333 next_wave: &str,
334 added_counts: &str,
335 node_count: u32,
336) -> Program {
337 frontier_absorb_new_bits_program(
338 visited,
339 neighbors,
340 next_wave,
341 added_counts,
342 bitset_words(node_count),
343 frontier_tail_mask(node_count),
344 )
345}
346
347#[must_use]
350pub fn frontier_absorb_new_bits_no_counts_for_node_count_program(
351 visited: &str,
352 neighbors: &str,
353 next_wave: &str,
354 node_count: u32,
355) -> Program {
356 frontier_absorb_new_bits_no_counts_program(
357 visited,
358 neighbors,
359 next_wave,
360 bitset_words(node_count),
361 frontier_tail_mask(node_count),
362 )
363}
364
365pub fn validate_frontier_shape(
367 node_count: u32,
368 frontier: &[u32],
369 name: &'static str,
370) -> Result<usize, FrontierError> {
371 let expected_words = frontier_words(node_count);
372 if frontier.len() != expected_words {
373 return Err(FrontierError::BadShape {
374 name,
375 node_count,
376 expected_words,
377 actual_words: frontier.len(),
378 });
379 }
380 Ok(expected_words)
381}
382
383pub fn checked_frontier_popcount(frontier: &[u32]) -> Result<u32, FrontierError> {
385 let mut popcount = 0u32;
386 for &word in frontier {
387 popcount =
388 popcount
389 .checked_add(word.count_ones())
390 .ok_or(FrontierError::PopcountOverflow {
391 frontier_words: frontier.len(),
392 })?;
393 }
394 Ok(popcount)
395}
396
397pub fn checked_frontier_domain_popcount(
399 node_count: u32,
400 frontier: &[u32],
401) -> Result<u32, FrontierError> {
402 let expected_words = validate_frontier_shape(node_count, frontier, "input")?;
403 let final_word_index = expected_words.saturating_sub(1);
404 let final_word_mask = frontier_tail_mask(node_count);
405 let mut popcount = 0u32;
406 for (word_index, &word) in frontier.iter().enumerate() {
407 let in_domain_word = if word_index == final_word_index {
408 word & final_word_mask
409 } else {
410 word
411 };
412 popcount = popcount.checked_add(in_domain_word.count_ones()).ok_or(
413 FrontierError::PopcountOverflow {
414 frontier_words: expected_words,
415 },
416 )?;
417 }
418 Ok(popcount)
419}
420
421pub fn materialize_frontier_queue_into(
427 node_count: u32,
428 frontier: &[u32],
429 queue_capacity: usize,
430 queue: &mut Vec<u32>,
431) -> Result<u32, FrontierError> {
432 let required = checked_frontier_domain_popcount(node_count, frontier)?;
433 materialize_frontier_queue_exact_count_into(
434 node_count,
435 frontier,
436 required,
437 queue_capacity,
438 queue,
439 )
440}
441
442#[cfg(any(test, feature = "cpu-parity"))]
449pub(crate) fn materialize_frontier_queue_prefix_into(
450 node_count: u32,
451 frontier: &[u32],
452 queue_capacity: usize,
453 queue: &mut Vec<u32>,
454) -> Result<u32, FrontierError> {
455 let expected_words = validate_frontier_shape(node_count, frontier, "input")?;
456 let reserve_words = queue_capacity.min(node_count as usize);
457 vyre_foundation::allocation::reserve_exact_cleared(queue, reserve_words).map_err(|source| {
458 FrontierError::Allocation {
459 name: "frontier_queue",
460 requested_words: reserve_words,
461 source: source.to_string(),
462 }
463 })?;
464 let final_word_index = expected_words.saturating_sub(1);
465 let final_word_mask = frontier_tail_mask(node_count);
466 let mut observed = 0u32;
467 for (word_index, &word) in frontier.iter().enumerate() {
468 let mut bits = if word_index == final_word_index {
469 word & final_word_mask
470 } else {
471 word
472 };
473 while bits != 0 {
474 let bit = bits.trailing_zeros();
475 if queue.len() < queue_capacity {
476 queue.push((word_index as u32 * u32::BITS) + bit);
477 }
478 observed = observed
479 .checked_add(1)
480 .ok_or(FrontierError::PopcountOverflow {
481 frontier_words: expected_words,
482 })?;
483 bits &= bits - 1;
484 }
485 }
486 Ok(observed)
487}
488
489pub fn materialize_frontier_queue_exact_count_into(
496 node_count: u32,
497 frontier: &[u32],
498 active_count: u32,
499 queue_capacity: usize,
500 queue: &mut Vec<u32>,
501) -> Result<u32, FrontierError> {
502 let expected_words = validate_frontier_shape(node_count, frontier, "input")?;
503 if active_count as usize > queue_capacity {
504 return Err(FrontierError::QueueCapacity {
505 node_count,
506 capacity: queue_capacity,
507 required: active_count,
508 });
509 }
510 let required_usize = active_count as usize;
511 vyre_foundation::allocation::reserve_exact_cleared(queue, required_usize).map_err(|source| {
512 FrontierError::Allocation {
513 name: "frontier_queue",
514 requested_words: required_usize,
515 source: source.to_string(),
516 }
517 })?;
518 let final_word_index = expected_words.saturating_sub(1);
519 let final_word_mask = frontier_tail_mask(node_count);
520 let mut observed = 0u32;
521 for (word_index, &word) in frontier.iter().enumerate() {
522 let mut bits = if word_index == final_word_index {
523 word & final_word_mask
524 } else {
525 word
526 };
527 while bits != 0 {
528 observed = observed
529 .checked_add(1)
530 .ok_or(FrontierError::PopcountOverflow {
531 frontier_words: expected_words,
532 })?;
533 if observed > active_count {
534 return Err(FrontierError::QueueCountMismatch {
535 node_count,
536 expected: active_count,
537 observed,
538 });
539 }
540 let bit = bits.trailing_zeros();
541 queue.push((word_index as u32 * u32::BITS) + bit);
542 bits &= bits - 1;
543 }
544 }
545 if observed != active_count {
546 return Err(FrontierError::QueueCountMismatch {
547 node_count,
548 expected: active_count,
549 observed,
550 });
551 }
552 Ok(observed)
553}
554
555pub fn mask_frontier_tail_bits(node_count: u32, frontier: &mut [u32]) {
557 if let Some(last_word) = frontier.last_mut() {
558 *last_word &= frontier_tail_mask(node_count);
559 }
560}
561
562pub fn absorb_new_frontier_bits(
564 node_count: u32,
565 visited: &mut [u32],
566 neighbors: &[u32],
567 next_wave: &mut Vec<u32>,
568) -> Result<FrontierAbsorbSummary, FrontierError> {
569 let expected_words = validate_frontier_shape(node_count, visited, "visited")?;
570 validate_frontier_shape(node_count, neighbors, "neighbors")?;
571 vyre_foundation::allocation::reserve_exact_cleared(next_wave, expected_words).map_err(|source| {
572 FrontierError::Allocation {
573 name: "next_wave",
574 requested_words: expected_words,
575 source: source.to_string(),
576 }
577 })?;
578 next_wave.resize(expected_words, 0);
579 let mut summary = FrontierAbsorbSummary::default();
580 let last_word_index = expected_words.saturating_sub(1);
581 let tail_mask = frontier_tail_mask(node_count);
582 for (word_index, (visited_word, neighbor_word)) in visited
583 .iter_mut()
584 .zip(neighbors.iter().copied())
585 .enumerate()
586 {
587 let in_domain_neighbors = if word_index == last_word_index {
588 neighbor_word & tail_mask
589 } else {
590 neighbor_word
591 };
592 let new_bits = in_domain_neighbors & !*visited_word;
593 next_wave[word_index] = new_bits;
594 *visited_word |= new_bits;
595 summary.added_any |= new_bits != 0;
596 summary.added_popcount = summary
597 .added_popcount
598 .checked_add(new_bits.count_ones())
599 .ok_or(FrontierError::PopcountOverflow {
600 frontier_words: expected_words,
601 })?;
602 }
603 Ok(summary)
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[test]
611 fn absorb_masks_tail_and_reports_added_popcount() {
612 let mut visited = vec![0b0001, 0b0001];
613 let neighbors = vec![0b0111, 0b1000_0111];
614 let mut next_wave = Vec::new();
615 let summary = absorb_new_frontier_bits(35, &mut visited, &neighbors, &mut next_wave)
616 .expect("Fix: valid frontier");
617 assert_eq!(summary.added_popcount, 4);
618 assert_eq!(next_wave, vec![0b0110, 0b0110]);
619 assert_eq!(visited, vec![0b0111, 0b0111]);
620 }
621
622 #[test]
623 fn no_count_absorb_program_keeps_only_frontier_outputs() {
624 let program = frontier_absorb_new_bits_no_counts_for_node_count_program(
625 "visited",
626 "neighbors",
627 "next",
628 65,
629 );
630 let names = program
631 .buffers()
632 .iter()
633 .map(|buffer| buffer.name())
634 .collect::<Vec<_>>();
635
636 assert_eq!(program.workgroup_size(), [256, 1, 1]);
637 assert_eq!(names, vec!["visited", "neighbors", "next"]);
638 assert_eq!(program.buffers()[0].count, 3);
639 assert_eq!(program.buffers()[1].count, 3);
640 assert_eq!(program.buffers()[2].count, 3);
641 assert!(!names.contains(&"added_counts"));
642 }
643
644 #[test]
645 fn frontier_queue_materializes_set_bits_in_order_and_masks_tail() {
646 let frontier = [0b1010_u32, u32::MAX, u32::MAX];
647 let mut queue = Vec::new();
648
649 let len = materialize_frontier_queue_into(65, &frontier, 100, &mut queue)
650 .expect("Fix: frontier queue should fit");
651
652 assert_eq!(len, 35);
653 assert_eq!(queue[0..4], [1, 3, 32, 33]);
654 assert_eq!(*queue.last().unwrap(), 64);
655 assert!(
656 queue.iter().all(|node| *node < 65),
657 "tail bits outside node_count must not enter the frontier queue"
658 );
659 }
660
661 #[test]
662 fn frontier_queue_rejects_under_capacity_without_mutating_output() {
663 let frontier = [0b1111_u32];
664 let mut queue = vec![99, 100];
665
666 let err = materialize_frontier_queue_into(4, &frontier, 3, &mut queue)
667 .expect_err("under-capacity queue must fail");
668
669 assert!(matches!(
670 err,
671 FrontierError::QueueCapacity {
672 node_count: 4,
673 capacity: 3,
674 required: 4,
675 }
676 ));
677 assert_eq!(queue, vec![99, 100]);
678 }
679
680 #[test]
681 fn exact_count_frontier_queue_materializes_ordered_tail_masked_bits() {
682 let frontier = [0b1010_u32, u32::MAX, u32::MAX];
683 let mut queue = Vec::new();
684
685 let len = materialize_frontier_queue_exact_count_into(65, &frontier, 35, 35, &mut queue)
686 .expect("Fix: exact-count frontier queue should fit");
687
688 assert_eq!(len, 35);
689 assert_eq!(queue[0..4], [1, 3, 32, 33]);
690 assert_eq!(*queue.last().unwrap(), 64);
691 assert_eq!(queue.len(), 35);
692 }
693
694 #[test]
695 fn exact_count_frontier_queue_rejects_stale_low_or_high_counts() {
696 let frontier = [0b1111_u32];
697 let mut queue = Vec::new();
698
699 let low = materialize_frontier_queue_exact_count_into(4, &frontier, 3, 4, &mut queue)
700 .expect_err("stale low count must fail");
701 assert!(matches!(
702 low,
703 FrontierError::QueueCountMismatch {
704 node_count: 4,
705 expected: 3,
706 observed: 4,
707 }
708 ));
709
710 let high = materialize_frontier_queue_exact_count_into(4, &frontier, 5, 5, &mut queue)
711 .expect_err("stale high count must fail");
712 assert!(matches!(
713 high,
714 FrontierError::QueueCountMismatch {
715 node_count: 4,
716 expected: 5,
717 observed: 4,
718 }
719 ));
720 }
721
722 #[test]
723 fn prefix_frontier_queue_clamps_capacity_and_returns_full_count() {
724 let frontier = [0b1010_u32, u32::MAX, u32::MAX];
725 let mut queue = Vec::new();
726
727 let len = materialize_frontier_queue_prefix_into(65, &frontier, 4, &mut queue)
728 .expect("Fix: prefix frontier queue should materialize");
729
730 assert_eq!(len, 35);
731 assert_eq!(queue, vec![1, 3, 32, 33]);
732 assert!(
733 queue.iter().all(|node| *node < 65),
734 "tail bits outside node_count must not enter the prefix queue"
735 );
736 }
737
738 #[test]
739 fn prefix_frontier_queue_zero_capacity_still_counts_active_bits() {
740 let frontier = [u32::MAX, u32::MAX];
741 let mut queue = vec![99, 100];
742
743 let len = materialize_frontier_queue_prefix_into(33, &frontier, 0, &mut queue)
744 .expect("Fix: zero-capacity prefix queue should still report pressure");
745
746 assert_eq!(len, 33);
747 assert!(queue.is_empty());
748 }
749
750 #[test]
751 fn prefix_frontier_queue_rejects_bad_shape_without_mutating_output() {
752 let frontier = [0b1010_u32];
753 let mut queue = vec![99, 100];
754
755 let err = materialize_frontier_queue_prefix_into(64, &frontier, 8, &mut queue)
756 .expect_err("bad prefix frontier shape must fail");
757
758 assert!(matches!(err, FrontierError::BadShape { name: "input", .. }));
759 assert_eq!(queue, vec![99, 100]);
760 }
761
762 #[test]
763 fn generated_frontier_queue_matches_scalar_scan_across_10000_shapes() {
764 for seed in 0..10_000_u32 {
765 let node_count = 1 + (mix32(seed) % 8_192);
766 let words = frontier_words(node_count);
767 let mut frontier = (0..words)
768 .map(|word| mix32(seed ^ (word as u32).wrapping_mul(0x9E37_79B9)))
769 .collect::<Vec<_>>();
770 if seed & 7 == 0 {
771 frontier.fill(0);
772 let node = mix32(seed ^ 0x5150_ACE5) % node_count;
773 frontier[(node / 32) as usize] |= 1_u32 << (node % 32);
774 }
775 let expected = scalar_frontier_queue(node_count, &frontier);
776 let mut queue = Vec::new();
777
778 let len =
779 materialize_frontier_queue_into(node_count, &frontier, expected.len(), &mut queue)
780 .expect("Fix: generated frontier queue should fit exactly");
781
782 assert_eq!(len as usize, expected.len(), "seed={seed}");
783 assert_eq!(queue, expected, "seed={seed} node_count={node_count}");
784 }
785 }
786
787 #[test]
788 fn generated_exact_count_frontier_queue_matches_scalar_scan_across_10000_shapes() {
789 for seed in 0..10_000_u32 {
790 let node_count = 1 + (mix32(seed ^ 0xECA7_C011) % 8_192);
791 let words = frontier_words(node_count);
792 let mut frontier = (0..words)
793 .map(|word| mix32(seed ^ (word as u32).wrapping_mul(0x85EB_CA6B)))
794 .collect::<Vec<_>>();
795 if seed & 15 == 0 {
796 frontier.fill(0);
797 let node = mix32(seed ^ 0xD47A_F10D) % node_count;
798 frontier[(node / 32) as usize] |= 1_u32 << (node % 32);
799 }
800 let expected = scalar_frontier_queue(node_count, &frontier);
801 let mut queue = Vec::new();
802
803 let len = materialize_frontier_queue_exact_count_into(
804 node_count,
805 &frontier,
806 expected.len() as u32,
807 expected.len(),
808 &mut queue,
809 )
810 .expect("Fix: generated exact-count frontier queue should fit exactly");
811
812 assert_eq!(len as usize, expected.len(), "seed={seed}");
813 assert_eq!(queue, expected, "seed={seed} node_count={node_count}");
814 }
815 }
816
817 #[test]
818 fn generated_prefix_frontier_queue_matches_scalar_scan_across_10000_shapes() {
819 for seed in 0..10_000_u32 {
820 let node_count = 1 + (mix32(seed ^ 0xB17C_0DE5) % 8_192);
821 let words = frontier_words(node_count);
822 let mut frontier = (0..words)
823 .map(|word| mix32(seed ^ (word as u32).wrapping_mul(0x27D4_EB2D)))
824 .collect::<Vec<_>>();
825 if seed & 31 == 0 {
826 frontier.fill(0);
827 let node = mix32(seed ^ 0xA11C_EED5) % node_count;
828 frontier[(node / 32) as usize] |= 1_u32 << (node % 32);
829 }
830 let expected = scalar_frontier_queue(node_count, &frontier);
831 let capacity = (mix32(seed ^ 0xCAFE_BA5E) as usize) % (expected.len() + 17);
832 let mut queue = Vec::new();
833
834 let len =
835 materialize_frontier_queue_prefix_into(node_count, &frontier, capacity, &mut queue)
836 .expect("Fix: generated prefix frontier queue should materialize");
837
838 assert_eq!(len as usize, expected.len(), "seed={seed}");
839 assert_eq!(
840 queue,
841 expected.iter().copied().take(capacity).collect::<Vec<_>>(),
842 "seed={seed} node_count={node_count} capacity={capacity}"
843 );
844 }
845 }
846
847 #[test]
848 fn absorb_reuses_next_wave_and_clears_stale_tail() {
849 let mut visited = vec![0b0001, 0b0001];
850 let neighbors = vec![0b0111, 0b1000_0111];
851 let mut next_wave = Vec::with_capacity(8);
852 next_wave.extend_from_slice(&[u32::MAX; 8]);
853 let ptr = next_wave.as_ptr();
854
855 let summary = absorb_new_frontier_bits(35, &mut visited, &neighbors, &mut next_wave)
856 .expect("Fix: valid frontier");
857
858 assert_eq!(summary.added_popcount, 4);
859 assert_eq!(next_wave, vec![0b0110, 0b0110]);
860 assert_eq!(next_wave.as_ptr(), ptr);
861 }
862
863 #[test]
864 fn absorb_rejects_bad_shape_without_mutating_buffers() {
865 let mut visited = vec![0b0001, 0b0010];
866 let before_visited = visited.clone();
867 let neighbors = vec![0b0111];
868 let mut next_wave = vec![0xDEAD_BEEF];
869
870 let err = absorb_new_frontier_bits(35, &mut visited, &neighbors, &mut next_wave)
871 .expect_err("bad neighbor shape must fail before mutation");
872
873 assert!(matches!(
874 err,
875 FrontierError::BadShape {
876 name: "neighbors",
877 ..
878 }
879 ));
880 assert_eq!(visited, before_visited);
881 assert_eq!(next_wave, vec![0xDEAD_BEEF]);
882 }
883
884 #[test]
885 fn generated_absorb_matches_scalar_reference() {
886 let patterns = [0, u32::MAX, 0x5555_5555, 0xAAAA_AAAA, 0x1357_9BDF];
887 for node_count in 0..=512 {
888 let words = frontier_words(node_count);
889 for (case_index, pattern) in patterns.into_iter().enumerate() {
890 let mut visited = (0..words)
891 .map(|word| pattern.rotate_left((word as u32 + case_index as u32) % 32))
892 .collect::<Vec<_>>();
893 let neighbors = (0..words)
894 .map(|word| (!pattern).rotate_right((word as u32 * 7) % 32))
895 .collect::<Vec<_>>();
896 mask_frontier_tail_bits(node_count, &mut visited);
897 let before = visited.clone();
898 let mut next_wave = Vec::new();
899 let summary =
900 absorb_new_frontier_bits(node_count, &mut visited, &neighbors, &mut next_wave)
901 .expect("Fix: generated shapes are valid");
902 let tail_index = words.saturating_sub(1);
903 let tail_mask = frontier_tail_mask(node_count);
904 let expected_next = before
905 .iter()
906 .zip(neighbors.iter())
907 .enumerate()
908 .map(|(idx, (&old, &neighbor))| {
909 let in_domain = if idx == tail_index {
910 neighbor & tail_mask
911 } else {
912 neighbor
913 };
914 in_domain & !old
915 })
916 .collect::<Vec<_>>();
917 assert_eq!(next_wave, expected_next, "node_count={node_count}");
918 assert_eq!(
919 summary.added_popcount,
920 expected_next
921 .iter()
922 .map(|word| word.count_ones())
923 .sum::<u32>()
924 );
925 }
926 }
927 }
928
929 fn scalar_frontier_queue(node_count: u32, frontier: &[u32]) -> Vec<u32> {
930 (0..node_count)
931 .filter(|node| {
932 let word = (*node / 32) as usize;
933 let bit = 1_u32 << (*node % 32);
934 frontier[word] & bit != 0
935 })
936 .collect()
937 }
938
939 fn mix32(mut value: u32) -> u32 {
940 value ^= value >> 16;
941 value = value.wrapping_mul(0x7FEB_352D);
942 value ^= value >> 15;
943 value = value.wrapping_mul(0x846C_A68B);
944 value ^ (value >> 16)
945 }
946}