use std::convert::Infallible;
use aho_corasick::{AhoCorasick, AhoCorasickBuilder, Anchored, Input, MatchKind, StartKind};
use rustc_hash::FxHashMap;
use super::policy::{PolicyError, SpecialMode};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AddedToken {
pub id: u32,
pub lstrip: bool,
pub rstrip: bool,
}
impl AddedToken {
pub const fn plain(id: u32) -> Self {
Self {
id,
lstrip: false,
rstrip: false,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct AddedTokenSet {
tokens: FxHashMap<String, AddedToken>,
}
impl AddedTokenSet {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, content: impl Into<String>, token: AddedToken) {
self.tokens.insert(content.into(), token);
}
pub fn insert_plain(&mut self, content: impl Into<String>, id: u32) {
self.insert(content, AddedToken::plain(id));
}
pub fn get(&self, content: &str) -> Option<AddedToken> {
self.tokens.get(content).copied()
}
pub fn len(&self) -> usize {
self.tokens.len()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, AddedToken)> + '_ {
self.tokens.iter().map(|(k, v)| (k.as_str(), *v))
}
pub fn into_id_map(self) -> FxHashMap<String, u32> {
self.tokens.into_iter().map(|(k, v)| (k, v.id)).collect()
}
}
impl From<FxHashMap<String, u32>> for AddedTokenSet {
fn from(map: FxHashMap<String, u32>) -> Self {
Self {
tokens: map
.into_iter()
.map(|(k, id)| (k, AddedToken::plain(id)))
.collect(),
}
}
}
impl From<&FxHashMap<String, u32>> for AddedTokenSet {
fn from(map: &FxHashMap<String, u32>) -> Self {
Self {
tokens: map
.iter()
.map(|(k, id)| (k.clone(), AddedToken::plain(*id)))
.collect(),
}
}
}
impl FromIterator<(String, AddedToken)> for AddedTokenSet {
fn from_iter<T: IntoIterator<Item = (String, AddedToken)>>(iter: T) -> Self {
Self {
tokens: iter.into_iter().collect(),
}
}
}
#[derive(Clone)]
struct StartBytes {
first: Vec<u8>,
pairs: Box<[u64; 1024]>,
}
impl StartBytes {
fn new(patterns: &[&str]) -> Option<Self> {
let mut first: Vec<u8> = Vec::new();
let mut pairs = Box::new([0u64; 1024]);
for pattern in patterns {
let bytes = pattern.as_bytes();
let &lead = bytes.first()?;
if !first.contains(&lead) {
first.push(lead);
if first.len() > 3 {
return None;
}
}
match bytes.get(1) {
Some(&second) => {
let bit = (lead as usize) << 8 | second as usize;
pairs[bit >> 6] |= 1 << (bit & 63);
}
None => {
for slot in &mut pairs[(lead as usize) << 2..(lead as usize) << 2 | 4] {
*slot = u64::MAX;
}
}
}
}
Some(Self { first, pairs })
}
#[inline]
fn admits(&self, haystack: &[u8], pos: usize) -> bool {
let bit = (haystack[pos] as usize) << 8 | *haystack.get(pos + 1).unwrap_or(&0) as usize;
self.pairs[bit >> 6] & 1 << (bit & 63) != 0
}
#[inline]
fn next(&self, haystack: &[u8], from: usize) -> Option<usize> {
let mut at = from;
loop {
let found = match *self.first.as_slice() {
[a] => memchr::memchr(a, &haystack[at..]),
[a, b] => memchr::memchr2(a, b, &haystack[at..]),
[a, b, c] => memchr::memchr3(a, b, c, &haystack[at..]),
_ => unreachable!("StartBytes holds one to three lead bytes"),
}?;
let pos = at + found;
if self.admits(haystack, pos) {
return Some(pos);
}
at = pos + 1;
}
}
}
enum Matches<'a, 'h> {
Scan(aho_corasick::FindIter<'a, 'h>),
Candidates {
matcher: &'a AhoCorasick,
starts: &'a StartBytes,
text: &'h str,
at: usize,
},
}
impl Iterator for Matches<'_, '_> {
type Item = aho_corasick::Match;
fn next(&mut self) -> Option<Self::Item> {
match self {
Matches::Scan(iter) => iter.next(),
Matches::Candidates {
matcher,
starts,
text,
at,
} => loop {
let pos = starts.next(text.as_bytes(), *at)?;
let found = matcher
.try_find(
Input::new(text)
.span(pos..text.len())
.anchored(Anchored::Yes),
)
.ok()
.flatten();
match found {
Some(m) => {
*at = m.end().max(pos + 1);
return Some(m);
}
None => *at = pos + 1,
}
},
}
}
}
pub(crate) fn opens_input(text: &str, gap: &str) -> bool {
std::ptr::eq(text.as_ptr(), gap.as_ptr())
}
#[derive(Clone)]
pub struct AddedTokens {
matcher: AhoCorasick,
tokens: Vec<AddedToken>,
starts: Option<StartBytes>,
}
impl AddedTokens {
pub fn new(set: &AddedTokenSet) -> Result<Option<Self>, aho_corasick::BuildError> {
if set.is_empty() {
return Ok(None);
}
let entries: Vec<(&str, AddedToken)> = set.iter().collect();
let patterns: Vec<&str> = entries.iter().map(|(k, _)| *k).collect();
let tokens: Vec<AddedToken> = entries.iter().map(|(_, t)| *t).collect();
let matcher = AhoCorasickBuilder::new()
.match_kind(MatchKind::LeftmostLongest)
.start_kind(StartKind::Both)
.build(&patterns)?;
let anchored_supported = matcher
.try_find(Input::new("").anchored(Anchored::Yes))
.is_ok();
let starts = anchored_supported
.then(|| StartBytes::new(&patterns))
.flatten();
Ok(Some(Self {
matcher,
tokens,
starts,
}))
}
fn find_iter<'h>(&self, text: &'h str) -> Matches<'_, 'h> {
match &self.starts {
Some(starts) => Matches::Candidates {
matcher: &self.matcher,
starts,
text,
at: 0,
},
None => Matches::Scan(self.matcher.find_iter(text)),
}
}
pub fn id_at_start(&self, text: &str) -> Option<u32> {
self.matcher
.find(text)
.filter(|m| m.start() == 0)
.map(|m| self.tokens[m.pattern().as_usize()].id)
}
fn swallowed_by_lstrip(&self, text: &str, start: usize, end: usize) -> bool {
if !text[start..end].chars().all(char::is_whitespace) {
return false;
}
let run_end = end + text[end..].len() - text[end..].trim_start().len();
self.matcher
.try_find(
Input::new(text)
.span(run_end..text.len())
.anchored(Anchored::Yes),
)
.ok()
.flatten()
.is_some_and(|m| self.tokens[m.pattern().as_usize()].lstrip)
}
pub fn encode_with<F>(&self, text: &str, encode_gap: F) -> Vec<u32>
where
F: FnMut(&str, &mut Vec<u32>),
{
match self.encode_matched(text, encode_gap, |_, _| Ok::<(), Infallible>(())) {
Ok(ids) => ids,
Err(never) => match never {},
}
}
pub fn encode_with_mode<F>(
&self,
text: &str,
mode: &SpecialMode<'_>,
mut encode_gap: F,
) -> Result<Vec<u32>, PolicyError>
where
F: FnMut(&str, &mut Vec<u32>),
{
match mode {
SpecialMode::Ordinary => {
let mut out = Vec::new();
encode_gap(text, &mut out);
Ok(out)
}
SpecialMode::All => Ok(self.encode_with(text, encode_gap)),
SpecialMode::Allow(allowed) => {
self.encode_matched(text, encode_gap, |matched, offset| {
if allowed.contains(matched) {
Ok(())
} else {
Err(PolicyError::DisallowedSpecial {
token: matched.to_owned(),
offset,
})
}
})
}
}
}
fn encode_matched<F, A, E>(
&self,
text: &str,
mut encode_gap: F,
mut admit: A,
) -> Result<Vec<u32>, E>
where
F: FnMut(&str, &mut Vec<u32>),
A: FnMut(&str, usize) -> Result<(), E>,
{
let mut out = Vec::new();
let mut last = 0;
for m in self.find_iter(text) {
if m.end() <= last {
continue;
}
if self.swallowed_by_lstrip(text, m.start(), m.end()) {
continue;
}
let token = self.tokens[m.pattern().as_usize()];
let match_start = m.start().max(last);
let gap_end = if token.lstrip {
last + text[last..match_start].trim_end().len()
} else {
match_start
};
if gap_end > last {
encode_gap(&text[last..gap_end], &mut out);
}
admit(&text[m.start()..m.end()], m.start())?;
out.push(token.id);
last = m.end();
if token.rstrip {
let tail = &text[last..];
last += tail.len() - tail.trim_start().len();
}
}
if last < text.len() {
encode_gap(&text[last..], &mut out);
}
Ok(out)
}
pub fn dispatch<F>(added: &Option<Self>, text: &str, mut encode_gap: F) -> Vec<u32>
where
F: FnMut(&str, &mut Vec<u32>),
{
match added {
Some(added) => added.encode_with(text, encode_gap),
None => {
let mut out = Vec::new();
encode_gap(text, &mut out);
out
}
}
}
pub fn dispatch_with_mode<F>(
added: &Option<Self>,
text: &str,
mode: &SpecialMode<'_>,
mut encode_gap: F,
) -> Result<Vec<u32>, PolicyError>
where
F: FnMut(&str, &mut Vec<u32>),
{
match added {
Some(added) => added.encode_with_mode(text, mode, encode_gap),
None => {
let mut out = Vec::new();
encode_gap(text, &mut out);
Ok(out)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn plain_set(entries: &[(&str, u32)]) -> AddedTokenSet {
let mut set = AddedTokenSet::new();
for (content, id) in entries {
set.insert_plain(*content, *id);
}
set
}
fn bytes(gap: &str) -> Vec<u32> {
gap.bytes().map(u32::from).collect()
}
#[test]
fn the_prefilter_finds_exactly_what_the_automaton_finds() {
use proptest::prelude::*;
let set = plain_set(&[
("<|start|>", 1),
("<|end|>", 2),
("<pad>", 3),
("|DSML|", 4),
("<", 5),
]);
let added = AddedTokens::new(&set).expect("builds").expect("non-empty");
assert!(
added.starts.is_some(),
"these patterns open with two distinct bytes and must take the prefilter"
);
let pieces = [
"<|start|>",
"<|end|>",
"<pad>",
"|DSML|",
"<",
",",
"。",
"中",
"a",
" ",
"<|",
"|",
"<p",
"\n",
];
let mut runner = proptest::test_runner::TestRunner::deterministic();
let strategy = proptest::collection::vec(0usize..pieces.len(), 0..24);
runner
.run(&strategy, |picks| {
let text: String = picks.iter().map(|&i| pieces[i]).collect();
let mine: Vec<_> = added
.find_iter(&text)
.map(|m| (m.start(), m.end(), m.pattern().as_usize()))
.collect();
let theirs: Vec<_> = added
.matcher
.find_iter(text.as_str())
.map(|m| (m.start(), m.end(), m.pattern().as_usize()))
.collect();
prop_assert_eq!(mine, theirs, "diverged on {:?}", text);
Ok(())
})
.expect("the prefilter must agree with the automaton on every generated string");
}
#[test]
fn the_prefilter_declines_patterns_with_many_lead_bytes() {
let set = plain_set(&[("a", 1), ("b", 2), ("c", 3), ("d", 4)]);
let added = AddedTokens::new(&set).expect("builds").expect("non-empty");
assert!(added.starts.is_none());
}
#[test]
fn prefers_longest_overlapping_added_token() {
let at = AddedTokens::new(&plain_set(&[(" ", 10), (" ", 20)]))
.unwrap()
.unwrap();
let ids = at.encode_with("a b", |gap: &str, out: &mut Vec<u32>| {
out.extend(bytes(gap))
});
assert_eq!(ids, vec![u32::from(b'a'), 20, u32::from(b'b')]);
}
#[test]
fn empty_map_yields_no_matcher() {
assert!(AddedTokens::new(&AddedTokenSet::new()).unwrap().is_none());
}
#[test]
fn plain_id_map_round_trips_without_flags() {
let mut map = FxHashMap::default();
map.insert("<pad>".to_string(), 1);
let set = AddedTokenSet::from(&map);
assert_eq!(set.get("<pad>"), Some(AddedToken::plain(1)));
assert_eq!(AddedTokenSet::from(map.clone()).into_id_map(), map);
}
fn special_map() -> AddedTokenSet {
plain_set(&[("<|im_start|>", 100), ("<|im_end|>", 101)])
}
#[test]
fn all_mode_matches_every_configured_special() {
let at = AddedTokens::new(&special_map()).unwrap().unwrap();
let ids = at
.encode_with_mode(
"<|im_start|>hi<|im_end|>",
&SpecialMode::All,
|gap, out: &mut Vec<u32>| out.extend(gap.bytes().map(u32::from)),
)
.unwrap();
assert_eq!(ids, vec![100, u32::from(b'h'), u32::from(b'i'), 101]);
}
#[test]
fn allow_mode_permits_a_listed_token() {
let at = AddedTokens::new(&special_map()).unwrap().unwrap();
let mut allowed = rustc_hash::FxHashSet::default();
allowed.insert("<|im_start|>".to_string());
let ids = at
.encode_with_mode(
"<|im_start|>hi",
&SpecialMode::Allow(&allowed),
|gap, out: &mut Vec<u32>| out.extend(gap.bytes().map(u32::from)),
)
.unwrap();
assert_eq!(ids, vec![100, u32::from(b'h'), u32::from(b'i')]);
}
#[test]
fn allow_mode_refuses_an_unlisted_token_with_the_right_token_and_offset() {
let at = AddedTokens::new(&special_map()).unwrap().unwrap();
let allowed = rustc_hash::FxHashSet::default();
let err = at
.encode_with_mode(
"hi<|im_end|>",
&SpecialMode::Allow(&allowed),
|gap, out: &mut Vec<u32>| out.extend(gap.bytes().map(u32::from)),
)
.unwrap_err();
match err {
PolicyError::DisallowedSpecial { token, offset } => {
assert_eq!(token, "<|im_end|>");
assert_eq!(offset, 2);
}
other => panic!("expected DisallowedSpecial, got {other:?}"),
}
}
#[test]
fn ordinary_mode_never_promotes_the_literal_text() {
let at = AddedTokens::new(&special_map()).unwrap().unwrap();
let mut gap_calls = Vec::new();
let ids = at
.encode_with_mode(
"<|im_start|>hi",
&SpecialMode::Ordinary,
|gap, out: &mut Vec<u32>| {
gap_calls.push(gap.to_string());
out.extend(gap.bytes().map(u32::from));
},
)
.unwrap();
assert_eq!(gap_calls, vec!["<|im_start|>hi".to_string()]);
assert_eq!(
ids,
"<|im_start|>hi".bytes().map(u32::from).collect::<Vec<_>>()
);
}
fn strip_set(lstrip: bool, rstrip: bool) -> AddedTokenSet {
let mut set = AddedTokenSet::new();
set.insert(
"<mask>",
AddedToken {
id: 250_001,
lstrip,
rstrip,
},
);
set.insert_plain("<pad>", 1);
set
}
fn record(calls: &mut Vec<String>) -> impl FnMut(&str, &mut Vec<u32>) + '_ {
move |gap: &str, out: &mut Vec<u32>| {
calls.push(gap.to_string());
out.extend(bytes(gap));
}
}
#[test]
fn lstrip_absorbs_the_preceding_whitespace() {
let at = AddedTokens::new(&strip_set(true, false)).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with("end. <mask>x", record(&mut calls));
assert_eq!(calls, vec!["end.".to_string(), "x".to_string()]);
let mut expect = bytes("end.");
expect.push(250_001);
expect.extend(bytes("x"));
assert_eq!(ids, expect);
}
#[test]
fn lstrip_beats_a_whitespace_token_claiming_the_same_run() {
let mut set = strip_set(true, false);
set.insert_plain(" ", 900);
let at = AddedTokens::new(&set).unwrap().unwrap();
let mut calls = Vec::new();
assert_eq!(
at.encode_with(" <mask>", record(&mut calls)),
vec![250_001]
);
assert!(calls.is_empty(), "a gap was encoded: {calls:?}");
let mut calls = Vec::new();
assert_eq!(
at.encode_with(" \n <mask>", record(&mut calls)),
vec![250_001]
);
assert!(calls.is_empty(), "a gap was encoded: {calls:?}");
let mut calls = Vec::new();
assert_eq!(
at.encode_with("<mask> ", record(&mut calls)),
vec![250_001, 900]
);
let mut plain = AddedTokenSet::new();
plain.insert_plain("<mask>", 250_001);
plain.insert_plain(" ", 900);
let at = AddedTokens::new(&plain).unwrap().unwrap();
let mut calls = Vec::new();
assert_eq!(
at.encode_with(" <mask>", record(&mut calls)),
vec![900, 250_001]
);
}
#[test]
fn rstrip_absorbs_the_following_whitespace() {
let at = AddedTokens::new(&strip_set(false, true)).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with("a <mask> b", record(&mut calls));
assert_eq!(calls, vec!["a ".to_string(), "b".to_string()]);
let mut expect = bytes("a ");
expect.push(250_001);
expect.extend(bytes("b"));
assert_eq!(ids, expect);
}
#[test]
fn both_flags_absorb_whitespace_on_both_sides() {
let at = AddedTokens::new(&strip_set(true, true)).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with("a \t<mask>\u{3000} b", record(&mut calls));
assert_eq!(calls, vec!["a".to_string(), "b".to_string()]);
let mut expect = bytes("a");
expect.push(250_001);
expect.extend(bytes("b"));
assert_eq!(ids, expect);
}
#[test]
fn flags_off_leave_both_gaps_untouched() {
let at = AddedTokens::new(&strip_set(false, false)).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with("a <mask> b", record(&mut calls));
assert_eq!(calls, vec!["a ".to_string(), " b".to_string()]);
let mut expect = bytes("a ");
expect.push(250_001);
expect.extend(bytes(" b"));
assert_eq!(ids, expect);
}
#[test]
fn a_gap_that_strips_to_empty_is_never_handed_to_the_gap_encoder() {
let at = AddedTokens::new(&strip_set(true, true)).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with(" <mask> ", record(&mut calls));
assert!(calls.is_empty(), "gap encoder was called with {calls:?}");
assert_eq!(ids, vec![250_001]);
}
#[test]
fn rstrip_reaching_over_a_whitespace_token_encodes_the_text_once() {
let mut set = AddedTokenSet::new();
set.insert(
"[R]",
AddedToken {
id: 5,
lstrip: false,
rstrip: true,
},
);
set.insert_plain(" ", 6);
let at = AddedTokens::new(&set).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with("[R] x", record(&mut calls));
assert_eq!(calls, vec!["x".to_string()]);
let mut expect = vec![5];
expect.extend(bytes("x"));
assert_eq!(ids, expect);
}
#[test]
fn only_the_flagged_one_of_two_adjacent_added_tokens_strips() {
let at = AddedTokens::new(&strip_set(true, false)).unwrap().unwrap();
let mut calls = Vec::new();
let ids = at.encode_with("x<pad> a <mask>a", record(&mut calls));
assert_eq!(
calls,
vec!["x".to_string(), " a".to_string(), "a".to_string()]
);
let mut expect = bytes("x");
expect.push(1);
expect.extend(bytes(" a"));
expect.push(250_001);
expect.extend(bytes("a"));
assert_eq!(ids, expect);
}
}