1use 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#[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 pub fn iter(&self) -> impl Iterator<Item = (TokenId, StateId)> + '_ {
25 self.entries.iter().copied()
26 }
27
28 #[must_use]
30 pub fn len(&self) -> usize {
31 self.entries.len()
32 }
33
34 #[must_use]
36 pub fn is_empty(&self) -> bool {
37 self.entries.is_empty()
38 }
39
40 #[must_use]
42 pub(crate) fn heap_bytes(&self) -> usize {
43 self.entries.capacity() * std::mem::size_of::<(TokenId, StateId)>()
44 }
45}
46
47#[derive(Copy, Clone, Debug)]
50pub(crate) struct IndexBuildLimits {
51 pub max_states: usize,
53 pub max_row_pairs: usize,
55 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
69pub 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
123struct 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
173const MAX_LAZY_TRANSIENT_BYTES: usize = 1 << 26;
176
177fn 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
186pub(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
197fn 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
220pub(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; 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
269fn 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()?; out.push(TokenId(tok), q);
283 Ok(())
284 })?;
285 Ok(out)
286}
287
288fn 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; }
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), };
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; }
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
337struct 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 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
366pub(crate) fn packed_scratch_fixed_bytes() -> Option<usize> {
369 Some(STEP_MEMO_BYTES + SCRATCH_HEADER_ALLOWANCE)
370}
371
372pub(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
378const STEP_MEMO_BYTES: usize = 256 * (std::mem::size_of::<u32>() + std::mem::size_of::<StateId>());
380
381const SCRATCH_HEADER_ALLOWANCE: usize = 4096;
383
384struct 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 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 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 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 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); 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(); 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); 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}