Skip to main content

maskforge_core/index/
build.rs

1//! Builds token transitions through a joint trie and DFA walk.
2
3use super::artifact::CompiledIndex;
4use super::trie::{TrieKind, VocabTrie};
5use super::BindMode;
6use crate::automaton::RefEngine;
7use crate::error::{CompileError, ErrorCode, Provenance, Stage};
8use crate::mask::Bitmask;
9use crate::primitives::{ByteClassId, StateId, TokenId, TrieNodeId};
10
11/// The tokens allowed from one DFA state, each with the state its byte string lands in.
12/// Diagnostic-only (see `build_delta`); kept `pub` for `benches/harness.rs`.
13#[derive(Clone, Debug, Default)]
14pub struct DeltaRow {
15    entries: Vec<(TokenId, StateId)>,
16}
17
18impl DeltaRow {
19    fn push(&mut self, token: TokenId, landing: StateId) {
20        self.entries.push((token, landing));
21    }
22
23    /// The `(token, landing state)` pairs allowed from this state. Order is unspecified.
24    pub fn iter(&self) -> impl Iterator<Item = (TokenId, StateId)> + '_ {
25        self.entries.iter().copied()
26    }
27
28    /// The number of allowed tokens (counting each id once per byte sequence it belongs to).
29    #[must_use]
30    pub fn len(&self) -> usize {
31        self.entries.len()
32    }
33
34    /// Whether no token is allowed from this state.
35    #[must_use]
36    pub fn is_empty(&self) -> bool {
37        self.entries.is_empty()
38    }
39
40    /// The row's allocated (not just live) byte footprint, by `Vec::capacity`.
41    #[must_use]
42    pub(crate) fn heap_bytes(&self) -> usize {
43        self.entries.capacity() * std::mem::size_of::<(TokenId, StateId)>()
44    }
45}
46
47/// Bounds on eager index construction, so a permissive DFA over a large vocabulary cannot build an
48/// unbounded `Delta`. Crate-internal defaults only; there is no public configuration API yet, so a
49#[derive(Copy, Clone, Debug)]
50pub(crate) struct IndexBuildLimits {
51    /// Maximum reachable states.
52    pub max_states: usize,
53    /// Maximum `(state, token)` incidences in ONE state's row (bounds a single permissive state).
54    pub max_row_pairs: usize,
55    /// Maximum total incidences across the whole `Delta`.
56    pub max_delta_pairs: usize,
57}
58
59impl Default for IndexBuildLimits {
60    fn default() -> Self {
61        Self {
62            max_states: 1 << 22,
63            max_row_pairs: 1 << 21,
64            max_delta_pairs: 1 << 26,
65        }
66    }
67}
68
69/// Builds the full per-state transition relation by walking every reachable state's live subtree.
70/// Only ever builds `TrieJointByte`/`TrieJointClass` - differential-tested correctness paths, not
71pub fn build_delta(
72    engine: &RefEngine,
73    trie: &VocabTrie,
74    mode: BindMode,
75    provenance: Provenance,
76) -> Result<CompiledIndex, CompileError> {
77    build_delta_limited(engine, trie, mode, provenance, IndexBuildLimits::default())
78}
79
80fn build_delta_limited(
81    engine: &RefEngine,
82    trie: &VocabTrie,
83    mode: BindMode,
84    provenance: Provenance,
85    limits: IndexBuildLimits,
86) -> Result<CompiledIndex, CompileError> {
87    match (mode, trie.kind()) {
88        (BindMode::TrieJointByte, TrieKind::Byte) | (BindMode::TrieJointClass, TrieKind::Class) => {
89        }
90        _ => {
91            return Err(CompileError::new(
92                ErrorCode::ArtifactOutOfBounds,
93                Stage::L4Bind,
94                "bind mode does not match the trie alphabet",
95            ))
96        }
97    }
98    if trie.kind() == TrieKind::Class
99        && trie.partition() != Some(super::trie::partition_fingerprint(engine))
100    {
101        return Err(CompileError::new(
102            ErrorCode::ArtifactMismatch,
103            Stage::L4Bind,
104            "class-trie partition does not match the engine byte-class partition",
105        ));
106    }
107    let count = engine.state_count();
108    if count > limits.max_states {
109        return Err(index_limit(
110            "reachable state count exceeds the index build limit",
111        ));
112    }
113    let mut scratch = WalkScratch::new();
114    let mut delta = Vec::with_capacity(count);
115    let mut budget = DeltaBudget::new(limits.max_row_pairs, limits.max_delta_pairs);
116    for raw in 0..count {
117        let s = StateId::try_from(raw).map_err(|_| state_overflow())?;
118        delta.push(walk_one_state(trie, engine, &mut scratch, s, &mut budget)?);
119    }
120    Ok(CompiledIndex::new(delta, provenance, mode))
121}
122
123/// A `Delta` budget checked before each emitted pair: a per-row cap bounds one permissive state, a
124/// total cap bounds the whole index. Both use checked arithmetic.
125struct DeltaBudget {
126    row_used: usize,
127    row_max: usize,
128    total_used: usize,
129    total_max: usize,
130}
131
132impl DeltaBudget {
133    fn new(row_max: usize, total_max: usize) -> Self {
134        Self {
135            row_used: 0,
136            row_max,
137            total_used: 0,
138            total_max,
139        }
140    }
141
142    fn start_row(&mut self) {
143        self.row_used = 0;
144    }
145
146    fn charge_one(&mut self) -> Result<(), CompileError> {
147        self.row_used = self
148            .row_used
149            .checked_add(1)
150            .ok_or_else(|| index_limit("delta row pair count overflow"))?;
151        if self.row_used > self.row_max {
152            return Err(index_limit(
153                "a state's row exceeds the index build row limit",
154            ));
155        }
156        self.total_used = self
157            .total_used
158            .checked_add(1)
159            .ok_or_else(|| index_limit("delta pair count overflow"))?;
160        if self.total_used > self.total_max {
161            return Err(index_limit(
162                "delta pair count exceeds the index build limit",
163            ));
164        }
165        Ok(())
166    }
167}
168
169fn index_limit(msg: &'static str) -> CompileError {
170    CompileError::new(ErrorCode::InternalLimitExceeded, Stage::L4Bind, msg)
171}
172
173/// Cap on one lazy query's transient scratch (just the DFS stack now - the livemask moved to
174/// `RefEngine::live_classes`, charged once at engine-build time, not per walk), checked before any
175const MAX_LAZY_TRANSIENT_BYTES: usize = 1 << 26;
176
177/// Upper bound on one lazy walk's transient bytes: the DFS stack (never exceeds `max_dfs_frontier`
178/// entries, the trie's own precomputed exact peak) plus the fixed step memo and header allowance.
179fn lazy_walk_transient_bytes(max_dfs_frontier: usize) -> Option<usize> {
180    let stack = packed_stack_bytes(max_dfs_frontier)?;
181    stack
182        .checked_add(STEP_MEMO_BYTES)?
183        .checked_add(SCRATCH_HEADER_ALLOWANCE)
184}
185
186/// Writes `s`'s allowed-token mask DIRECTLY into `mask` (the lazy hot path): no `DeltaRow`, no
187/// `(TokenId, StateId)` vector, landing states never materialized - bit-set is idempotent, so the
188pub(crate) fn walk_state_mask_into(
189    trie: &VocabTrie,
190    engine: &RefEngine,
191    s: StateId,
192    mask: &mut Bitmask,
193) -> Result<(), CompileError> {
194    walk_state_mask_into_budgeted(trie, engine, s, mask, MAX_LAZY_TRANSIENT_BYTES)
195}
196
197/// `walk_state_mask_into` with an explicit transient-byte budget (tests can inject a tiny one
198/// instead of needing a real oversized trie). Checked BEFORE any write to `mask`, so a rejection
199fn walk_state_mask_into_budgeted(
200    trie: &VocabTrie,
201    engine: &RefEngine,
202    s: StateId,
203    mask: &mut Bitmask,
204    max_transient_bytes: usize,
205) -> Result<(), CompileError> {
206    let max_dfs_frontier = trie.max_dfs_frontier();
207    let estimated = lazy_walk_transient_bytes(max_dfs_frontier)
208        .ok_or_else(|| index_limit("lazy walk transient estimate overflows"))?;
209    if estimated > max_transient_bytes {
210        return Err(index_limit("lazy walk transient memory exceeds the budget"));
211    }
212    let _permit = crate::mem_gate::try_acquire_vocab_serving_bytes(estimated)?;
213    let stack_fuel = max_dfs_frontier;
214    let mut scratch = WalkScratch::with_stack_capacity(stack_fuel)?;
215    walk_live_subtree(trie, engine, &mut scratch, s, stack_fuel, |tok, _q| {
216        mask.set(TokenId(tok))
217    })
218}
219
220/// Fills a packed mask matrix DIRECTLY from the trie walk, one contiguous `wpr`-word row per state,
221/// with NO intermediate `Delta` and NO per-row allocation. `rows.len()` must be `state_count * wpr`.
222pub(crate) fn fill_packed_rows(
223    trie: &VocabTrie,
224    engine: &RefEngine,
225    rows: &mut [u32],
226    wpr: usize,
227    vocab_size: usize,
228    stack_fuel: usize,
229) -> Result<(), CompileError> {
230    let n = engine.state_count();
231    if wpr != vocab_size.div_ceil(32) {
232        return Err(CompileError::new(
233            ErrorCode::ArtifactOutOfBounds,
234            Stage::L4Bind,
235            "packed words_per_row does not match ceil(vocab_size / 32)",
236        ));
237    }
238    let expected = n
239        .checked_mul(wpr)
240        .ok_or_else(|| index_limit("packed rows length overflows"))?;
241    if rows.len() != expected {
242        return Err(CompileError::new(
243            ErrorCode::ArtifactOutOfBounds,
244            Stage::L4Bind,
245            "packed rows length does not match state_count * words_per_row",
246        ));
247    }
248    let mut scratch = WalkScratch::with_stack_capacity(trie.max_dfs_frontier())?;
249    for raw in 0..n {
250        let s = StateId::try_from(raw).map_err(|_| state_overflow())?;
251        let base = raw * wpr; // base + wpr <= rows.len() (caller sized rows as n * wpr)
252        let row = &mut rows[base..base + wpr];
253        walk_live_subtree(trie, engine, &mut scratch, s, stack_fuel, |tok, _q| {
254            let id = tok as usize;
255            if id >= vocab_size {
256                return Err(CompileError::new(
257                    ErrorCode::ArtifactOutOfBounds,
258                    Stage::L4Bind,
259                    "a token id exceeds the mask vocab size",
260                ));
261            }
262            row[id / 32] |= 1u32 << (id % 32);
263            Ok(())
264        })?;
265    }
266    Ok(())
267}
268
269/// Walks the live subtree under `s`, emitting every allowed token with its landing state.
270/// Iterative (never recursive) so a deep trie cannot overflow the stack.
271fn walk_one_state(
272    trie: &VocabTrie,
273    engine: &RefEngine,
274    scratch: &mut WalkScratch,
275    s: StateId,
276    budget: &mut DeltaBudget,
277) -> Result<DeltaRow, CompileError> {
278    let mut out = DeltaRow::default();
279    budget.start_row();
280    walk_live_subtree(trie, engine, scratch, s, usize::MAX, |tok, q| {
281        budget.charge_one()?; // bound the row DURING construction, not after
282        out.push(TokenId(tok), q);
283        Ok(())
284    })?;
285    Ok(out)
286}
287
288/// The one dead-pruned joint DFS, shared by the row and direct-mask consumers so they can never
289/// diverge. `emit(token, landing_state)` is called for every allowed token; the caller decides
290fn walk_live_subtree<F>(
291    trie: &VocabTrie,
292    engine: &RefEngine,
293    scratch: &mut WalkScratch,
294    s: StateId,
295    stack_fuel: usize,
296    mut emit: F,
297) -> Result<(), CompileError>
298where
299    F: FnMut(u32, StateId) -> Result<(), CompileError>,
300{
301    let dead = engine.dead();
302    let start_live = start_livemask_bug().then(|| engine.live_classes(s));
303    scratch.stack.clear();
304    scratch.stack.push((TrieNodeId(0), s));
305    while let Some((n, q)) = scratch.stack.pop() {
306        record_pop();
307        if q == dead {
308            continue; // reached only with pruning disabled; skipping keeps the output correct
309        }
310        for &tok in trie.leaf_tokens_of(n)? {
311            emit(tok, q)?;
312        }
313        let live = match start_live {
314            Some(buggy) => buggy,
315            None => engine.live_classes(q), // keyed by the CURRENT state q, never the start
316        };
317        scratch.memo.bump();
318        let (keys, kids) = trie.children(n)?;
319        for (&key, &child) in keys.iter().zip(kids) {
320            let class = trie.class_of_edge(key, engine);
321            let ci = usize::from(class.get());
322            if !livemask_all_ones() && !live.contains(ci) {
323                continue; // dead-from-q classes cannot survive; a pure pre-filter (T-F1)
324            }
325            let q2 = scratch.memo.get_or(ci, || edge_step(engine, q, class));
326            if q2 != dead || dead_prune_disabled() {
327                if scratch.stack.len() >= stack_fuel {
328                    return Err(index_limit("joint-walk stack exceeds the build fuel"));
329                }
330                scratch.stack.push((child, q2));
331            }
332        }
333    }
334    Ok(())
335}
336
337/// Per-walk scratch: a per-node step memo and the reusable DFS stack. The livemask that used to
338/// live here (SPARSE hashmap or DENSE table, recomputed/memoized per walk) moved to
339struct WalkScratch {
340    memo: StepMemo,
341    stack: Vec<(TrieNodeId, StateId)>,
342}
343
344impl WalkScratch {
345    fn new() -> Self {
346        Self {
347            memo: StepMemo::new(),
348            stack: Vec::new(),
349        }
350    }
351
352    /// A scratch whose DFS stack is fallibly reserved to `stack_capacity` up front, so an
353    /// allocator failure is a typed error, never an abort - used by both the lazy per-query path
354    fn with_stack_capacity(stack_capacity: usize) -> Result<Self, CompileError> {
355        let mut stack = Vec::new();
356        stack
357            .try_reserve_exact(stack_capacity)
358            .map_err(|_| index_limit("walk stack allocation failed"))?;
359        Ok(Self {
360            memo: StepMemo::new(),
361            stack,
362        })
363    }
364}
365
366/// Fixed scratch bytes the packed fill retains, EXCLUDING the DFS stack (the caller sizes the stack
367/// from the remaining budget and passes it as fuel): just the fixed step memo and a small header
368pub(crate) fn packed_scratch_fixed_bytes() -> Option<usize> {
369    Some(STEP_MEMO_BYTES + SCRATCH_HEADER_ALLOWANCE)
370}
371
372/// EXACT DFS-stack bytes for a walk over a trie whose precomputed exact peak is
373/// `max_dfs_frontier` (`VocabTrie::max_dfs_frontier`): the dense scratch reserves the stack to that
374pub(crate) fn packed_stack_bytes(max_dfs_frontier: usize) -> Option<usize> {
375    max_dfs_frontier.checked_mul(std::mem::size_of::<(TrieNodeId, StateId)>())
376}
377
378/// The step memo's fixed footprint: a 256-entry generation table plus a 256-entry value table.
379const STEP_MEMO_BYTES: usize = 256 * (std::mem::size_of::<u32>() + std::mem::size_of::<StateId>());
380
381/// Small fixed allowance for the scratch `Vec` headers and allocator rounding.
382const SCRATCH_HEADER_ALLOWANCE: usize = 4096;
383
384/// A versioned per-node cache of `step(q, class)`, avoiding a re-step for byte children sharing a
385/// class. Sized to the whole 256-class space so a foreign class id can never index out of range.
386struct StepMemo {
387    generation: Vec<u32>,
388    value: Vec<StateId>,
389    current: u32,
390}
391
392impl StepMemo {
393    fn new() -> Self {
394        Self {
395            generation: vec![0; 256],
396            value: vec![StateId(0); 256],
397            current: 0,
398        }
399    }
400
401    /// Starts a fresh node, invalidating the previous node's cached steps in O(1).
402    fn bump(&mut self) {
403        self.current = self.current.wrapping_add(1);
404        if self.current == 0 {
405            self.generation.iter_mut().for_each(|g| *g = 0);
406            self.current = 1;
407        }
408    }
409
410    fn get_or(&mut self, c: usize, compute: impl FnOnce() -> StateId) -> StateId {
411        if self.generation[c] == self.current {
412            return self.value[c];
413        }
414        let v = compute();
415        self.generation[c] = self.current;
416        self.value[c] = v;
417        v
418    }
419}
420
421#[cfg(test)]
422use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
423
424#[cfg(test)]
425static EDGE_STEPS: AtomicU64 = AtomicU64::new(0);
426#[cfg(test)]
427static NODES_POPPED: AtomicU64 = AtomicU64::new(0);
428#[cfg(test)]
429static LIVEMASK_ALL_ONES: AtomicBool = AtomicBool::new(false);
430#[cfg(test)]
431static DEAD_PRUNE_DISABLED: AtomicBool = AtomicBool::new(false);
432#[cfg(test)]
433static START_LIVEMASK_BUG: AtomicBool = AtomicBool::new(false);
434
435#[inline]
436fn edge_step(engine: &RefEngine, q: StateId, class: ByteClassId) -> StateId {
437    #[cfg(test)]
438    EDGE_STEPS.fetch_add(1, Ordering::Relaxed);
439    engine.step(q, class)
440}
441
442#[inline]
443fn record_pop() {
444    #[cfg(test)]
445    NODES_POPPED.fetch_add(1, Ordering::Relaxed);
446}
447
448#[cfg(test)]
449fn livemask_all_ones() -> bool {
450    LIVEMASK_ALL_ONES.load(Ordering::Relaxed)
451}
452#[cfg(not(test))]
453fn livemask_all_ones() -> bool {
454    false
455}
456
457#[cfg(test)]
458fn dead_prune_disabled() -> bool {
459    DEAD_PRUNE_DISABLED.load(Ordering::Relaxed)
460}
461#[cfg(not(test))]
462fn dead_prune_disabled() -> bool {
463    false
464}
465
466#[cfg(test)]
467fn start_livemask_bug() -> bool {
468    START_LIVEMASK_BUG.load(Ordering::Relaxed)
469}
470#[cfg(not(test))]
471fn start_livemask_bug() -> bool {
472    false
473}
474
475fn state_overflow() -> CompileError {
476    CompileError::new(
477        ErrorCode::InternalLimitExceeded,
478        Stage::L4Bind,
479        "reference state count exceeds the StateId width",
480    )
481}
482
483#[cfg(test)]
484mod tests {
485    use super::*;
486    use crate::automaton::build_from_regex;
487    use crate::mask::Bitmask;
488    use crate::vocab::{build_vocabulary, Vocabulary};
489    use rustc_hash::FxHashMap;
490
491    static GUARD: std::sync::Mutex<()> = std::sync::Mutex::new(());
492    fn guard() -> std::sync::MutexGuard<'static, ()> {
493        GUARD.lock().unwrap_or_else(|p| p.into_inner())
494    }
495
496    fn vocab(pairs: &[(&[u8], &[u32])]) -> Vocabulary {
497        let mut map: FxHashMap<Vec<u8>, Vec<u32>> = FxHashMap::default();
498        for &(bytes, ids) in pairs {
499            map.insert(bytes.to_vec(), ids.to_vec());
500        }
501        build_vocabulary(9999, map).expect("vocab")
502    }
503
504    /// The independent reference: the naive per-token walk, sharing no code with the trie walk.
505    fn naive_mask(engine: &RefEngine, v: &Vocabulary, s: StateId, width: usize) -> Bitmask {
506        let mut m = Bitmask::zeros(width);
507        for (bytes, ids) in v.tokens() {
508            if engine.consume_token(s, bytes).is_some() {
509                for &id in ids {
510                    m.set(TokenId(id)).unwrap();
511                }
512            }
513        }
514        m
515    }
516
517    fn row_mask(row: &DeltaRow, width: usize) -> Bitmask {
518        let mut m = Bitmask::zeros(width);
519        for (tok, _land) in row.iter() {
520            m.set(tok).unwrap();
521        }
522        m
523    }
524
525    /// The mask width covering every token id plus EOS.
526    fn width(v: &Vocabulary) -> usize {
527        v.tokens()
528            .values()
529            .flatten()
530            .copied()
531            .max()
532            .map_or(v.eos_token_id(), |m| m.max(v.eos_token_id())) as usize
533            + 1
534    }
535
536    fn byte_index(engine: &RefEngine, v: &Vocabulary) -> CompiledIndex {
537        let trie = VocabTrie::build_byte(v).unwrap();
538        build_delta(engine, &trie, BindMode::TrieJointByte, prov()).unwrap()
539    }
540
541    fn class_index(engine: &RefEngine, v: &Vocabulary) -> CompiledIndex {
542        let trie = VocabTrie::build_class(v, engine).unwrap();
543        build_delta(engine, &trie, BindMode::TrieJointClass, prov()).unwrap()
544    }
545
546    fn prov() -> Provenance {
547        Provenance::reference(crate::error::HashState::Hash([5; 32]))
548    }
549
550    /// Asserts a trie index agrees with the independent naive walk on every reachable state.
551    fn assert_matches_naive(engine: &RefEngine, v: &Vocabulary, idx: &CompiledIndex) {
552        let w = width(v);
553        for raw in 0..engine.state_count() {
554            let s = StateId(raw as u32);
555            let naive = naive_mask(engine, v, s, w);
556            let trie = row_mask(idx.row(s).unwrap(), w);
557            assert_eq!(
558                trie.as_words(),
559                naive.as_words(),
560                "state {raw} mask diverged from naive"
561            );
562        }
563    }
564
565    #[test]
566    fn byte_route_equals_naive_on_string_pattern() {
567        let _g = guard();
568        let engine = build_from_regex("\"(cat|car|carbon)\"").unwrap();
569        let v = vocab(&[
570            (b"\"", &[0]),
571            (b"c", &[1]),
572            (b"a", &[2]),
573            (b"t", &[3]),
574            (b"r", &[4]),
575            (b"b", &[5]),
576            (b"o", &[6]),
577            (b"n", &[7]),
578            (b"cat", &[8]),
579            (b"x", &[9]),
580        ]);
581        assert_matches_naive(&engine, &v, &byte_index(&engine, &v));
582    }
583
584    #[test]
585    fn class_trie_equals_byte_trie_and_naive() {
586        let _g = guard();
587        let engine = build_from_regex("[0-9]{1,3}").unwrap();
588        let v = vocab(&[
589            (b"1", &[0]),
590            (b"2", &[1]),
591            (b"3", &[2]),
592            (b"12", &[3]),
593            (b"123", &[4]),
594            (b"a", &[5]),
595        ]);
596        let byte = byte_index(&engine, &v);
597        let class = class_index(&engine, &v);
598        assert_matches_naive(&engine, &v, &byte);
599        assert_matches_naive(&engine, &v, &class);
600        let w = width(&v);
601        for raw in 0..engine.state_count() {
602            let s = StateId(raw as u32);
603            assert_eq!(
604                row_mask(byte.row(s).unwrap(), w).as_words(),
605                row_mask(class.row(s).unwrap(), w).as_words(),
606                "byte and class routes diverged at state {raw}"
607            );
608        }
609    }
610
611    #[test]
612    fn state_dependent_livemask_open_quote_then_ordinary_char() {
613        let _g = guard();
614        let engine = build_from_regex("\"[a-z]\"").unwrap();
615        let v = vocab(&[(b"\"", &[0]), (b"a", &[1]), (b"\"a", &[2])]);
616        let idx = byte_index(&engine, &v);
617        assert_matches_naive(&engine, &v, &idx);
618        let after_quote = engine.consume_token(engine.start(), b"\"").unwrap();
619        let w = width(&v);
620        assert!(row_mask(idx.row(after_quote).unwrap(), w).get(TokenId(1)));
621    }
622
623    #[test]
624    fn buggy_start_livemask_drops_a_multibyte_token_whose_tail_needs_a_late_class() {
625        let _g = guard();
626        let engine = build_from_regex("\"[a-z]\"").unwrap();
627        let v = vocab(&[(b"\"", &[0]), (b"\"a", &[1])]);
628        let start = engine.start();
629        let w = width(&v);
630
631        let correct = byte_index(&engine, &v);
632        assert!(row_mask(correct.row(start).unwrap(), w).get(TokenId(1)));
633
634        START_LIVEMASK_BUG.store(true, Ordering::Relaxed);
635        let buggy = byte_index(&engine, &v);
636        START_LIVEMASK_BUG.store(false, Ordering::Relaxed);
637        assert!(
638            !row_mask(buggy.row(start).unwrap(), w).get(TokenId(1)),
639            "the start-livemask bug must drop the multibyte token (proving the test is not vacuous)"
640        );
641        assert!(row_mask(byte_index(&engine, &v).row(start).unwrap(), w).get(TokenId(1)));
642    }
643
644    #[test]
645    fn dead_prune_disabled_stays_correct_but_pops_more_nodes() {
646        let _g = guard();
647        let engine = build_from_regex("(cat|car|carbon)").unwrap();
648        let v = vocab(&[(b"cat", &[0]), (b"car", &[1]), (b"dog", &[2]), (b"c", &[3])]);
649
650        LIVEMASK_ALL_ONES.store(true, Ordering::Relaxed);
651
652        NODES_POPPED.store(0, Ordering::Relaxed);
653        let pruned = byte_index(&engine, &v);
654        let pruned_pops = NODES_POPPED.load(Ordering::Relaxed);
655
656        DEAD_PRUNE_DISABLED.store(true, Ordering::Relaxed);
657        NODES_POPPED.store(0, Ordering::Relaxed);
658        let unpruned = byte_index(&engine, &v);
659        let unpruned_pops = NODES_POPPED.load(Ordering::Relaxed);
660        DEAD_PRUNE_DISABLED.store(false, Ordering::Relaxed);
661        LIVEMASK_ALL_ONES.store(false, Ordering::Relaxed);
662
663        assert_matches_naive(&engine, &v, &pruned);
664        assert_matches_naive(&engine, &v, &unpruned); // still correct
665        assert!(
666            unpruned_pops > pruned_pops,
667            "disabling the dead-prune must visit strictly more nodes ({unpruned_pops} vs {pruned_pops})"
668        );
669    }
670
671    #[test]
672    fn livemask_all_ones_stays_correct_but_steps_more() {
673        let _g = guard();
674        let engine = build_from_regex("(cat|car|carbon)").unwrap();
675        let v = vocab(&[(b"cat", &[0]), (b"car", &[1]), (b"z", &[2])]);
676
677        EDGE_STEPS.store(0, Ordering::Relaxed);
678        let filtered = byte_index(&engine, &v);
679        let filtered_steps = EDGE_STEPS.load(Ordering::Relaxed);
680
681        LIVEMASK_ALL_ONES.store(true, Ordering::Relaxed);
682        EDGE_STEPS.store(0, Ordering::Relaxed);
683        let unfiltered = byte_index(&engine, &v);
684        let unfiltered_steps = EDGE_STEPS.load(Ordering::Relaxed);
685        LIVEMASK_ALL_ONES.store(false, Ordering::Relaxed);
686
687        assert_matches_naive(&engine, &v, &filtered);
688        assert_matches_naive(&engine, &v, &unfiltered);
689        assert!(
690            unfiltered_steps > filtered_steps,
691            "the livemask AND must remove edge steps ({unfiltered_steps} vs {filtered_steps})"
692        );
693    }
694
695    #[test]
696    fn shared_prefix_walk_steps_fewer_than_the_naive_restart() {
697        let _g = guard();
698        let engine = build_from_regex("(ca|cat|car|carbon)").unwrap();
699        let v = vocab(&[
700            (b"ca", &[0]),
701            (b"cat", &[1]),
702            (b"car", &[2]),
703            (b"carbon", &[3]),
704        ]);
705        let trie = VocabTrie::build_byte(&v).unwrap();
706
707        EDGE_STEPS.store(0, Ordering::Relaxed);
708        let mut scratch = WalkScratch::new();
709        let mut budget = DeltaBudget::new(usize::MAX, usize::MAX);
710        let _ = walk_one_state(&trie, &engine, &mut scratch, engine.start(), &mut budget).unwrap();
711        let trie_steps = EDGE_STEPS.load(Ordering::Relaxed);
712
713        EDGE_STEPS.store(0, Ordering::Relaxed);
714        for bytes in v.tokens().keys() {
715            let mut cur = engine.start();
716            for &b in bytes {
717                if cur == engine.dead() {
718                    break;
719                }
720                cur = edge_step(&engine, cur, engine.class_of_byte(b));
721            }
722        }
723        let naive_steps = EDGE_STEPS.load(Ordering::Relaxed);
724        assert!(
725            trie_steps < naive_steps,
726            "the shared prefix must be walked once ({trie_steps} trie steps vs {naive_steps} naive)"
727        );
728    }
729
730    #[test]
731    fn empty_language_and_dead_state_have_empty_rows() {
732        let _g = guard();
733        let engine = build_from_regex("[a&&b]").unwrap(); // empty language: start is DEAD
734        let v = vocab(&[(b"a", &[0]), (b"b", &[1])]);
735        let idx = byte_index(&engine, &v);
736        for raw in 0..engine.state_count() {
737            assert!(idx.row(StateId(raw as u32)).unwrap().is_empty());
738        }
739    }
740
741    #[test]
742    fn stack_scratch_rejects_an_unsatisfiable_allocation_instead_of_aborting() {
743        match WalkScratch::with_stack_capacity(usize::MAX / 2) {
744            Err(e) => assert_eq!(e.code, ErrorCode::InternalLimitExceeded),
745            Ok(_) => panic!("an allocation this large must not succeed"),
746        }
747    }
748
749    #[test]
750    fn deep_trie_uses_the_iterative_stack_without_overflow() {
751        let _g = guard();
752        let engine = build_from_regex("a+").unwrap();
753        let long: Vec<u8> = vec![b'a'; 20_000];
754        let v = vocab(&[(&long, &[0]), (b"a", &[1])]);
755        let idx = byte_index(&engine, &v);
756        assert_matches_naive(&engine, &v, &idx);
757    }
758
759    #[test]
760    fn every_delta_landing_equals_the_naive_consume_token_landing() {
761        let _g = guard();
762        let engine = build_from_regex("(cat|car|carbon)").unwrap();
763        let v = vocab(&[
764            (b"c", &[0]),
765            (b"ca", &[1]),
766            (b"car", &[2]),
767            (b"carbon", &[3]),
768        ]);
769        let idx = byte_index(&engine, &v);
770        for raw in 0..engine.state_count() {
771            let s = StateId(raw as u32);
772            for (tok, landing) in idx.row(s).unwrap().iter() {
773                let bytes = v
774                    .tokens()
775                    .iter()
776                    .find(|(_, ids)| ids.contains(&tok.get()))
777                    .map(|(b, _)| b.clone())
778                    .unwrap();
779                let expected = engine.consume_token(s, &bytes).unwrap();
780                assert_eq!(landing, expected, "token {tok:?} landing at state {raw}");
781                assert_ne!(
782                    landing,
783                    engine.dead(),
784                    "an emitted token never lands on DEAD"
785                );
786            }
787        }
788    }
789
790    #[test]
791    fn a_perturbed_delta_landing_is_caught_by_the_naive_landing_diff() {
792        let _g = guard();
793        let engine = build_from_regex("(cat|car|carbon)").unwrap();
794        let v = vocab(&[
795            (b"c", &[0]),
796            (b"ca", &[1]),
797            (b"car", &[2]),
798            (b"carbon", &[3]),
799        ]);
800        let idx = byte_index(&engine, &v);
801
802        let mut observed: Vec<(u32, u32, u32)> = Vec::new();
803        for raw in 0..engine.state_count() {
804            let s = StateId(raw as u32);
805            for (tok, landing) in idx.row(s).unwrap().iter() {
806                observed.push((raw as u32, tok.get(), landing.get()));
807            }
808        }
809
810        let landing_matches = |set: &[(u32, u32, u32)]| {
811            set.iter().all(|&(raw, tok, landing)| {
812                let bytes = v
813                    .tokens()
814                    .iter()
815                    .find(|(_, ids)| ids.contains(&tok))
816                    .map(|(b, _)| b.clone())
817                    .unwrap();
818                engine.consume_token(StateId(raw), &bytes).unwrap().get() == landing
819            })
820        };
821        assert!(
822            landing_matches(&observed),
823            "unperturbed landings must match naive"
824        );
825
826        let mut perturbed = observed.clone();
827        let (_, _, landing) = &mut perturbed[0];
828        *landing = landing.wrapping_add(1) % engine.state_count() as u32;
829        assert!(
830            !landing_matches(&perturbed),
831            "a single perturbed landing must be rejected (the diff is not vacuous)"
832        );
833    }
834
835    #[test]
836    fn count_conservation_dropping_one_signature_token_would_be_caught() {
837        let _g = guard();
838        let engine = build_from_regex("[0-9]+").unwrap();
839        let v = vocab(&[(b"1", &[0]), (b"2", &[1]), (b"3", &[2]), (b"12", &[3])]);
840        let idx = class_index(&engine, &v);
841        let s = engine.start();
842        let mut allowed: Vec<u32> = idx.row(s).unwrap().iter().map(|(t, _)| t.get()).collect();
843        allowed.sort_unstable();
844        let w = width(&v);
845        let naive = naive_mask(&engine, &v, s, w);
846        let naive_ids: Vec<u32> = (0..w as u32).filter(|&i| naive.get(TokenId(i))).collect();
847        assert_eq!(allowed, naive_ids);
848        let mut dropped = allowed.clone();
849        dropped.pop();
850        assert_ne!(dropped, naive_ids);
851    }
852
853    #[test]
854    fn delta_pair_cap_trips_during_row_construction_no_partial_artifact() {
855        let _g = guard();
856        let engine = build_from_regex("[a-z]+").unwrap();
857        let mut map: FxHashMap<Vec<u8>, Vec<u32>> = FxHashMap::default();
858        for (i, b) in (b'a'..=b'z').enumerate() {
859            map.insert(vec![b], vec![i as u32]);
860        }
861        let v = build_vocabulary(9999, map).unwrap();
862        let trie = VocabTrie::build_byte(&v).unwrap();
863
864        let mut scratch = WalkScratch::new();
865        let mut budget = DeltaBudget::new(usize::MAX, 1); // total cap 1, row cap unlimited
866        let err = walk_one_state(&trie, &engine, &mut scratch, engine.start(), &mut budget)
867            .expect_err("the second emitted pair must trip the cap");
868        assert_eq!(err.code, ErrorCode::InternalLimitExceeded);
869        assert!(
870            budget.total_used <= 2,
871            "the walk stopped at the cap, not after emitting the whole row (used={})",
872            budget.total_used
873        );
874        assert!(build_delta(&engine, &trie, BindMode::TrieJointByte, prov()).is_ok());
875    }
876
877    #[test]
878    fn delta_pair_cap_boundaries_zero_exact_and_one_under() {
879        let _g = guard();
880        let engine = build_from_regex("(true|false)").unwrap();
881        let v = vocab(&[
882            (b"t", &[0]),
883            (b"true", &[1]),
884            (b"f", &[2]),
885            (b"false", &[3]),
886            (b"x", &[4]),
887        ]);
888        let trie = VocabTrie::build_byte(&v).unwrap();
889
890        let total = build_delta(&engine, &trie, BindMode::TrieJointByte, prov())
891            .unwrap()
892            .live_pairs();
893        assert!(total >= 1);
894
895        let with = |cap: usize| {
896            build_delta_limited(
897                &engine,
898                &trie,
899                BindMode::TrieJointByte,
900                prov(),
901                IndexBuildLimits {
902                    max_states: 1 << 20,
903                    max_row_pairs: usize::MAX,
904                    max_delta_pairs: cap,
905                },
906            )
907        };
908        assert!(with(total).is_ok(), "the exact total must fit");
909        assert_eq!(
910            with(total - 1).unwrap_err().code,
911            ErrorCode::InternalLimitExceeded
912        );
913        assert_eq!(with(0).unwrap_err().code, ErrorCode::InternalLimitExceeded);
914    }
915
916    #[test]
917    fn delta_row_cap_boundaries_and_global_interaction() {
918        let _g = guard();
919        let engine = build_from_regex("[a-z]+").unwrap();
920        let mut map: FxHashMap<Vec<u8>, Vec<u32>> = FxHashMap::default();
921        for (i, b) in (b'a'..=b'z').enumerate() {
922            map.insert(vec![b], vec![i as u32]);
923        }
924        let v = build_vocabulary(9999, map).unwrap();
925        let trie = VocabTrie::build_byte(&v).unwrap();
926
927        let start_row = build_delta(&engine, &trie, BindMode::TrieJointByte, prov())
928            .unwrap()
929            .max_row_len();
930        assert_eq!(start_row, 26);
931
932        let with = |row: usize, total: usize| {
933            build_delta_limited(
934                &engine,
935                &trie,
936                BindMode::TrieJointByte,
937                prov(),
938                IndexBuildLimits {
939                    max_states: 1 << 20,
940                    max_row_pairs: row,
941                    max_delta_pairs: total,
942                },
943            )
944        };
945        assert!(with(start_row, usize::MAX).is_ok(), "exact row must fit");
946        assert_eq!(
947            with(start_row - 1, usize::MAX).unwrap_err().code,
948            ErrorCode::InternalLimitExceeded
949        );
950        assert_eq!(
951            with(0, usize::MAX).unwrap_err().code,
952            ErrorCode::InternalLimitExceeded
953        );
954        assert_eq!(
955            with(usize::MAX, 1).unwrap_err().code,
956            ErrorCode::InternalLimitExceeded
957        );
958    }
959
960    #[test]
961    fn fill_packed_rows_rejects_a_mismatched_buffer_shape() {
962        let _g = guard();
963        let engine = build_from_regex("(true|false)").unwrap();
964        let v = vocab(&[
965            (b"t", &[0]),
966            (b"true", &[1]),
967            (b"f", &[2]),
968            (b"false", &[3]),
969        ]);
970        let trie = VocabTrie::build_byte(&v).unwrap();
971        let n = engine.state_count();
972        let vocab_size = width(&v);
973        let wpr = vocab_size.div_ceil(32);
974
975        let mut good = vec![0u32; n * wpr];
976        fill_packed_rows(&trie, &engine, &mut good, wpr, vocab_size, usize::MAX).unwrap();
977        let mut wrong_len = vec![0u32; n * wpr + 1];
978        assert_eq!(
979            fill_packed_rows(&trie, &engine, &mut wrong_len, wpr, vocab_size, usize::MAX)
980                .unwrap_err()
981                .code,
982            ErrorCode::ArtifactOutOfBounds
983        );
984        let mut right_len = vec![0u32; n * wpr];
985        assert_eq!(
986            fill_packed_rows(
987                &trie,
988                &engine,
989                &mut right_len,
990                wpr + 1,
991                vocab_size,
992                usize::MAX
993            )
994            .unwrap_err()
995            .code,
996            ErrorCode::ArtifactOutOfBounds
997        );
998    }
999
1000    #[test]
1001    fn wrong_class_map_is_rejected_structurally_not_walked() {
1002        let _g = guard();
1003        let digit_engine = build_from_regex("[0-9]+").unwrap();
1004        let letter_engine = build_from_regex("[a-z]+").unwrap();
1005        let v = vocab(&[(b"a", &[0]), (b"b", &[1]), (b"1", &[2])]);
1006        let trie = VocabTrie::build_class(&v, &digit_engine).unwrap();
1007        let err = build_delta(&letter_engine, &trie, BindMode::TrieJointClass, prov())
1008            .expect_err("a mismatched class partition must be rejected");
1009        assert_eq!(err.code, ErrorCode::ArtifactMismatch);
1010    }
1011
1012    #[test]
1013    fn lazy_walk_matches_naive_below_the_default_budget() {
1014        let _g = guard();
1015        let engine = build_from_regex("(cat|car|carbon)").unwrap();
1016        let v = vocab(&[
1017            (b"c", &[0]),
1018            (b"ca", &[1]),
1019            (b"car", &[2]),
1020            (b"carbon", &[3]),
1021        ]);
1022        let trie = VocabTrie::build_byte(&v).unwrap();
1023        let w = width(&v);
1024        for raw in 0..engine.state_count() {
1025            let s = StateId(raw as u32);
1026            let mut mask = Bitmask::zeros(w);
1027            walk_state_mask_into(&trie, &engine, s, &mut mask).unwrap();
1028            assert_eq!(mask, naive_mask(&engine, &v, s, w), "state {raw}");
1029        }
1030    }
1031
1032    #[test]
1033    fn lazy_walk_matches_naive_on_a_wide_trie() {
1034        let _g = guard();
1035        let engine = build_from_regex("[\\x00-\\xc7]").unwrap();
1036        let pairs: Vec<(Vec<u8>, Vec<u32>)> =
1037            (0u8..=199).map(|b| (vec![b], vec![u32::from(b)])).collect();
1038        let refs: Vec<(&[u8], &[u32])> = pairs
1039            .iter()
1040            .map(|(b, i)| (b.as_slice(), i.as_slice()))
1041            .collect();
1042        let v = vocab(&refs);
1043        let trie = VocabTrie::build_byte(&v).unwrap();
1044        let w = width(&v);
1045        let mut mask = Bitmask::zeros(w);
1046        walk_state_mask_into(&trie, &engine, engine.start(), &mut mask).unwrap();
1047        assert_eq!(mask, naive_mask(&engine, &v, engine.start(), w));
1048    }
1049
1050    #[test]
1051    fn lazy_walk_matches_naive_on_a_deep_trie() {
1052        let _g = guard();
1053        let engine = build_from_regex("a+").unwrap();
1054        let long: Vec<u8> = vec![b'a'; 20_000];
1055        let v = vocab(&[(&long, &[0]), (b"a", &[1])]);
1056        let trie = VocabTrie::build_byte(&v).unwrap();
1057        let w = width(&v);
1058        let mut mask = Bitmask::zeros(w);
1059        walk_state_mask_into(&trie, &engine, engine.start(), &mut mask).unwrap();
1060        assert_eq!(mask, naive_mask(&engine, &v, engine.start(), w));
1061    }
1062
1063    #[test]
1064    fn lazy_walk_rejects_under_a_tiny_injected_budget() {
1065        let _g = guard();
1066        let engine = build_from_regex("a+").unwrap();
1067        let v = vocab(&[(b"a", &[0]), (b"b", &[1])]);
1068        let trie = VocabTrie::build_byte(&v).unwrap();
1069        let mut mask = Bitmask::zeros(width(&v));
1070        let err = walk_state_mask_into_budgeted(&trie, &engine, engine.start(), &mut mask, 4)
1071            .expect_err("a 4-byte budget cannot hold even the smallest real walk");
1072        assert_eq!(err.code, ErrorCode::InternalLimitExceeded);
1073    }
1074
1075    #[test]
1076    fn lazy_walk_budget_rejection_leaves_the_mask_untouched() {
1077        let _g = guard();
1078        let engine = build_from_regex("a+").unwrap();
1079        let v = vocab(&[(b"a", &[0]), (b"b", &[1])]);
1080        let trie = VocabTrie::build_byte(&v).unwrap();
1081        let mut mask = Bitmask::zeros(width(&v));
1082        walk_state_mask_into_budgeted(&trie, &engine, engine.start(), &mut mask, 4).unwrap_err();
1083        assert_eq!(
1084            mask.count_ones(),
1085            0,
1086            "a rejected admission must not have written any bit"
1087        );
1088    }
1089
1090    #[test]
1091    fn lazy_walk_binds_once_the_budget_covers_the_real_estimate() {
1092        let _g = guard();
1093        let engine = build_from_regex("a+").unwrap();
1094        let v = vocab(&[(b"a", &[0]), (b"b", &[1])]);
1095        let trie = VocabTrie::build_byte(&v).unwrap();
1096        let generous = lazy_walk_transient_bytes(trie.max_dfs_frontier()).unwrap() + 1;
1097        let mut mask = Bitmask::zeros(width(&v));
1098        assert!(
1099            walk_state_mask_into_budgeted(&trie, &engine, engine.start(), &mut mask, generous)
1100                .is_ok()
1101        );
1102    }
1103
1104    #[test]
1105    fn lazy_walk_is_safe_under_concurrent_cold_calls_on_one_trie() {
1106        let _g = guard();
1107        let engine = std::sync::Arc::new(build_from_regex("(cat|car|carbon)").unwrap());
1108        let v = vocab(&[
1109            (b"c", &[0]),
1110            (b"ca", &[1]),
1111            (b"car", &[2]),
1112            (b"carbon", &[3]),
1113        ]);
1114        let trie = std::sync::Arc::new(VocabTrie::build_byte(&v).unwrap());
1115        let w = width(&v);
1116        let handles: Vec<_> = (0..engine.state_count())
1117            .map(|raw| {
1118                let engine = engine.clone();
1119                let trie = trie.clone();
1120                std::thread::spawn(move || {
1121                    let s = StateId(raw as u32);
1122                    let mut mask = Bitmask::zeros(w);
1123                    walk_state_mask_into(&trie, &engine, s, &mut mask).unwrap();
1124                    mask
1125                })
1126            })
1127            .collect();
1128        for (raw, h) in handles.into_iter().enumerate() {
1129            let mask = h.join().unwrap();
1130            let s = StateId(raw as u32);
1131            assert_eq!(mask, naive_mask(&engine, &v, s, w), "state {raw}");
1132        }
1133    }
1134
1135    #[test]
1136    fn lazy_serving_does_not_wait_behind_a_full_build_gate() {
1137        let _g = guard();
1138        let _build_hog = crate::mem_gate::acquire_vocab_build_bytes(1).unwrap();
1139        let engine = build_from_regex("a+").unwrap();
1140        let v = vocab(&[(b"a", &[0]), (b"b", &[1])]);
1141        let trie = VocabTrie::build_byte(&v).unwrap();
1142        let mut mask = Bitmask::zeros(width(&v));
1143        let t = std::time::Instant::now();
1144        walk_state_mask_into(&trie, &engine, engine.start(), &mut mask).unwrap();
1145        assert!(
1146            t.elapsed() < std::time::Duration::from_secs(1),
1147            "a lazy query must not queue behind the build gate"
1148        );
1149    }
1150}