1use std::fmt;
19use std::sync::Arc;
20
21use crate::error::{CompileError, HashState, MatcherError, Provenance};
22use crate::frontend::SchemaResourceLimits;
23use crate::index::{BindMode, BoundByteTrie, TrieCache, VocabularyHandle};
24use crate::ir::CompileOptions;
25use crate::primitives::TokenId;
26use crate::runtime::{
27 CachedExecutable, CompiledArtifact, ExecutableCache, ExecutableCacheStats, ExecutableSchema,
28 Matcher,
29};
30use crate::structured::{StructuredMatcher, StructuredMatcherError, StructuredProgram};
31use crate::vocab::Vocabulary;
32
33#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum BindPolicy {
37 #[default]
39 Adaptive,
40 Eager,
42 Lazy,
44}
45
46#[derive(Clone, Copy, Debug)]
48#[non_exhaustive]
49pub struct CompilerOptions {
50 pub executable_cache_bytes: usize,
52 pub trie_cache_bytes: usize,
54 pub bind_policy: BindPolicy,
56}
57
58impl Default for CompilerOptions {
59 fn default() -> Self {
60 Self {
61 executable_cache_bytes: 256 << 20,
62 trie_cache_bytes: 256 << 20,
63 bind_policy: BindPolicy::Adaptive,
64 }
65 }
66}
67
68impl CompilerOptions {
69 #[must_use]
71 pub fn with_executable_cache_bytes(mut self, bytes: usize) -> Self {
72 self.executable_cache_bytes = bytes;
73 self
74 }
75
76 #[must_use]
78 pub fn with_trie_cache_bytes(mut self, bytes: usize) -> Self {
79 self.trie_cache_bytes = bytes;
80 self
81 }
82
83 #[must_use]
85 pub fn with_bind_policy(mut self, policy: BindPolicy) -> Self {
86 self.bind_policy = policy;
87 self
88 }
89}
90
91#[derive(Clone, Copy, Debug)]
93#[non_exhaustive]
94pub struct TrieCacheStats {
95 pub entries: usize,
97 pub bytes: usize,
99 pub hits: u64,
101 pub misses: u64,
103 pub evictions: u64,
105 pub oversized_bypasses: u64,
107}
108
109pub struct Compiler {
112 executable_cache: ExecutableCache,
113 trie_cache: Arc<TrieCache>,
114 bind_policy: BindPolicy,
115}
116
117impl Compiler {
118 #[must_use]
120 pub fn new(options: CompilerOptions) -> Self {
121 Self {
122 executable_cache: ExecutableCache::with_budget(options.executable_cache_bytes),
123 trie_cache: Arc::new(TrieCache::with_budget(options.trie_cache_bytes)),
124 bind_policy: options.bind_policy,
125 }
126 }
127
128 #[must_use]
131 pub fn without_cache() -> Self {
132 Self::new(CompilerOptions {
133 executable_cache_bytes: 0,
134 trie_cache_bytes: 0,
135 bind_policy: BindPolicy::Adaptive,
136 })
137 }
138
139 pub fn compile_json_schema(&self, schema: &str) -> Result<SchemaProgram, CompileError> {
142 self.compile_json_schema_with_options(schema, CompileOptions::default())
143 }
144
145 pub fn compile_json_schema_with_options(
147 &self,
148 schema: &str,
149 options: CompileOptions,
150 ) -> Result<SchemaProgram, CompileError> {
151 let cached = self.executable_cache.get_or_compile_json(schema, options)?;
152 Ok(self.wrap(cached))
153 }
154
155 pub fn compile_json_schema_with_resources(
157 &self,
158 schema: &str,
159 retrieval_uri: Option<&str>,
160 resources: &[(String, String)],
161 options: CompileOptions,
162 limits: SchemaResourceLimits,
163 ) -> Result<SchemaProgram, CompileError> {
164 let cached = self.executable_cache.get_or_compile_schema_set(
165 schema,
166 retrieval_uri,
167 resources,
168 options,
169 limits,
170 )?;
171 Ok(self.wrap(cached))
172 }
173
174 pub fn compile_ir_wire(&self, wire: &[u8]) -> Result<SchemaProgram, CompileError> {
176 let cached = self.executable_cache.get_or_compile_wire(wire)?;
177 Ok(self.wrap(cached))
178 }
179
180 fn wrap(&self, cached: Arc<CachedExecutable>) -> SchemaProgram {
181 SchemaProgram {
182 cached,
183 trie_cache: Arc::clone(&self.trie_cache),
184 bind_policy: self.bind_policy,
185 }
186 }
187
188 #[must_use]
190 pub fn cache_stats(&self) -> ExecutableCacheStats {
191 self.executable_cache.stats()
192 }
193
194 #[must_use]
196 pub fn trie_cache_stats(&self) -> TrieCacheStats {
197 let (hits, misses, evictions, oversized_bypasses) = self.trie_cache.stats();
198 TrieCacheStats {
199 entries: self.trie_cache.len(),
200 bytes: self.trie_cache.bytes(),
201 hits,
202 misses,
203 evictions,
204 oversized_bypasses,
205 }
206 }
207
208 pub fn clear_caches(&self) {
210 self.executable_cache.clear();
211 self.trie_cache.clear();
212 }
213}
214
215#[derive(Clone)]
218pub struct SchemaProgram {
219 cached: Arc<CachedExecutable>,
220 trie_cache: Arc<TrieCache>,
221 bind_policy: BindPolicy,
222}
223
224impl SchemaProgram {
225 pub fn bind(&self, vocabulary: &CompiledVocabulary) -> Result<BoundSchema, CompileError> {
227 let handle = vocabulary.handle.clone();
228 let source_hash = HashState::Hash(self.cached.ir_hash());
229 let eos_token_id = TokenId(handle.eos_token_id());
230 let mask_vocab_size = handle.mask_vocab_size();
231 let inner = match self.cached.executable().as_ref() {
232 ExecutableSchema::Regular(engine) => {
233 let bound_trie = self.trie_cache.bind(&handle)?;
234 let requested = match self.bind_policy {
235 BindPolicy::Lazy => BindMode::TrieJointByteLazy,
236 BindPolicy::Eager | BindPolicy::Adaptive => BindMode::TrieJointBytePacked,
237 };
238 let mode = match self.bind_policy {
239 BindPolicy::Adaptive => CompiledArtifact::recommended_bind_mode(
240 engine.state_count(),
241 mask_vocab_size,
242 &bound_trie,
243 requested,
244 ),
245 BindPolicy::Eager | BindPolicy::Lazy => requested,
246 };
247 let artifact = CompiledArtifact::new_from_bound_trie_shared(
248 &handle,
249 Arc::clone(engine),
250 Provenance::reference(source_hash),
251 mode,
252 &bound_trie,
253 )?;
254 BoundSchemaKind::Regular(Arc::new(artifact))
255 }
256 ExecutableSchema::Structured(program) | ExecutableSchema::Hybrid(program) => {
257 let trie = program
258 .uses_incremental_backend()
259 .then(|| self.trie_cache.bind(&handle))
260 .transpose()?;
261 BoundSchemaKind::Structured(Arc::clone(program), handle, trie)
262 }
263 };
264 Ok(BoundSchema {
265 inner,
266 eos_token_id,
267 mask_vocab_size,
268 })
269 }
270
271 pub fn bind_new_vocabulary(
273 &self,
274 vocabulary: Arc<Vocabulary>,
275 ) -> Result<BoundSchema, CompileError> {
276 self.bind(&CompiledVocabulary::new(vocabulary)?)
277 }
278}
279
280#[derive(Clone)]
282pub struct CompiledVocabulary {
283 handle: VocabularyHandle,
284}
285
286impl CompiledVocabulary {
287 pub fn new(vocabulary: Arc<Vocabulary>) -> Result<Self, CompileError> {
289 Ok(Self {
290 handle: VocabularyHandle::new(vocabulary)?,
291 })
292 }
293
294 pub fn with_logits_vocab_size(
297 vocabulary: Arc<Vocabulary>,
298 logits_vocab_size: usize,
299 ) -> Result<Self, CompileError> {
300 Ok(Self {
301 handle: VocabularyHandle::new_with_logits_vocab_size(
302 vocabulary,
303 Some(logits_vocab_size),
304 )?,
305 })
306 }
307
308 pub fn from_packed(
311 token_bytes: &[u8],
312 byte_offsets: &[u32],
313 token_ids: &[u32],
314 id_offsets: &[u32],
315 eos_token_id: u32,
316 ) -> Result<Self, CompileError> {
317 Ok(Self {
318 handle: VocabularyHandle::from_packed(
319 token_bytes,
320 byte_offsets,
321 token_ids,
322 id_offsets,
323 eos_token_id,
324 )?,
325 })
326 }
327
328 #[must_use]
330 pub fn from_handle(handle: VocabularyHandle) -> Self {
331 Self { handle }
332 }
333
334 #[must_use]
336 pub fn mask_vocab_size(&self) -> usize {
337 self.handle.mask_vocab_size()
338 }
339
340 #[must_use]
342 pub fn eos_token_id(&self) -> u32 {
343 self.handle.eos_token_id()
344 }
345
346 #[must_use]
348 pub fn token_bytes(&self, token_id: u32) -> Option<&[u8]> {
349 self.handle.token_bytes(token_id)
350 }
351}
352
353impl TryFrom<Arc<Vocabulary>> for CompiledVocabulary {
354 type Error = CompileError;
355
356 fn try_from(vocabulary: Arc<Vocabulary>) -> Result<Self, Self::Error> {
357 Self::new(vocabulary)
358 }
359}
360
361#[derive(Clone)]
362enum BoundSchemaKind {
363 Regular(Arc<CompiledArtifact>),
364 Structured(
365 Arc<StructuredProgram>,
366 VocabularyHandle,
367 Option<BoundByteTrie>,
368 ),
369}
370
371#[derive(Clone)]
374pub struct BoundSchema {
375 inner: BoundSchemaKind,
376 eos_token_id: TokenId,
377 mask_vocab_size: usize,
378}
379
380impl BoundSchema {
381 #[must_use]
383 pub fn mask_word_count(&self) -> usize {
384 self.mask_vocab_size.div_ceil(32)
385 }
386
387 #[must_use]
389 pub fn mask_vocab_size(&self) -> usize {
390 self.mask_vocab_size
391 }
392
393 #[must_use]
395 pub fn eos_token_id(&self) -> TokenId {
396 self.eos_token_id
397 }
398
399 pub fn start_session(&self) -> Result<Session, SessionError> {
402 let inner = match &self.inner {
403 BoundSchemaKind::Regular(artifact) => {
404 SessionKind::Regular(Matcher::new(Arc::clone(artifact)))
405 }
406 BoundSchemaKind::Structured(program, handle, trie) => {
407 let matcher = program.new_matcher()?;
408 SessionKind::Structured(Box::new(StructuredSessionState {
409 matcher,
410 program: Arc::clone(program),
411 vocabulary: handle.clone(),
412 trie: trie.clone(),
413 }))
414 }
415 };
416 Ok(Session {
417 inner,
418 lifecycle: Lifecycle::Active,
419 eos_token_id: self.eos_token_id,
420 mask_vocab_size: self.mask_vocab_size,
421 })
422 }
423}
424
425struct StructuredSessionState {
426 matcher: StructuredMatcher,
427 program: Arc<StructuredProgram>,
428 vocabulary: VocabularyHandle,
429 trie: Option<BoundByteTrie>,
430}
431
432enum SessionKind {
433 Regular(Matcher),
434 Structured(Box<StructuredSessionState>),
435}
436
437#[derive(Clone, Copy, PartialEq, Eq, Debug)]
438enum Lifecycle {
439 Active,
442 Stopped,
444}
445
446pub struct Session {
449 inner: SessionKind,
450 lifecycle: Lifecycle,
451 eos_token_id: TokenId,
452 mask_vocab_size: usize,
453}
454
455fn set_bit_le(mask: &mut [u8], id: u32) {
456 mask[(id / 8) as usize] |= 1u8 << (id % 8);
457}
458
459fn u32_words_as_bytes_mut(words: &mut [u32]) -> &mut [u8] {
462 unsafe { std::slice::from_raw_parts_mut(words.as_mut_ptr().cast::<u8>(), words.len() * 4) }
463}
464
465impl Session {
466 #[must_use]
468 pub fn mask_vocab_size(&self) -> usize {
469 self.mask_vocab_size
470 }
471
472 #[must_use]
474 pub fn mask_word_count(&self) -> usize {
475 self.mask_vocab_size.div_ceil(32)
476 }
477
478 #[must_use]
480 pub fn eos_token_id(&self) -> TokenId {
481 self.eos_token_id
482 }
483
484 pub fn mask_byte_count(&self) -> Result<usize, SessionError> {
487 self.mask_word_count()
488 .checked_mul(4)
489 .ok_or(SessionError::MaskWidthOverflow {
490 mask_vocab_size: self.mask_vocab_size,
491 })
492 }
493
494 pub fn write_mask(&mut self, mask: &mut [u32]) -> Result<(), SessionError> {
497 let expected = self.mask_word_count();
498 if mask.len() != expected {
499 return Err(SessionError::MaskBufferLength {
500 expected,
501 actual: mask.len(),
502 });
503 }
504 self.write_mask_le_bytes(u32_words_as_bytes_mut(mask))?;
505 #[cfg(target_endian = "big")]
508 for word in mask.iter_mut() {
509 *word = word.swap_bytes();
510 }
511 Ok(())
512 }
513
514 pub fn write_mask_le_bytes(&mut self, out: &mut [u8]) -> Result<(), SessionError> {
516 let expected = self.mask_byte_count()?;
517 if out.len() != expected {
518 return Err(SessionError::MaskByteBufferLength {
519 expected,
520 actual: out.len(),
521 });
522 }
523 out.fill(0);
524 if self.lifecycle == Lifecycle::Stopped {
525 set_bit_le(out, self.eos_token_id.get());
526 return Ok(());
527 }
528 let result = match &mut self.inner {
529 SessionKind::Regular(matcher) => {
530 let artifact = matcher.artifact();
531 artifact
532 .write_mask_le_bytes_into(&[matcher.state()], out, None)
533 .map_err(SessionError::from)
534 }
535 SessionKind::Structured(session) => session
536 .matcher
537 .write_mask_le_bytes_into(&session.vocabulary, session.trie.as_ref(), out)
538 .map_err(SessionError::Structured),
539 };
540 if result.is_err() {
541 out.fill(0);
542 return result;
543 }
544 if self.is_accepting() {
545 set_bit_le(out, self.eos_token_id.get());
546 }
547 Ok(())
548 }
549
550 pub fn advance(&mut self, token: TokenId) -> Result<(), SessionError> {
552 if self.lifecycle == Lifecycle::Stopped {
553 return Err(SessionError::SessionStopped);
554 }
555 if token == self.eos_token_id {
556 if !self.is_accepting() {
557 return Err(SessionError::IllegalToken { token });
558 }
559 self.lifecycle = Lifecycle::Stopped;
560 return Ok(());
561 }
562 match &mut self.inner {
563 SessionKind::Regular(matcher) => matcher.advance(token).map_err(SessionError::from),
564 SessionKind::Structured(session) => {
565 let bytes = session
566 .vocabulary
567 .token_bytes(token.get())
568 .ok_or(SessionError::UnknownToken { token })?;
569 let accepted = session
570 .matcher
571 .advance(bytes)
572 .map_err(SessionError::Structured)?;
573 if !accepted {
574 return Err(SessionError::IllegalToken { token });
575 }
576 Ok(())
577 }
578 }
579 }
580
581 pub fn reset(&mut self) -> Result<(), SessionError> {
583 match &mut self.inner {
584 SessionKind::Regular(matcher) => {
585 *matcher = Matcher::new(Arc::clone(matcher.artifact()));
586 }
587 SessionKind::Structured(session) => {
588 let replacement = StructuredMatcher::new(Arc::clone(&session.program))
589 .map_err(SessionError::Structured)?;
590 session.matcher = replacement;
591 }
592 }
593 self.lifecycle = Lifecycle::Active;
594 Ok(())
595 }
596
597 #[must_use]
600 pub fn is_accepting(&self) -> bool {
601 match &self.inner {
602 SessionKind::Regular(matcher) => matcher.is_accepting(),
603 SessionKind::Structured(session) => session.matcher.is_accepting(),
604 }
605 }
606
607 #[must_use]
610 pub fn is_dead(&self) -> bool {
611 match &self.inner {
612 SessionKind::Regular(matcher) => matcher.is_dead(),
613 SessionKind::Structured(session) => session.matcher.is_dead(),
614 }
615 }
616
617 #[must_use]
619 pub fn is_stopped(&self) -> bool {
620 self.lifecycle == Lifecycle::Stopped
621 }
622}
623
624#[derive(Debug)]
626#[non_exhaustive]
627pub enum SessionError {
628 Compile(CompileError),
630 Matcher(MatcherError),
632 Structured(StructuredMatcherError),
634 IllegalToken {
636 token: TokenId,
638 },
639 UnknownToken {
641 token: TokenId,
643 },
644 SessionStopped,
646 MaskBufferLength {
648 expected: usize,
650 actual: usize,
652 },
653 MaskByteBufferLength {
655 expected: usize,
657 actual: usize,
659 },
660 MaskWidthOverflow {
662 mask_vocab_size: usize,
664 },
665}
666
667impl fmt::Display for SessionError {
668 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
669 match self {
670 Self::Compile(e) => write!(f, "{e}"),
671 Self::Matcher(e) => write!(f, "{e}"),
672 Self::Structured(e) => write!(f, "{e}"),
673 Self::IllegalToken { token } => write!(f, "[IllegalToken] token {}", token.get()),
674 Self::UnknownToken { token } => write!(f, "unknown token id {}", token.get()),
675 Self::SessionStopped => write!(f, "[SessionStopped] session already committed EOS"),
676 Self::MaskBufferLength { expected, actual } => write!(
677 f,
678 "mask buffer must contain exactly {expected} words, got {actual}"
679 ),
680 Self::MaskByteBufferLength { expected, actual } => write!(
681 f,
682 "mask byte buffer must contain exactly {expected} bytes, got {actual}"
683 ),
684 Self::MaskWidthOverflow { mask_vocab_size } => write!(
685 f,
686 "vocabulary width {mask_vocab_size} overflows the packed mask byte length"
687 ),
688 }
689 }
690}
691
692impl std::error::Error for SessionError {
693 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
694 match self {
695 Self::Compile(e) => Some(e),
696 Self::Matcher(e) => Some(e),
697 Self::Structured(e) => Some(e),
698 _ => None,
699 }
700 }
701}
702
703impl From<CompileError> for SessionError {
704 fn from(e: CompileError) -> Self {
705 Self::Compile(e)
706 }
707}
708
709impl From<MatcherError> for SessionError {
710 fn from(e: MatcherError) -> Self {
711 Self::Matcher(e)
712 }
713}
714
715impl From<StructuredMatcherError> for SessionError {
716 fn from(e: StructuredMatcherError) -> Self {
717 Self::Structured(e)
718 }
719}
720
721#[cfg(test)]
722mod tests {
723 use super::*;
724 use crate::build_vocabulary;
725 use rustc_hash::FxHashMap;
726
727 fn vocabulary(eos: u32) -> CompiledVocabulary {
728 let map: FxHashMap<Vec<u8>, Vec<u32>> =
729 (0u32..256).map(|b| (vec![b as u8], vec![b])).collect();
730 CompiledVocabulary::new(Arc::new(build_vocabulary(eos, map).unwrap())).unwrap()
731 }
732
733 fn eos_bit_set(mask: &[u32], eos: u32) -> bool {
734 (mask[(eos / 32) as usize] >> (eos % 32)) & 1 == 1
735 }
736
737 #[test]
738 fn regular_write_mask_omits_eos_while_active_and_not_accepting() {
739 let compiler = Compiler::new(CompilerOptions::default());
740 let vocab = vocabulary(256);
741 let bound = compiler
742 .compile_json_schema(r#"{"type":"boolean"}"#)
743 .unwrap()
744 .bind(&vocab)
745 .unwrap();
746 let mut session = bound.start_session().unwrap();
747 let mut mask = vec![0u32; bound.mask_word_count()];
748 session.write_mask(&mut mask).unwrap();
749 assert!(
750 !eos_bit_set(&mask, 256),
751 "EOS must not be legal before any content is generated"
752 );
753 }
754
755 #[test]
756 fn regular_write_mask_sets_eos_exactly_when_accepting() {
757 let compiler = Compiler::new(CompilerOptions::default());
758 let vocab = vocabulary(256);
759 let bound = compiler
760 .compile_json_schema(r#"{"type":"boolean"}"#)
761 .unwrap()
762 .bind(&vocab)
763 .unwrap();
764 let mut session = bound.start_session().unwrap();
765 for byte in b"true" {
766 session
767 .advance(TokenId::try_from(*byte as usize).unwrap())
768 .unwrap();
769 }
770 assert!(session.is_accepting());
771 let mut mask = vec![0u32; bound.mask_word_count()];
772 session.write_mask(&mut mask).unwrap();
773 assert!(
774 eos_bit_set(&mask, 256),
775 "EOS must be legal once the prefix is a complete value"
776 );
777 }
778
779 #[test]
780 fn structured_write_mask_sets_eos_exactly_when_accepting() {
781 let compiler = Compiler::new(CompilerOptions::default());
782 let vocab = vocabulary(256);
783 let bound = compiler
784 .compile_json_schema(r#"{"not":{"type":"string"}}"#)
785 .unwrap()
786 .bind(&vocab)
787 .unwrap();
788 let mut session = bound.start_session().unwrap();
789 let mut mask = vec![0u32; bound.mask_word_count()];
790 session.write_mask(&mut mask).unwrap();
791 assert!(!eos_bit_set(&mask, 256));
792 session
793 .advance(TokenId::try_from(b'4' as usize).unwrap())
794 .unwrap();
795 assert!(session.is_accepting());
796 session.write_mask(&mut mask).unwrap();
797 assert!(
798 eos_bit_set(&mask, 256),
799 "a single digit is a complete number"
800 );
801 }
802
803 #[test]
804 fn eos_before_acceptance_is_illegal_and_leaves_state_unchanged() {
805 let compiler = Compiler::new(CompilerOptions::default());
806 let vocab = vocabulary(256);
807 let bound = compiler
808 .compile_json_schema(r#"{"type":"boolean"}"#)
809 .unwrap()
810 .bind(&vocab)
811 .unwrap();
812 let mut session = bound.start_session().unwrap();
813 let err = session.advance(TokenId(256)).unwrap_err();
814 assert!(matches!(err, SessionError::IllegalToken { .. }));
815 assert!(!session.is_stopped());
816 session
818 .advance(TokenId::try_from(b't' as usize).unwrap())
819 .unwrap();
820 }
821
822 #[test]
823 fn eos_at_acceptance_stops_without_reaching_the_matcher() {
824 let compiler = Compiler::new(CompilerOptions::default());
825 let vocab = vocabulary(256);
826 let bound = compiler
827 .compile_json_schema(r#"{"type":"boolean"}"#)
828 .unwrap()
829 .bind(&vocab)
830 .unwrap();
831 let mut session = bound.start_session().unwrap();
832 for byte in b"true" {
833 session
834 .advance(TokenId::try_from(*byte as usize).unwrap())
835 .unwrap();
836 }
837 session.advance(TokenId(256)).unwrap();
838 assert!(session.is_stopped());
839 }
840
841 #[test]
842 fn stopped_session_rejects_every_further_advance() {
843 let compiler = Compiler::new(CompilerOptions::default());
844 let vocab = vocabulary(256);
845 let bound = compiler
846 .compile_json_schema(r#"{"type":"boolean"}"#)
847 .unwrap()
848 .bind(&vocab)
849 .unwrap();
850 let mut session = bound.start_session().unwrap();
851 for byte in b"true" {
852 session
853 .advance(TokenId::try_from(*byte as usize).unwrap())
854 .unwrap();
855 }
856 session.advance(TokenId(256)).unwrap();
857 let err = session
858 .advance(TokenId::try_from(b' ' as usize).unwrap())
859 .unwrap_err();
860 assert!(matches!(err, SessionError::SessionStopped));
861 let err = session.advance(TokenId(256)).unwrap_err();
862 assert!(matches!(err, SessionError::SessionStopped));
863 }
864
865 #[test]
866 fn stopped_mask_is_eos_only() {
867 let compiler = Compiler::new(CompilerOptions::default());
868 let vocab = vocabulary(256);
869 let bound = compiler
870 .compile_json_schema(r#"{"type":"boolean"}"#)
871 .unwrap()
872 .bind(&vocab)
873 .unwrap();
874 let mut session = bound.start_session().unwrap();
875 for byte in b"true" {
876 session
877 .advance(TokenId::try_from(*byte as usize).unwrap())
878 .unwrap();
879 }
880 session.advance(TokenId(256)).unwrap();
881 let mut mask = vec![0xFFFF_FFFFu32; bound.mask_word_count()]; session.write_mask(&mut mask).unwrap();
883 for (i, word) in mask.iter().enumerate() {
884 let expected = if i == 8 { 1u32 } else { 0u32 };
885 assert_eq!(*word, expected, "word {i} must be EOS-only");
886 }
887 }
888
889 #[test]
890 fn reset_returns_to_active_at_the_initial_state() {
891 let compiler = Compiler::new(CompilerOptions::default());
892 let vocab = vocabulary(256);
893 let bound = compiler
894 .compile_json_schema(r#"{"type":"boolean"}"#)
895 .unwrap()
896 .bind(&vocab)
897 .unwrap();
898 let mut session = bound.start_session().unwrap();
899 for byte in b"true" {
900 session
901 .advance(TokenId::try_from(*byte as usize).unwrap())
902 .unwrap();
903 }
904 session.advance(TokenId(256)).unwrap();
905 assert!(session.is_stopped());
906 session.reset().unwrap();
907 assert!(!session.is_stopped());
908 assert!(!session.is_accepting());
909 for byte in b"false" {
911 session
912 .advance(TokenId::try_from(*byte as usize).unwrap())
913 .unwrap();
914 }
915 assert!(session.is_accepting());
916 }
917
918 #[test]
919 fn structured_session_reset_also_returns_to_active() {
920 let compiler = Compiler::new(CompilerOptions::default());
921 let vocab = vocabulary(256);
922 let bound = compiler
923 .compile_json_schema(r#"{"not":{"type":"string"}}"#)
924 .unwrap()
925 .bind(&vocab)
926 .unwrap();
927 let mut session = bound.start_session().unwrap();
928 session
929 .advance(TokenId::try_from(b'4' as usize).unwrap())
930 .unwrap();
931 session.advance(TokenId(256)).unwrap();
932 assert!(session.is_stopped());
933 session.reset().unwrap();
934 assert!(!session.is_stopped());
935 assert!(!session.is_accepting());
936 }
937
938 #[test]
939 fn accessors_agree_with_the_vocabulary() {
940 let compiler = Compiler::new(CompilerOptions::default());
941 let vocab = vocabulary(256);
942 let bound = compiler
943 .compile_json_schema(r#"{"type":"boolean"}"#)
944 .unwrap()
945 .bind(&vocab)
946 .unwrap();
947 assert_eq!(bound.eos_token_id(), TokenId(256));
948 assert_eq!(bound.mask_vocab_size(), vocab.mask_vocab_size());
949 let session = bound.start_session().unwrap();
950 assert_eq!(session.eos_token_id(), TokenId(256));
951 assert_eq!(session.mask_vocab_size(), vocab.mask_vocab_size());
952 assert_eq!(session.mask_word_count(), bound.mask_word_count());
953 }
954
955 #[test]
956 fn compiled_vocabulary_is_reused_across_two_binds() {
957 let compiler = Compiler::new(CompilerOptions::default());
958 let vocab = vocabulary(256);
959 let program = compiler
960 .compile_json_schema(r#"{"type":"boolean"}"#)
961 .unwrap();
962 let bound_a = program.bind(&vocab).unwrap();
963 let bound_b = program.bind(&vocab).unwrap();
964 assert_eq!(bound_a.mask_vocab_size(), bound_b.mask_vocab_size());
965 }
966
967 #[test]
968 fn bound_schema_is_clone_and_sessions_stay_independent() {
969 let compiler = Compiler::new(CompilerOptions::default());
970 let vocab = vocabulary(256);
971 let bound = compiler
972 .compile_json_schema(r#"{"type":"boolean"}"#)
973 .unwrap()
974 .bind(&vocab)
975 .unwrap();
976 let cloned = bound.clone();
977 let mut a = bound.start_session().unwrap();
978 let mut b = cloned.start_session().unwrap();
979 for byte in b"true" {
980 a.advance(TokenId::try_from(*byte as usize).unwrap())
981 .unwrap();
982 }
983 assert!(a.is_accepting());
984 assert!(!b.is_accepting());
986 for byte in b"false" {
987 b.advance(TokenId::try_from(*byte as usize).unwrap())
988 .unwrap();
989 }
990 assert!(b.is_accepting());
991 }
992
993 #[test]
994 fn compile_with_resources_resolves_a_caller_supplied_ref() {
995 let compiler = Compiler::new(CompilerOptions::default());
996 let resources = vec![(
997 "https://example.com/a.json".to_string(),
998 r#"{"$anchor":"value","type":"integer"}"#.to_string(),
999 )];
1000 let program = compiler
1001 .compile_json_schema_with_resources(
1002 r#"{"$ref":"a.json#value"}"#,
1003 Some("https://example.com/root.json"),
1004 &resources,
1005 CompileOptions::default(),
1006 SchemaResourceLimits::default(),
1007 )
1008 .unwrap();
1009 let vocab = vocabulary(256);
1010 let bound = program.bind(&vocab).unwrap();
1011 let mut session = bound.start_session().unwrap();
1012 session
1013 .advance(TokenId::try_from(b'4' as usize).unwrap())
1014 .unwrap();
1015 assert!(session.is_accepting());
1016 }
1017
1018 #[test]
1019 fn compiled_vocabulary_try_from_matches_new() {
1020 let raw = Arc::new(build_vocabulary(256, FxHashMap::default()).unwrap());
1021 let via_try_from = CompiledVocabulary::try_from(Arc::clone(&raw)).unwrap();
1022 let via_new = CompiledVocabulary::new(raw).unwrap();
1023 assert_eq!(via_try_from.eos_token_id(), via_new.eos_token_id());
1024 }
1025
1026 #[test]
1027 fn compiled_vocabulary_from_handle_matches_new() {
1028 let raw = Arc::new(build_vocabulary(256, FxHashMap::default()).unwrap());
1029 let handle = VocabularyHandle::new(Arc::clone(&raw)).unwrap();
1030 let via_handle = CompiledVocabulary::from_handle(handle);
1031 let via_new = CompiledVocabulary::new(raw).unwrap();
1032 assert_eq!(via_handle.eos_token_id(), via_new.eos_token_id());
1033 assert_eq!(via_handle.mask_vocab_size(), via_new.mask_vocab_size());
1034 }
1035
1036 #[test]
1037 fn advancing_an_unknown_token_id_is_a_typed_error_not_a_panic() {
1038 let compiler = Compiler::new(CompilerOptions::default());
1039 let vocab = vocabulary(256);
1040 let bound = compiler
1041 .compile_json_schema(r#"{"not":{"type":"string"}}"#)
1042 .unwrap()
1043 .bind(&vocab)
1044 .unwrap();
1045 let mut session = bound.start_session().unwrap();
1046 let err = session
1047 .advance(TokenId::try_from(9999usize).unwrap())
1048 .unwrap_err();
1049 assert!(matches!(err, SessionError::UnknownToken { .. }));
1050 }
1051
1052 #[test]
1053 fn one_program_binds_to_two_vocabularies_independently() {
1054 let compiler = Compiler::new(CompilerOptions::default());
1055 let program = compiler
1056 .compile_json_schema(r#"{"type":"boolean"}"#)
1057 .unwrap();
1058 let bound_a = program.bind(&vocabulary(256)).unwrap();
1059 let bound_b = program.bind(&vocabulary(257)).unwrap();
1060 let mut session_a = bound_a.start_session().unwrap();
1061 let session_b = bound_b.start_session().unwrap();
1062 for byte in b"true" {
1063 session_a
1064 .advance(TokenId::try_from(*byte as usize).unwrap())
1065 .unwrap();
1066 }
1067 assert!(session_a.is_accepting());
1068 assert!(!session_b.is_accepting());
1069 }
1070
1071 #[test]
1072 fn without_cache_still_compiles_correctly_and_caches_nothing() {
1073 let compiler = Compiler::without_cache();
1074 let schema = r#"{"type":"boolean"}"#;
1075 compiler.compile_json_schema(schema).unwrap();
1076 compiler.compile_json_schema(schema).unwrap();
1077 assert_eq!(compiler.cache_stats().entries, 0);
1078 }
1079
1080 #[test]
1081 fn clear_caches_resets_retention() {
1082 let compiler = Compiler::new(CompilerOptions::default());
1083 compiler
1084 .compile_json_schema(r#"{"type":"boolean"}"#)
1085 .unwrap();
1086 assert!(compiler.cache_stats().entries > 0);
1087 compiler.clear_caches();
1088 assert_eq!(compiler.cache_stats().entries, 0);
1089 }
1090
1091 #[test]
1092 fn a_reused_schema_program_hits_the_compiler_cache() {
1093 let compiler = Compiler::new(CompilerOptions::default());
1094 let schema = r#"{"type":"boolean"}"#;
1095 compiler.compile_json_schema(schema).unwrap();
1096 let misses_after_first = compiler.cache_stats().misses;
1097 compiler.compile_json_schema(schema).unwrap();
1098 assert_eq!(compiler.cache_stats().misses, misses_after_first);
1099 assert!(compiler.cache_stats().hits > 0);
1100 }
1101
1102 #[test]
1103 fn default_bind_policy_is_trie_backed_never_naive() {
1104 let compiler = Compiler::new(CompilerOptions::default());
1105 let vocab = vocabulary(256);
1106 let bound = compiler
1107 .compile_json_schema(r#"{"type":"boolean"}"#)
1108 .unwrap()
1109 .bind(&vocab)
1110 .unwrap();
1111 let BoundSchemaKind::Regular(artifact) = &bound.inner else {
1112 panic!("a boolean schema binds to the regular backend");
1113 };
1114 assert_ne!(artifact.bind_mode(), crate::index::BindMode::Naive);
1115 }
1116
1117 #[test]
1118 fn eager_lazy_adaptive_masks_equal_the_naive_oracle_across_states() {
1119 let vocab = vocabulary(256);
1120 let schema = r#"{"type":"boolean"}"#;
1121 let reference = Compiler::new(CompilerOptions::default());
1122 let program = reference.compile_json_schema(schema).unwrap();
1123 let engine = match program.cached.executable().as_ref() {
1124 ExecutableSchema::Regular(engine) => Arc::clone(engine),
1125 _ => panic!("a boolean schema compiles to the regular backend"),
1126 };
1127 let source_hash = HashState::Hash(program.cached.ir_hash());
1128
1129 for policy in [BindPolicy::Eager, BindPolicy::Lazy, BindPolicy::Adaptive] {
1130 let naive = CompiledArtifact::new_from_handle_shared(
1131 &vocab.handle,
1132 Arc::clone(&engine),
1133 Provenance::reference(source_hash),
1134 )
1135 .unwrap();
1136 let mut naive_matcher = Matcher::new(Arc::new(naive));
1137
1138 let policy_compiler = Compiler::new(CompilerOptions {
1139 bind_policy: policy,
1140 ..CompilerOptions::default()
1141 });
1142 let bound = policy_compiler
1143 .compile_json_schema(schema)
1144 .unwrap()
1145 .bind(&vocab)
1146 .unwrap();
1147 let mut session = bound.start_session().unwrap();
1148
1149 for byte in b"true" {
1150 let mut mask = vec![0u32; bound.mask_word_count()];
1151 session.write_mask(&mut mask).unwrap();
1152 let mut naive_mask = vec![0u32; bound.mask_word_count()];
1153 naive_matcher
1154 .artifact()
1155 .write_mask_into(&[naive_matcher.state()], &mut naive_mask, None)
1156 .unwrap();
1157 assert_eq!(
1158 mask, naive_mask,
1159 "{policy:?} mask disagrees with the naive oracle"
1160 );
1161
1162 let token = TokenId::try_from(*byte as usize).unwrap();
1163 session.advance(token).unwrap();
1164 naive_matcher.advance(token).unwrap();
1165 }
1166 }
1167 }
1168
1169 #[test]
1170 fn structured_trie_mask_equals_the_record_scan_oracle() {
1171 use crate::frontend::schema_to_ir;
1172 let vocab = vocabulary(256);
1173 let schema = r#"{"not":{"type":"string"}}"#;
1174 let ir = Arc::new(schema_to_ir(schema, CompileOptions::default()).unwrap());
1175 let program = StructuredProgram::compile(ir).unwrap();
1176 let mut oracle = StructuredMatcher::new(Arc::clone(&program)).unwrap();
1177
1178 let compiler = Compiler::new(CompilerOptions::default());
1179 let bound = compiler
1180 .compile_json_schema(schema)
1181 .unwrap()
1182 .bind(&vocab)
1183 .unwrap();
1184 let mut session = bound.start_session().unwrap();
1185
1186 let eos_word = 256usize / 32;
1187 let eos_bit = 1u32 << (256u32 % 32);
1188 let mut mask = vec![0u32; bound.mask_word_count()];
1189 let mut oracle_mask = vec![0u32; bound.mask_word_count()];
1190
1191 session.write_mask(&mut mask).unwrap();
1192 oracle
1193 .write_record_mask_words_into(
1194 vocab.mask_vocab_size(),
1195 vocab.handle.iter_records(),
1196 &mut oracle_mask,
1197 )
1198 .unwrap();
1199 assert_eq!(mask, oracle_mask, "at the initial state");
1200
1201 session
1202 .advance(TokenId::try_from(b'4' as usize).unwrap())
1203 .unwrap();
1204 oracle.advance(b"4").unwrap();
1205 session.write_mask(&mut mask).unwrap();
1206 mask[eos_word] &= !eos_bit; oracle
1208 .write_record_mask_words_into(
1209 vocab.mask_vocab_size(),
1210 vocab.handle.iter_records(),
1211 &mut oracle_mask,
1212 )
1213 .unwrap();
1214 assert_eq!(
1215 mask, oracle_mask,
1216 "at the accepting state's ordinary-token bits"
1217 );
1218 }
1219
1220 #[test]
1221 fn write_mask_le_bytes_rejects_wrong_length_and_zeroes_output() {
1222 let compiler = Compiler::new(CompilerOptions::default());
1223 let vocab = vocabulary(256);
1224 let bound = compiler
1225 .compile_json_schema(r#"{"type":"boolean"}"#)
1226 .unwrap()
1227 .bind(&vocab)
1228 .unwrap();
1229 let mut session = bound.start_session().unwrap();
1230 let mut out = vec![0xFFu8; 4 * bound.mask_word_count() - 1];
1231 let err = session.write_mask_le_bytes(&mut out).unwrap_err();
1232 assert!(matches!(err, SessionError::MaskByteBufferLength { .. }));
1233 }
1234
1235 #[test]
1236 fn write_mask_le_bytes_matches_write_mask_bit_for_bit() {
1237 let compiler = Compiler::new(CompilerOptions::default());
1238 let vocab = vocabulary(256);
1239 let bound = compiler
1240 .compile_json_schema(r#"{"type":"boolean"}"#)
1241 .unwrap()
1242 .bind(&vocab)
1243 .unwrap();
1244 let mut session = bound.start_session().unwrap();
1245 for byte in b"tru" {
1246 session
1247 .advance(TokenId::try_from(*byte as usize).unwrap())
1248 .unwrap();
1249 }
1250 let mut words = vec![0u32; bound.mask_word_count()];
1251 session.write_mask(&mut words).unwrap();
1252 let mut bytes = vec![0u8; 4 * bound.mask_word_count()];
1253 session.write_mask_le_bytes(&mut bytes).unwrap();
1254 let words_from_bytes: Vec<u32> = bytes
1255 .chunks_exact(4)
1256 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
1257 .collect();
1258 assert_eq!(words, words_from_bytes);
1259 }
1260
1261 #[test]
1262 fn compiler_without_cache_also_builds_no_retained_trie() {
1263 let compiler = Compiler::without_cache();
1264 let vocab = vocabulary(256);
1265 compiler
1266 .compile_json_schema(r#"{"type":"boolean"}"#)
1267 .unwrap()
1268 .bind(&vocab)
1269 .unwrap();
1270 assert_eq!(compiler.trie_cache_stats().entries, 0);
1271 }
1272
1273 #[test]
1274 fn clear_caches_also_clears_the_trie_cache() {
1275 let compiler = Compiler::new(CompilerOptions::default());
1276 let vocab = vocabulary(256);
1277 compiler
1278 .compile_json_schema(r#"{"type":"boolean"}"#)
1279 .unwrap()
1280 .bind(&vocab)
1281 .unwrap();
1282 assert!(compiler.trie_cache_stats().entries > 0);
1283 compiler.clear_caches();
1284 assert_eq!(compiler.trie_cache_stats().entries, 0);
1285 }
1286}