Skip to main content

vyre_primitives/bitset/
frontier.rs

1//! Packed-frontier bitset utilities and fused frontier absorption.
2
3use 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
10/// Canonical op id for fused frontier absorption.
11pub const ABSORB_NEW_BITS_OP_ID: &str = "vyre-primitives::bitset::frontier_absorb_new_bits";
12/// Canonical op id for fused frontier absorption when the caller does not need
13/// per-word popcounts.
14pub const ABSORB_NEW_BITS_NO_COUNTS_OP_ID: &str =
15    "vyre-primitives::bitset::frontier_absorb_new_bits_no_counts";
16
17/// Error returned by packed-frontier helpers.
18#[derive(Clone, Debug, Eq, PartialEq)]
19#[non_exhaustive]
20pub enum FrontierError {
21    /// A frontier slice length does not match `bitset_words(node_count)`.
22    BadShape {
23        /// Human-readable slice role.
24        name: &'static str,
25        /// Declared graph/node domain width.
26        node_count: u32,
27        /// Expected number of u32 words.
28        expected_words: usize,
29        /// Actual number of u32 words.
30        actual_words: usize,
31    },
32    /// A popcount exceeded the compact u32 count representation.
33    PopcountOverflow {
34        /// Number of frontier words counted before overflow.
35        frontier_words: usize,
36    },
37    /// Caller-owned output frontier could not reserve enough storage.
38    Allocation {
39        /// Human-readable output buffer role.
40        name: &'static str,
41        /// Requested u32 words.
42        requested_words: usize,
43        /// Allocator detail.
44        source: String,
45    },
46    /// A compact frontier queue cannot hold all active in-domain bits.
47    QueueCapacity {
48        /// Declared graph/node domain width.
49        node_count: u32,
50        /// Queue slots available to the caller.
51        capacity: usize,
52        /// In-domain active bits that must be materialized.
53        required: u32,
54    },
55    /// Caller-provided active-bit count disagrees with the frontier bits.
56    QueueCountMismatch {
57        /// Declared graph/node domain width.
58        node_count: u32,
59        /// Active in-domain bits promised by the caller.
60        expected: u32,
61        /// Active in-domain bits observed before mismatch was proven.
62        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/// Summary from absorbing a neighbor frontier into a visited frontier.
113#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
114pub struct FrontierAbsorbSummary {
115    /// True when at least one previously-unvisited node was added.
116    pub added_any: bool,
117    /// Number of newly-added in-domain nodes.
118    pub added_popcount: u32,
119}
120
121/// Number of u32 words expected for a frontier over `node_count` nodes.
122#[must_use]
123pub const fn frontier_words(node_count: u32) -> usize {
124    bitset_words(node_count) as usize
125}
126
127/// Mask for valid bits in the final frontier word.
128#[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/// Build a fused GPU program for one frontier-closure absorption step.
139#[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/// Build one frontier-absorption body.
179///
180/// `next_wave[w] = (neighbors[w] & domain_mask) & !visited[w]`
181/// and `visited[w] |= next_wave[w]`. When `added_counts` is supplied, the body
182/// also writes `popcount(next_wave[w])`.
183#[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/// Build one frontier-absorption body and optionally set a scalar flag when
206/// any new bit is added.
207#[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/// Build a fused GPU program for one frontier-closure absorption step without
293/// per-word popcount output.
294#[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/// Build a fused frontier-absorption GPU program from the logical node count.
329#[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/// Build a no-count frontier-absorption GPU program from the logical node
348/// count.
349#[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
365/// Validate that `frontier` has the canonical packed shape for `node_count`.
366pub 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
383/// Count set bits in a packed u32 frontier with checked overflow reporting.
384pub 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
397/// Count only in-domain set bits in a frontier with the canonical shape.
398pub 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
421/// Materialize active node ids from a packed frontier into queue order.
422///
423/// The output queue is exact-length, not padded to `queue_capacity`. Callers
424/// that need fixed-size resident buffers can resize after the active prefix is
425/// written.
426pub 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/// Materialize the queue prefix that fits while returning the full active count.
443///
444/// Device-side frontier compaction exposes overflow pressure by allowing the
445/// observed queue length to exceed the queue storage capacity, while clamping
446/// stores to the resident queue. This host helper mirrors that behavior and
447/// still scans set bits word-by-word instead of walking every node id.
448#[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
489/// Materialize active node ids when the caller already knows the active count.
490///
491/// This is the preferred host path for benchmark/dataflow fixtures that counted
492/// active sources while constructing the frontier. It preserves the same
493/// ordered queue and tail-masking behavior as [`materialize_frontier_queue_into`]
494/// without a second full popcount pass.
495pub 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
555/// Clear out-of-domain bits in the final frontier word.
556pub 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
562/// Merge a neighbor frontier into a visited set and materialize only new bits.
563pub 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}