use std::{
collections::{HashMap, HashSet, VecDeque},
sync::{mpsc, Arc, Mutex},
thread,
time::{Duration, Instant},
};
use ts_grammar::{CompiledGrammar, Compiler, GrammarKind, Matcher, TokenizerInfo};
use ts_wire::SamplingParams;
use crate::codec::{Reader, Writer};
const MAX_ROLLBACK_TOKENS: i32 = 200;
const RETAINED_LIMIT_BYTES: usize = 1 << 30;
const COMPILE_THREADS: i32 = 8;
const COMPILE_WORKERS: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrammarConfig {
pub tokenizer_info: String,
pub any_whitespace: bool,
pub compile_timeout: Duration,
pub max_retries: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GrammarKey {
pub kind: &'static str,
pub source: String,
}
impl GrammarKey {
pub fn of(params: &SamplingParams) -> Option<Self> {
let (kind, source) = if let Some(schema) = ¶ms.json_schema {
("json", schema)
} else if let Some(regex) = ¶ms.regex {
("regex", regex)
} else if let Some(ebnf) = ¶ms.ebnf {
("ebnf", ebnf)
} else {
("structural_tag", params.structural_tag.as_ref()?)
};
Some(Self {
kind,
source: source.clone(),
})
}
}
enum Entry {
Compiling {
started_at: Option<Instant>,
},
Ready(Arc<CompiledGrammar>),
Invalid {
message: String,
expires_at: Option<Instant>,
},
}
#[derive(Clone)]
pub enum Lookup {
Ready(Arc<CompiledGrammar>),
Invalid(InvalidGrammar),
Compiling,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidGrammar {
pub message: String,
pub permanent: bool,
}
pub(crate) fn encode_invalid(entries: &[Option<InvalidGrammar>]) -> Vec<u8> {
let mut out = Writer::new();
for entry in entries {
out.option(entry.as_ref(), |out, invalid| {
out.flag(invalid.permanent);
out.bytes(invalid.message.as_bytes());
});
}
out.finish()
}
pub(crate) fn decode_invalid(bytes: &[u8], count: usize) -> Option<Vec<Option<InvalidGrammar>>> {
let mut reader = Reader::new(bytes);
let entries = (0..count)
.map(|_| {
reader.option(|reader| {
let permanent = reader.flag()?;
Some(InvalidGrammar {
message: String::from_utf8(reader.bytes()?.to_vec()).ok()?,
permanent,
})
})
})
.collect::<Option<Vec<_>>>()?;
reader.finish(entries)
}
impl Entry {
fn lookup(&self) -> Lookup {
match self {
Entry::Compiling { .. } => Lookup::Compiling,
Entry::Ready(grammar) => Lookup::Ready(grammar.clone()),
Entry::Invalid {
message,
expires_at,
} => Lookup::Invalid(InvalidGrammar {
message: message.clone(),
permanent: expires_at.is_none(),
}),
}
}
}
#[derive(Default)]
struct Shared {
cache: HashMap<GrammarKey, Entry>,
timeout_history: HashMap<GrammarKey, u32>,
compiled_keys: HashSet<GrammarKey>,
}
pub struct Grammars {
config: GrammarConfig,
shared: Arc<Mutex<Shared>>,
jobs: mpsc::Sender<GrammarKey>,
retained: VecDeque<GrammarKey>,
retained_grammars: HashMap<GrammarKey, (Arc<CompiledGrammar>, usize)>,
retained_bytes: usize,
retained_limit_bytes: usize,
}
fn compile(
compiler: &Compiler,
key: &GrammarKey,
any_whitespace: bool,
) -> Result<CompiledGrammar, String> {
let failed = |error: ts_grammar::Error| format!("RuntimeError: {error}");
match key.kind {
"json" => {
let schema = if key.source == "$$ANY$$" {
r#"{"type": "object"}"#
} else {
&key.source
};
compiler
.compile(GrammarKind::JsonSchema, schema, any_whitespace)
.map_err(failed)
}
"regex" => compiler
.compile(GrammarKind::Regex, &key.source, any_whitespace)
.map_err(failed),
"ebnf" => compiler
.compile(GrammarKind::Ebnf, &key.source, any_whitespace)
.map_err(failed),
"structural_tag" => {
let tag = structural_tag(&key.source)?;
compiler
.compile(GrammarKind::StructuralTag, &tag, any_whitespace)
.map_err(failed)
}
other => Err(format!("ValueError: Invalid key_type: {other}")),
}
}
fn structural_tag(source: &str) -> Result<String, String> {
let tag: serde_json::Value =
serde_json::from_str(source).map_err(|error| format!("JSONDecodeError: {error}"))?;
if tag.get("format").is_some() {
return Ok(source.to_owned());
}
let missing = |field: &str| format!("KeyError: '{field}'");
let structures = tag
.get("structures")
.and_then(|s| s.as_array())
.ok_or_else(|| missing("structures"))?;
let triggers = tag.get("triggers").ok_or_else(|| missing("triggers"))?;
let tags = structures
.iter()
.map(|structure| {
let field = |name: &str| structure.get(name).cloned().ok_or_else(|| missing(name));
Ok(serde_json::json!({
"type": "tag",
"begin": field("begin")?,
"content": {
"type": "json_schema",
"json_schema": field("schema")?,
"style": "json",
"any_order": false,
"max_whitespace_cnt": null,
"excludes": [],
},
"end": field("end")?,
}))
})
.collect::<Result<Vec<_>, String>>()?;
Ok(serde_json::json!({
"type": "structural_tag",
"format": {
"type": "triggered_tags",
"triggers": triggers,
"tags": tags,
"at_least_one": false,
"stop_after_first": false,
"excludes": [],
},
})
.to_string())
}
impl Grammars {
pub fn new(config: GrammarConfig) -> Result<Self, String> {
let info = TokenizerInfo::from_json(&config.tokenizer_info).map_err(|e| e.to_string())?;
let compiler = Arc::new(Compiler::new(&info, COMPILE_THREADS).map_err(|e| e.to_string())?);
let shared = Arc::new(Mutex::new(Shared::default()));
let (jobs, queue) = mpsc::channel::<GrammarKey>();
let queue = Arc::new(Mutex::new(queue));
for worker in 0..COMPILE_WORKERS {
let (compiler, shared, queue) = (compiler.clone(), shared.clone(), queue.clone());
let any_whitespace = config.any_whitespace;
thread::Builder::new()
.name(format!("ts-grammar-{worker}"))
.spawn(move || loop {
let Ok(key) = queue.lock().expect("compile queue lock").recv() else {
return;
};
if let Some(Entry::Compiling { started_at }) = shared
.lock()
.expect("grammar cache lock")
.cache
.get_mut(&key)
{
*started_at = Some(Instant::now());
}
let compiled = compile(&compiler, &key, any_whitespace);
let mut state = shared.lock().expect("grammar cache lock");
match compiled {
Ok(grammar) => {
state.timeout_history.remove(&key);
state.compiled_keys.insert(key.clone());
state.cache.insert(key, Entry::Ready(Arc::new(grammar)));
}
Err(message) => {
tracing::warn!(kind = key.kind, %message, "grammar does not compile");
state.cache.insert(
key,
Entry::Invalid {
message,
expires_at: None,
},
);
}
}
})
.map_err(|error| error.to_string())?;
}
Ok(Self {
config,
shared,
jobs,
retained: VecDeque::new(),
retained_grammars: HashMap::new(),
retained_bytes: 0,
retained_limit_bytes: RETAINED_LIMIT_BYTES,
})
}
pub fn compile_timeout(&self) -> Duration {
self.config.compile_timeout
}
pub fn lookup(&mut self, key: &GrammarKey) -> (Lookup, bool) {
if let Some((grammar, _)) = self.retained_grammars.get(key) {
let grammar = grammar.clone();
self.retained.retain(|retained| retained != key);
self.retained.push_back(key.clone());
return (Lookup::Ready(grammar), true);
}
let mut state = self.shared.lock().expect("grammar cache lock");
if let Some(Entry::Invalid {
expires_at: Some(expires_at),
..
}) = state.cache.get(key)
{
if Instant::now() >= *expires_at {
state.cache.remove(key);
}
}
match state.cache.get(key) {
Some(entry @ (Entry::Ready(_) | Entry::Invalid { .. })) => (entry.lookup(), true),
Some(Entry::Compiling { .. }) => (Lookup::Compiling, false),
None => {
state
.cache
.insert(key.clone(), Entry::Compiling { started_at: None });
drop(state);
let _ = self.jobs.send(key.clone());
(Lookup::Compiling, false)
}
}
}
pub fn status(&self, key: &GrammarKey) -> Lookup {
if let Some((grammar, _)) = self.retained_grammars.get(key) {
return Lookup::Ready(grammar.clone());
}
match self
.shared
.lock()
.expect("grammar cache lock")
.cache
.get(key)
{
Some(entry) => entry.lookup(),
None => Lookup::Compiling,
}
}
pub fn started_at(&self, key: &GrammarKey) -> Option<Instant> {
match self
.shared
.lock()
.expect("grammar cache lock")
.cache
.get(key)
{
Some(Entry::Compiling { started_at }) => *started_at,
_ => None,
}
}
pub fn record_timeout(&mut self, key: &GrammarKey, message: &str) {
if self.retained_grammars.contains_key(key) {
return;
}
let mut state = self.shared.lock().expect("grammar cache lock");
if matches!(state.cache.get(key), Some(Entry::Ready(_))) {
return;
}
let count = {
let count = state.timeout_history.entry(key.clone()).or_insert(0);
*count += 1;
*count
};
let (message, expires_at) = if count > self.config.max_retries {
(
format!("{message} (gave up after {} retries)", count - 1),
None,
)
} else {
(
message.to_owned(),
Some(Instant::now() + self.config.compile_timeout),
)
};
state.cache.insert(
key.clone(),
Entry::Invalid {
message,
expires_at,
},
);
}
pub fn retain(
&mut self,
admitted: &[(GrammarKey, Arc<CompiledGrammar>)],
waiting: &HashSet<GrammarKey>,
) {
for (key, grammar) in admitted {
if self.retained_grammars.contains_key(key) {
self.retained.retain(|retained| retained != key);
self.retained.push_back(key.clone());
continue;
}
let per_char = if key.source.is_ascii() { 1 } else { 4 };
let size = key.source.len() * per_char + grammar.memory_size_bytes();
if size > self.retained_limit_bytes {
continue;
}
self.retained.push_back(key.clone());
self.retained_grammars
.insert(key.clone(), (grammar.clone(), size));
self.retained_bytes += size;
}
while self.retained_bytes > self.retained_limit_bytes {
let Some(key) = self.retained.pop_front() else {
break;
};
if let Some((_, size)) = self.retained_grammars.remove(&key) {
self.retained_bytes -= size;
}
}
let mut state = self.shared.lock().expect("grammar cache lock");
let done: Vec<GrammarKey> = state
.compiled_keys
.iter()
.filter(|key| !waiting.contains(*key))
.cloned()
.collect();
for key in done {
state.compiled_keys.remove(&key);
if matches!(state.cache.get(&key), Some(Entry::Ready(_))) {
state.cache.remove(&key);
}
}
}
pub fn has_compiled(&self) -> bool {
!self
.shared
.lock()
.expect("grammar cache lock")
.compiled_keys
.is_empty()
}
pub fn matcher(grammar: &CompiledGrammar) -> Result<Matcher, String> {
grammar
.matcher(MAX_ROLLBACK_TOKENS)
.map_err(|error| error.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn grammars(compile_timeout: Duration, max_retries: u32) -> Grammars {
Grammars::new(GrammarConfig {
tokenizer_info: include_str!("../../ts-grammar/tests/fixtures/toy_tokenizer_info.json")
.to_owned(),
any_whitespace: true,
compile_timeout,
max_retries,
})
.unwrap()
}
fn regex(source: &str) -> GrammarKey {
GrammarKey {
kind: "regex",
source: source.to_owned(),
}
}
fn compiled(grammars: &Grammars, key: &GrammarKey) -> Lookup {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
match grammars.status(key) {
Lookup::Compiling if Instant::now() < deadline => {
thread::sleep(Duration::from_millis(1));
}
lookup => return lookup,
}
}
}
fn invalid(lookup: Lookup) -> Option<InvalidGrammar> {
match lookup {
Lookup::Invalid(invalid) => Some(invalid),
_ => None,
}
}
#[test]
fn invalid_grammars_survive_the_replica_exchange() {
let entries = vec![
None,
Some(InvalidGrammar {
message: "timed out".to_owned(),
permanent: false,
}),
Some(InvalidGrammar {
message: "does not compile: ü".to_owned(),
permanent: true,
}),
];
let bytes = encode_invalid(&entries);
assert_eq!(decode_invalid(&bytes, 3), Some(entries));
assert_eq!(decode_invalid(&bytes[..bytes.len() - 1], 3), None);
assert_eq!(decode_invalid(&bytes, 2), None);
}
#[test]
fn a_grammar_that_does_not_compile_stays_invalid() {
let mut grammars = grammars(Duration::from_millis(20), 2);
let key = regex("(");
assert!(matches!(grammars.lookup(&key), (Lookup::Compiling, false)));
assert!(invalid(compiled(&grammars, &key)).unwrap().permanent);
thread::sleep(Duration::from_millis(40));
let (lookup, hit) = grammars.lookup(&key);
assert!(hit && invalid(lookup).unwrap().permanent);
}
#[test]
fn a_timed_out_grammar_fails_fast_for_one_timeout_then_compiles_again() {
let mut grammars = grammars(Duration::from_millis(50), 2);
let key = regex("a");
grammars.record_timeout(&key, "timed out");
let (lookup, hit) = grammars.lookup(&key);
assert!(hit);
assert_eq!(
invalid(lookup),
Some(InvalidGrammar {
message: "timed out".to_owned(),
permanent: false,
})
);
thread::sleep(Duration::from_millis(60));
assert!(matches!(grammars.lookup(&key), (Lookup::Compiling, false)));
assert!(matches!(compiled(&grammars, &key), Lookup::Ready(_)));
}
#[test]
fn repeated_timeouts_give_a_grammar_up_for_good() {
let mut grammars = grammars(Duration::from_millis(20), 1);
let key = regex("a");
grammars.record_timeout(&key, "timed out");
grammars.record_timeout(&key, "timed out");
thread::sleep(Duration::from_millis(30));
let given_up = invalid(grammars.lookup(&key).0).unwrap();
assert!(given_up.permanent);
assert!(
given_up.message.contains("gave up after 1 retries"),
"{given_up:?}"
);
}
#[test]
fn a_compile_clears_the_timeout_history_and_wins_over_a_late_timeout() {
let mut grammars = grammars(Duration::from_millis(20), 1);
let key = regex("a");
grammars.record_timeout(&key, "timed out");
thread::sleep(Duration::from_millis(30));
grammars.lookup(&key);
assert!(matches!(compiled(&grammars, &key), Lookup::Ready(_)));
grammars.record_timeout(&key, "timed out");
assert!(matches!(grammars.status(&key), Lookup::Ready(_)));
grammars.retain(&[], &HashSet::new());
grammars.record_timeout(&key, "timed out");
assert!(!invalid(grammars.status(&key)).unwrap().permanent);
}
#[test]
fn a_compiled_grammar_stays_while_its_request_waits() {
let mut grammars = grammars(Duration::from_secs(30), 2);
let key = regex("a");
grammars.lookup(&key);
assert!(matches!(compiled(&grammars, &key), Lookup::Ready(_)));
let waiting: HashSet<GrammarKey> = [key.clone()].into();
grammars.retain(&[], &waiting);
assert!(grammars.has_compiled());
assert!(matches!(grammars.status(&key), Lookup::Ready(_)));
grammars.retain(&[], &HashSet::new());
assert!(!grammars.has_compiled());
assert!(matches!(grammars.status(&key), Lookup::Compiling));
}
#[test]
fn admitted_grammars_are_kept_most_recently_used_within_the_bound() {
let mut grammars = grammars(Duration::from_secs(30), 2);
let keys: Vec<GrammarKey> = ["a", "b", "x"].into_iter().map(regex).collect();
let mut admitted = Vec::new();
for key in &keys {
grammars.lookup(key);
let Lookup::Ready(grammar) = compiled(&grammars, key) else {
panic!("{key:?} did not compile");
};
admitted.push((key.clone(), grammar));
}
let size = |(key, grammar): &(GrammarKey, Arc<CompiledGrammar>)| {
key.source.len() + grammar.memory_size_bytes()
};
grammars.retained_limit_bytes = size(&admitted[0]) + size(&admitted[1]);
grammars.retain(&admitted[..2], &HashSet::new());
assert!(matches!(
grammars.lookup(&keys[0]),
(Lookup::Ready(_), true)
));
grammars.retain(&admitted[2..], &HashSet::new());
assert!(matches!(grammars.status(&keys[0]), Lookup::Ready(_)));
assert!(matches!(grammars.status(&keys[1]), Lookup::Compiling));
assert!(matches!(grammars.status(&keys[2]), Lookup::Ready(_)));
assert!(grammars.retained_bytes <= grammars.retained_limit_bytes);
}
#[test]
fn a_legacy_structural_tag_becomes_triggered_tags() {
let legacy = r#"{"structures": [{"begin": "<f>", "schema": {"type": "object"}, "end": "</f>"}], "triggers": ["<f"]}"#;
let tag: serde_json::Value =
serde_json::from_str(&structural_tag(legacy).unwrap()).unwrap();
let expected: serde_json::Value = serde_json::from_str(
r#"{"type":"structural_tag","format":{"type":"triggered_tags","triggers":["<f"],"tags":[{"type":"tag","begin":"<f>","content":{"type":"json_schema","json_schema":{"type":"object"},"style":"json","any_order":false,"max_whitespace_cnt":null,"excludes":[]},"end":"</f>"}],"at_least_one":false,"stop_after_first":false,"excludes":[]}}"#,
)
.unwrap();
assert_eq!(tag, expected);
let modern =
r#"{"type": "structural_tag", "format": {"type": "const_string", "value": "x"}}"#;
assert_eq!(structural_tag(modern).unwrap(), modern);
assert!(structural_tag("{")
.unwrap_err()
.starts_with("JSONDecodeError"));
}
}