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
);
let state_id = self.find_state_id(state);
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, next_state) = simulate_token(state, &self.grammar, token_bytes);
match result {
SimResult::Reject => {
logits[token_id] = f32::NEG_INFINITY;
}
SimResult::ContextDependent => {
logits[token_id] = f32::NEG_INFINITY;
}
SimResult::Accept => {
let _ = next_state; }
}
}
}
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) -> 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 sid;
}
}
}
0 }
}
#[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 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 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");
}
}
}