use regex::Regex;
#[derive(Debug, Clone, Default)]
pub struct SpecialTokens {
entries: Vec<(String, u32)>,
matcher: Option<Regex>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment<'a> {
Text(&'a str),
Special(u32),
}
impl SpecialTokens {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, content: impl Into<String>, id: u32) {
let content = content.into();
if self.entries.iter().any(|(c, i)| c == &content && *i == id) {
return;
}
self.entries.push((content, id));
self.rebuild_matcher();
}
pub fn id_of(&self, content: &str) -> Option<u32> {
self.entries
.iter()
.find(|(c, _)| c == content)
.map(|(_, id)| *id)
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn split<'a>(&self, text: &'a str) -> Vec<Segment<'a>> {
let Some(matcher) = &self.matcher else {
return vec![Segment::Text(text)];
};
let mut segments = Vec::new();
let mut cursor = 0;
for m in matcher.find_iter(text) {
if m.start() > cursor {
segments.push(Segment::Text(&text[cursor..m.start()]));
}
let id = self
.id_of(m.as_str())
.expect("matcher is built only from registered content");
segments.push(Segment::Special(id));
cursor = m.end();
}
if cursor < text.len() {
segments.push(Segment::Text(&text[cursor..]));
}
segments
}
fn rebuild_matcher(&mut self) {
let mut contents: Vec<&str> = self.entries.iter().map(|(c, _)| c.as_str()).collect();
contents.sort_unstable_by_key(|c| std::cmp::Reverse(c.len()));
let pattern = contents
.into_iter()
.map(regex::escape)
.collect::<Vec<_>>()
.join("|");
self.matcher =
Some(Regex::new(&pattern).expect("escaped literal alternation is always valid"));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_specials_registered_is_a_single_text_segment() {
let specials = SpecialTokens::new();
assert_eq!(
specials.split("hello world"),
vec![Segment::Text("hello world")]
);
}
#[test]
fn splits_around_a_special_token() {
let mut specials = SpecialTokens::new();
specials.register("<|endoftext|>", 100);
assert_eq!(
specials.split("hello<|endoftext|>world"),
vec![
Segment::Text("hello"),
Segment::Special(100),
Segment::Text("world"),
]
);
}
#[test]
fn special_token_at_start_or_end_produces_no_empty_text_segment() {
let mut specials = SpecialTokens::new();
specials.register("<|endoftext|>", 100);
assert_eq!(
specials.split("<|endoftext|>hello<|endoftext|>"),
vec![
Segment::Special(100),
Segment::Text("hello"),
Segment::Special(100),
]
);
}
#[test]
fn back_to_back_special_tokens_produce_no_text_segment_between_them() {
let mut specials = SpecialTokens::new();
specials.register("<|im_start|>", 1);
specials.register("<|im_end|>", 2);
assert_eq!(
specials.split("<|im_start|><|im_end|>"),
vec![Segment::Special(1), Segment::Special(2)]
);
}
#[test]
fn longest_match_wins_when_one_special_token_prefixes_another() {
let mut specials = SpecialTokens::new();
specials.register("<|im_start|>", 1);
specials.register("<|im_start|>extra", 2);
assert_eq!(
specials.split("<|im_start|>extra"),
vec![Segment::Special(2)]
);
}
#[test]
fn regex_metacharacters_in_special_token_content_are_treated_literally() {
let mut specials = SpecialTokens::new();
specials.register("[SEP]", 5);
assert_eq!(
specials.split("a[SEP]b"),
vec![Segment::Text("a"), Segment::Special(5), Segment::Text("b")]
);
}
}