use super::fsm::Fsm;
use crate::sampling::GrammarTokenizer;
#[derive(Debug, Clone, Default)]
struct Node {
children: Vec<(char, u32)>,
tokens: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct TokenTrie {
nodes: Vec<Node>,
vocab_size: usize,
empty_tokens: usize,
}
impl TokenTrie {
#[must_use]
pub fn build<T: GrammarTokenizer + ?Sized>(tokenizer: &T, vocab_size: usize) -> Self {
let mut nodes: Vec<Node> = vec![Node::default()];
let mut empty_tokens = 0usize;
for id in 0..vocab_size {
let id_u32 = u32::try_from(id).expect("vocab_size exceeds u32");
let text = tokenizer.text_of(id_u32);
if text.is_empty() {
empty_tokens += 1;
continue;
}
let mut cur = 0usize;
for ch in text.chars() {
let pos = nodes[cur].children.binary_search_by_key(&ch, |&(c, _)| c);
cur = match pos {
Ok(i) => nodes[cur].children[i].1 as usize,
Err(i) => {
let next = nodes.len();
let next_u32 = u32::try_from(next).expect("trie exceeds u32 nodes");
nodes[cur].children.insert(i, (ch, next_u32));
nodes.push(Node::default());
next
}
};
}
nodes[cur].tokens.push(id_u32);
}
Self {
nodes,
vocab_size,
empty_tokens,
}
}
#[must_use]
pub const fn vocab_size(&self) -> usize {
self.vocab_size
}
#[must_use]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub const fn empty_token_count(&self) -> usize {
self.empty_tokens
}
pub fn allowed_tokens(&self, fsm: &Fsm<'_>, out: &mut Vec<u32>) -> usize {
out.clear();
let mut advances = 0usize;
self.walk(0, fsm, out, &mut advances);
advances
}
fn walk(&self, node: usize, fsm: &Fsm<'_>, out: &mut Vec<u32>, advances: &mut usize) {
for &(ch, child) in &self.nodes[node].children {
if !fsm.accepts(ch) {
continue;
}
let mut next = fsm.clone();
if next.advance(ch).is_err() {
continue;
}
*advances += 1;
let child = child as usize;
out.extend_from_slice(&self.nodes[child].tokens);
self.walk(child, &next, out, advances);
}
}
}
#[cfg(test)]
mod tests {
use super::super::gbnf::parse_gbnf;
use super::*;
struct MockTokenizer {
tokens: Vec<String>,
eos: u32,
}
impl GrammarTokenizer for MockTokenizer {
fn text_of(&self, id: u32) -> String {
self.tokens.get(id as usize).cloned().unwrap_or_default()
}
fn eos_id(&self) -> u32 {
self.eos
}
}
fn mock(words: &[&str]) -> MockTokenizer {
MockTokenizer {
tokens: words.iter().map(|s| (*s).to_string()).collect(),
eos: 0,
}
}
fn naive_allowed(fsm: &Fsm<'_>, tok: &MockTokenizer) -> Vec<u32> {
(0..tok.tokens.len() as u32)
.filter(|&id| {
let t = tok.text_of(id);
!t.is_empty() && fsm.accepts_str(&t)
})
.collect()
}
#[test]
fn build_shares_prefixes_and_counts_empties() {
let tok = mock(&["", "sp", "sph", "sphere", "s", "box", ""]);
let trie = TokenTrie::build(&tok, tok.tokens.len());
assert_eq!(trie.node_count(), 10);
assert_eq!(trie.empty_token_count(), 2);
assert_eq!(trie.vocab_size(), 7);
}
#[test]
fn allowed_matches_naive_on_terminal_grammar() {
let tok = mock(&["", "y", "ye", "yes", "yesterday", "n", "s", "es"]);
let trie = TokenTrie::build(&tok, tok.tokens.len());
let grammar = parse_gbnf(r#"root ::= "yes""#).unwrap();
let mut fsm = Fsm::start(&grammar).unwrap();
let mut got = Vec::new();
trie.allowed_tokens(&fsm, &mut got);
got.sort_unstable();
assert_eq!(got, naive_allowed(&fsm, &tok));
assert_eq!(got, vec![1, 2, 3]);
fsm.advance('y').unwrap();
trie.allowed_tokens(&fsm, &mut got);
got.sort_unstable();
assert_eq!(got, naive_allowed(&fsm, &tok));
assert_eq!(got, vec![7]); }
#[test]
fn allowed_matches_naive_across_lol_like_walk() {
let tok = mock(&[
"", "sphere", "sp", "s", "(", "1", ".", "5", ")", "1.5", "(1", "1.", ".5", "))", " ",
"box", "b", "e(", "here(", "5)", "0", "-", "-1",
]);
let trie = TokenTrie::build(&tok, tok.tokens.len());
let grammar = parse_gbnf(
r#"root ::= ws expr ws
ws ::= [ \t]*
expr ::= "sphere" "(" number ")" | "box" "(" number ")"
number ::= "-"? [0-9]+ ("." [0-9]+)?"#,
)
.unwrap();
let mut fsm = Fsm::start(&grammar).unwrap();
let mut got = Vec::new();
for ch in "sphere(1.5)".chars() {
trie.allowed_tokens(&fsm, &mut got);
got.sort_unstable();
assert_eq!(got, naive_allowed(&fsm, &tok), "state before {ch:?}");
fsm.advance(ch).unwrap();
}
trie.allowed_tokens(&fsm, &mut got);
got.sort_unstable();
assert_eq!(got, naive_allowed(&fsm, &tok));
assert_eq!(got, vec![14]);
}
#[test]
fn duplicate_texts_report_every_id() {
let tok = mock(&["", "a", "a", "ab"]);
let trie = TokenTrie::build(&tok, tok.tokens.len());
let grammar = parse_gbnf(r#"root ::= "ab""#).unwrap();
let fsm = Fsm::start(&grammar).unwrap();
let mut got = Vec::new();
trie.allowed_tokens(&fsm, &mut got);
got.sort_unstable();
assert_eq!(got, vec![1, 2, 3]);
}
}