use crate::grammar::gbnf::parse_gbnf;
use crate::grammar::json_schema::compile;
use crate::grammar::pda::{
CompiledGrammar, GrammarState, SimResult, StepResult, advance_byte, simulate_token,
};
use crate::grammar::spec::GrammarSpec;
use crate::grammar::vocab_partition::VocabPartition;
use std::fmt;
#[derive(Debug, Clone)]
pub struct GrammarError(pub String);
impl fmt::Display for GrammarError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "GrammarEngine error: {}", self.0)
}
}
impl std::error::Error for GrammarError {}
impl From<crate::grammar::json_schema::SchemaError> for GrammarError {
fn from(e: crate::grammar::json_schema::SchemaError) -> Self {
GrammarError(e.0)
}
}
impl From<crate::grammar::gbnf::GbnfError> for GrammarError {
fn from(e: crate::grammar::gbnf::GbnfError) -> Self {
GrammarError(e.0)
}
}
fn enumerate_grammar_states(
grammar: &CompiledGrammar,
vocab_bytes: &[Vec<u8>],
max_states: usize,
) -> Vec<GrammarState> {
let initial = GrammarState::initial();
let mut queue: Vec<GrammarState> = vec![initial.clone()];
let mut visited: Vec<GrammarState> = vec![initial];
let mut head = 0;
while head < queue.len() && visited.len() < max_states {
let state = queue[head].clone();
head += 1;
for token_bytes in vocab_bytes {
if token_bytes.is_empty() {
continue;
}
let (result, next_state) = simulate_token(&state, grammar, token_bytes);
if result == SimResult::Accept || result == SimResult::ContextDependent {
if !visited.iter().any(|s| states_equal(s, &next_state)) {
visited.push(next_state.clone());
if visited.len() < max_states {
queue.push(next_state);
}
}
}
}
}
visited
}
fn states_equal(a: &GrammarState, b: &GrammarState) -> bool {
a.stack == b.stack && a.complete == b.complete
}
pub struct GrammarEngine {
grammar: CompiledGrammar,
partition: VocabPartition,
vocab_size: usize,
vocab_bytes: Vec<Vec<u8>>,
}
impl GrammarEngine {
pub fn new(spec: &GrammarSpec, vocab_bytes: Vec<Vec<u8>>) -> Result<Self, GrammarError> {
let vocab_size = vocab_bytes.len();
let grammar = match spec {
GrammarSpec::JsonSchema(schema) => compile(schema)?,
GrammarSpec::Gbnf(gbnf) => parse_gbnf(gbnf)?,
};
let states = enumerate_grammar_states(
&grammar,
&vocab_bytes,
crate::grammar::vocab_partition::MAX_GRAMMAR_STATES,
);
if states.len() >= crate::grammar::vocab_partition::MAX_GRAMMAR_STATES {
tracing::warn!(
"grammar state count hit limit ({}); some states may fall back to context-dependent checks",
crate::grammar::vocab_partition::MAX_GRAMMAR_STATES
);
}
let partition = VocabPartition::build(&grammar, states, &vocab_bytes);
Ok(Self {
grammar,
partition,
vocab_size,
vocab_bytes,
})
}
pub fn initial_state(&self) -> GrammarState {
GrammarState::initial()
}
pub fn mask_logits(&self, state: &mut GrammarState, logits: &mut [f32]) {
assert!(
logits.len() >= self.vocab_size,
"logits length {} < vocab_size {}",
logits.len(),
self.vocab_size
);
match self.find_state_id(state) {
Some(state_id) => {
self.partition.apply_mask(state_id, logits);
for &token_id in self.partition.context_dependent_ids() {
if token_id >= self.vocab_size {
continue;
}
if logits[token_id] == f32::NEG_INFINITY {
continue;
}
let token_bytes = &self.vocab_bytes[token_id];
if token_bytes.is_empty() {
logits[token_id] = f32::NEG_INFINITY;
continue;
}
let (result, _) = simulate_token(state, &self.grammar, token_bytes);
match result {
SimResult::Reject | SimResult::ContextDependent => {
logits[token_id] = f32::NEG_INFINITY;
}
SimResult::Accept => {}
}
}
}
None => {
self.mask_by_simulation(state, logits);
}
}
}
fn mask_by_simulation(&self, state: &GrammarState, logits: &mut [f32]) {
for token_id in 0..self.vocab_size {
if logits[token_id] == f32::NEG_INFINITY {
continue;
}
let token_bytes = &self.vocab_bytes[token_id];
if token_bytes.is_empty() {
logits[token_id] = f32::NEG_INFINITY;
continue;
}
let (result, _) = simulate_token(state, &self.grammar, token_bytes);
if result != SimResult::Accept {
logits[token_id] = f32::NEG_INFINITY;
}
}
}
pub fn advance(&self, state: &mut GrammarState, token_id: u32) -> bool {
let token_id = token_id as usize;
if token_id >= self.vocab_size {
return false;
}
let token_bytes = &self.vocab_bytes[token_id];
if token_bytes.is_empty() {
return true;
}
for &b in token_bytes {
if advance_byte(state, &self.grammar, b) == StepResult::Rejected {
return false;
}
}
true
}
fn find_state_id(&self, state: &GrammarState) -> Option<usize> {
for sid in 0..self.partition.num_states() {
if let Some(ps) = self.partition.grammar_state(sid) {
if ps.stack == state.stack && ps.complete == state.complete {
return Some(sid);
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tiny_vocab() -> Vec<Vec<u8>> {
vec![b"t".to_vec(), b"f".to_vec(), b"x".to_vec()]
}
fn bool_spec() -> GrammarSpec {
GrammarSpec::JsonSchema(serde_json::json!({"type": "boolean"}))
}
#[test]
fn engine_new_from_json_schema() {
let vocab = tiny_vocab();
let spec = bool_spec();
let result = GrammarEngine::new(&spec, vocab);
assert!(result.is_ok(), "engine construction should succeed");
}
#[test]
fn engine_new_from_gbnf() {
let vocab = tiny_vocab();
let spec = GrammarSpec::Gbnf("root ::= \"t\" | \"f\"\n".to_string());
let result = GrammarEngine::new(&spec, vocab);
assert!(result.is_ok());
}
#[test]
fn cyclic_gbnf_does_not_hang() {
let vocab = tiny_vocab();
let spec = GrammarSpec::Gbnf("root ::= root\n".to_string());
let result = GrammarEngine::new(&spec, vocab);
assert!(result.is_ok(), "cyclic GBNF should construct, not hang");
}
#[test]
fn cyclic_ref_schema_does_not_hang() {
let vocab = tiny_vocab();
let spec = GrammarSpec::JsonSchema(serde_json::json!({
"$ref": "#/$defs/Node",
"$defs": { "Node": { "$ref": "#/$defs/Node" } }
}));
let result = GrammarEngine::new(&spec, vocab);
assert!(
result.is_ok(),
"cyclic $ref schema should construct, not hang"
);
}
#[test]
fn array_maxitems_overflow_rejected() {
let vocab = tiny_vocab();
let spec = GrammarSpec::JsonSchema(serde_json::json!({
"type": "array",
"items": { "type": "boolean" },
"maxItems": 18446744073709551615u64
}));
let err = match GrammarEngine::new(&spec, vocab) {
Ok(_) => panic!("absurd maxItems must be rejected, not overflow the stack"),
Err(e) => e.to_string(),
};
assert!(
err.contains("maxItems"),
"error should name the offending field: {err}"
);
}
#[test]
fn array_minitems_overflow_rejected() {
let vocab = tiny_vocab();
let spec = GrammarSpec::JsonSchema(serde_json::json!({
"type": "array",
"items": { "type": "boolean" },
"minItems": 18446744073709551615u64
}));
let err = match GrammarEngine::new(&spec, vocab) {
Ok(_) => panic!("absurd minItems must be rejected, not hang"),
Err(e) => e.to_string(),
};
assert!(
err.contains("minItems"),
"error should name the offending field: {err}"
);
}
#[test]
fn deep_ref_chain_rejected() {
let n = 2000usize;
let mut defs = serde_json::Map::new();
for i in 0..n {
let target = if i + 1 < n {
serde_json::json!({ "$ref": format!("#/$defs/N{}", i + 1) })
} else {
serde_json::json!({ "type": "boolean" })
};
defs.insert(format!("N{i}"), target);
}
let schema = serde_json::json!({ "$ref": "#/$defs/N0", "$defs": defs });
let spec = GrammarSpec::JsonSchema(schema);
let err = match GrammarEngine::new(&spec, tiny_vocab()) {
Ok(_) => panic!("deep $ref chain must be rejected, not overflow the stack"),
Err(e) => e.to_string(),
};
assert!(err.contains("depth"), "error should mention depth: {err}");
}
#[test]
fn shallow_ref_chain_accepted() {
let n = 64usize;
let mut defs = serde_json::Map::new();
for i in 0..n {
let target = if i + 1 < n {
serde_json::json!({ "$ref": format!("#/$defs/N{}", i + 1) })
} else {
serde_json::json!({ "type": "boolean" })
};
defs.insert(format!("N{i}"), target);
}
let schema = serde_json::json!({ "$ref": "#/$defs/N0", "$defs": defs });
let result = GrammarEngine::new(&GrammarSpec::JsonSchema(schema), tiny_vocab());
assert!(result.is_ok(), "a 64-link $ref chain should compile");
}
#[test]
fn array_maxitems_boundary() {
let over = GrammarSpec::JsonSchema(serde_json::json!({
"type": "array", "items": { "type": "boolean" }, "maxItems": 4097
}));
assert!(
GrammarEngine::new(&over, tiny_vocab()).is_err(),
"maxItems just past the cap must be rejected"
);
let ok = GrammarSpec::JsonSchema(serde_json::json!({
"type": "array", "items": { "type": "boolean" }, "maxItems": 8
}));
assert!(
GrammarEngine::new(&ok, tiny_vocab()).is_ok(),
"a small in-range maxItems must still compile"
);
}
#[test]
fn mask_logits_blocks_disallowed() {
let vocab = vec![b"true".to_vec(), b"false".to_vec(), b"other".to_vec()];
let spec = GrammarSpec::JsonSchema(serde_json::json!({"type": "boolean"}));
let engine = GrammarEngine::new(&spec, vocab).unwrap();
let mut state = engine.initial_state();
let mut logits = vec![1.0f32, 2.0f32, 3.0f32];
engine.mask_logits(&mut state, &mut logits);
assert_eq!(
logits[2],
f32::NEG_INFINITY,
"token 'other' should be blocked"
);
assert!(
logits[0] > f32::NEG_INFINITY || logits[1] > f32::NEG_INFINITY,
"at least one of 'true'/'false' must be allowed"
);
}
#[test]
fn advance_updates_state() {
let vocab = vec![b"n".to_vec(), b"u".to_vec(), b"l".to_vec(), b"l".to_vec()];
let spec = GrammarSpec::JsonSchema(serde_json::json!({"type": "null"}));
let engine = GrammarEngine::new(&spec, vocab).unwrap();
let mut state = engine.initial_state();
assert!(engine.advance(&mut state, 0));
}
#[test]
fn advance_rejects_wrong_token() {
let vocab = vec![b"x".to_vec(), b"n".to_vec()];
let spec = GrammarSpec::JsonSchema(serde_json::json!({"type": "null"}));
let engine = GrammarEngine::new(&spec, vocab).unwrap();
let mut state = engine.initial_state();
let result = engine.advance(&mut state, 0);
assert!(!result, "'x' should be rejected for null grammar");
}
#[test]
fn initial_state_not_complete() {
let vocab = vec![b"t".to_vec()];
let spec = GrammarSpec::Gbnf("root ::= \"test\"\n".to_string());
let engine = GrammarEngine::new(&spec, vocab).unwrap();
let state = engine.initial_state();
assert!(!state.is_complete(), "initial state should not be complete");
}
#[test]
fn engine_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<GrammarEngine>();
}
#[test]
fn mask_logits_logits_shorter_panics() {
let vocab = vec![b"a".to_vec(), b"b".to_vec()];
let spec = GrammarSpec::JsonSchema(serde_json::json!({"type": "null"}));
let engine = GrammarEngine::new(&spec, vocab).unwrap();
let mut state = engine.initial_state();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut logits = vec![0.0f32]; engine.mask_logits(&mut state, &mut logits);
}));
assert!(result.is_err(), "should panic when logits too short");
}
#[test]
fn unknown_state_fallback_is_sound() {
use crate::grammar::pda::{CompiledGrammar, Rule, Symbol};
let mut chain = vec![Symbol::Terminal(b'"')];
chain.extend(std::iter::repeat_n(Symbol::Terminal(b'a'), 300));
chain.push(Symbol::Terminal(b'"'));
let grammar = CompiledGrammar {
rules: vec![Rule {
name: "root".to_string(),
alts: vec![chain],
}],
};
let vocab = vec![b"\"".to_vec(), b"a".to_vec()];
let spec = GrammarSpec::Gbnf("root ::= \"placeholder\"\n".to_string());
let mut engine = GrammarEngine::new(&spec, vocab.clone()).unwrap();
let states = enumerate_grammar_states(
&grammar,
&vocab,
crate::grammar::vocab_partition::MAX_GRAMMAR_STATES,
);
assert_eq!(
states.len(),
crate::grammar::vocab_partition::MAX_GRAMMAR_STATES,
"grammar must exceed the state cap for this regression to bite"
);
engine.partition = VocabPartition::build(&grammar, states, &vocab);
engine.grammar = grammar;
let mut state = engine.initial_state();
assert!(engine.advance(&mut state, 0), "opening quote accepted");
for _ in 0..270 {
assert!(engine.advance(&mut state, 1), "mid-chain 'a' accepted");
}
assert!(
engine.find_state_id(&state).is_none(),
"deep state must be unknown to the capped partition (else the \
simulation fallback is not the path under test)"
);
let mut logits = vec![1.0f32, 1.0f32];
engine.mask_logits(&mut state, &mut logits);
assert!(
logits[1] > f32::NEG_INFINITY,
"valid mid-chain token 'a' must remain allowed at the deep state"
);
assert_eq!(
logits[0],
f32::NEG_INFINITY,
"invalid closing-quote token must be blocked at the deep state \
(state-0 fallback would wrongly allow it)"
);
}
#[test]
fn bitmask_and_correctness_large_vocab() {
let mut vocab: Vec<Vec<u8>> = vec![b"true".to_vec(), b"false".to_vec()];
for i in 2..130 {
vocab.push(format!("tok{i}").into_bytes());
}
let spec = GrammarSpec::JsonSchema(serde_json::json!({"type": "boolean"}));
let engine = GrammarEngine::new(&spec, vocab).unwrap();
let mut state = engine.initial_state();
let mut logits = vec![1.0f32; 130];
engine.mask_logits(&mut state, &mut logits);
assert!(
logits[0] > f32::NEG_INFINITY,
"token 'true' should be allowed"
);
assert!(
logits[1] > f32::NEG_INFINITY,
"token 'false' should be allowed"
);
for i in 2..130 {
assert_eq!(logits[i], f32::NEG_INFINITY, "token {i} should be blocked");
}
}
}