use std::borrow::Cow;
use super::policy::{PolicyError, SpecialDecode, SpecialMode};
use super::streaming::{
ByteFallbackRule, DecodePost, DecodeState, RenderRules, StreamingDecoder, Surfaces,
WordSeparator,
};
use super::tokenize::{Tokenize, TokenizeError};
use super::trie::ByteTrie;
use std::collections::HashMap;
use std::convert::Infallible;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum WordPieceError {
#[error("Failed to build added-token matcher: {0}")]
AddedTokensError(#[from] aho_corasick::BuildError),
}
pub struct WordPieceTokenizer {
initial: ByteTrie,
continuation: Option<ByteTrie>,
cache: super::tokenizer::cache::ChunkCache,
id_to_token: Arc<Vec<String>>,
unk_token_id: u32,
max_word_len: usize,
do_lower_case: bool,
strip_accents: bool,
continuation_prefix: String,
handle_chinese_chars: bool,
clean_text: bool,
ascii_fold: [u8; 128],
cls_token_id: Option<u32>,
sep_token_id: Option<u32>,
pad_token_id: Option<u32>,
added: Option<super::added::AddedTokens>,
special_decode: rustc_hash::FxHashSet<u32>,
}
impl WordPieceTokenizer {
pub fn new(
vocab: Vec<String>,
unk_token_id: u32,
max_word_len: usize,
do_lower_case: bool,
) -> Self {
let prefix = if vocab.iter().any(|k| k.starts_with("##")) {
"##".to_string()
} else {
String::new()
};
Self::with_options(
vocab,
unk_token_id,
max_word_len,
do_lower_case,
true,
true,
prefix,
)
}
#[allow(clippy::too_many_arguments)]
pub fn with_options(
vocab: Vec<String>,
unk_token_id: u32,
max_word_len: usize,
do_lower_case: bool,
handle_chinese_chars: bool,
clean_text: bool,
continuation_prefix: String,
) -> Self {
let mut token_to_id = HashMap::with_capacity(vocab.len());
for (id, token) in vocab.iter().enumerate() {
token_to_id.insert(token.clone(), id as u32);
}
let cls_token_id = token_to_id.get("[CLS]").copied();
let sep_token_id = token_to_id.get("[SEP]").copied();
let pad_token_id = token_to_id.get("[PAD]").copied();
let mut special_decode = rustc_hash::FxHashSet::default();
for (id, token) in vocab.iter().enumerate() {
if is_special_token(token) {
special_decode.insert(id as u32);
}
}
let initial = ByteTrie::build(
vocab
.iter()
.enumerate()
.filter(|(_, token)| {
continuation_prefix.is_empty() || !token.starts_with(&continuation_prefix)
})
.map(|(id, token)| (token.as_str(), id as u32)),
);
let continuation = (!continuation_prefix.is_empty()).then(|| {
ByteTrie::build(vocab.iter().enumerate().filter_map(|(id, token)| {
token
.strip_prefix(&continuation_prefix)
.map(|rest| (rest, id as u32))
}))
});
Self {
initial,
continuation,
cache: super::tokenizer::cache::ChunkCache::new(65_536),
id_to_token: Arc::new(vocab),
unk_token_id,
max_word_len,
do_lower_case,
strip_accents: do_lower_case,
continuation_prefix,
handle_chinese_chars,
clean_text,
ascii_fold: ascii_fold_table(clean_text, do_lower_case),
cls_token_id,
sep_token_id,
pad_token_id,
added: None,
special_decode,
}
}
pub fn with_strip_accents(mut self, strip_accents: bool) -> Self {
self.strip_accents = strip_accents;
self
}
pub fn with_added_tokens(
mut self,
tokens: impl Into<super::added::AddedTokenSet>,
) -> Result<Self, WordPieceError> {
self.added = super::added::AddedTokens::new(&tokens.into())?;
Ok(self)
}
pub fn with_special_decode_ids(mut self, ids: rustc_hash::FxHashSet<u32>) -> Self {
self.special_decode.extend(ids);
self
}
pub fn cls_token_id(&self) -> Option<u32> {
self.cls_token_id
}
pub fn sep_token_id(&self) -> Option<u32> {
self.sep_token_id
}
pub fn pad_token_id(&self) -> Option<u32> {
self.pad_token_id
}
pub fn unk_token_id(&self) -> u32 {
self.unk_token_id
}
fn basic_normalize<'a>(&self, text: &'a str) -> Cow<'a, str> {
match self.fold(text) {
Some(folded) => folded,
None => self.fold_staged(text),
}
}
fn fold<'a>(&self, text: &'a str) -> Option<Cow<'a, str>> {
use unicode_general_category::{get_general_category, GeneralCategory};
use unicode_normalization::char::{canonical_combining_class, decompose_canonical};
if !self.clean_text
&& !self.handle_chinese_chars
&& !self.strip_accents
&& !self.do_lower_case
{
return Some(Cow::Borrowed(text));
}
let bytes = text.as_bytes();
let prefix = bytes
.iter()
.position(|&b| b >= 0x80 || self.ascii_fold[b as usize] != b)
.unwrap_or(bytes.len());
if prefix == bytes.len() {
return Some(Cow::Borrowed(text));
}
let mut out = String::with_capacity(text.len() + text.len() / 8 + 16);
out.push_str(&text[..prefix]);
let mut ordered = true;
let rest = &text[prefix..];
let rb = rest.as_bytes();
let mut i = 0;
while i < rb.len() {
let b = rb[i];
if b < 0x80 {
let folded = self.ascii_fold[b as usize];
if folded != ASCII_DROP {
out.push(folded as char);
}
i += 1;
continue;
}
let c = rest[i..]
.chars()
.next()
.unwrap_or(char::REPLACEMENT_CHARACTER);
i += c.len_utf8();
let ideograph = is_chinese_char(c);
if !ideograph && self.clean_text {
if c == '\u{fffd}' {
continue;
}
match get_general_category(c) {
GeneralCategory::Control
| GeneralCategory::Format
| GeneralCategory::Surrogate
| GeneralCategory::PrivateUse => continue,
GeneralCategory::SpaceSeparator => {
out.push(' ');
continue;
}
_ => {}
}
}
let isolate = ideograph && self.handle_chinese_chars;
if isolate {
out.push(' ');
}
if is_plain_ideograph(c) || (!self.strip_accents && !self.do_lower_case) {
out.push(c);
} else if !self.strip_accents {
push_folded(&mut out, c, self.do_lower_case);
} else {
decompose_canonical(c, |d| {
if get_general_category(d) == GeneralCategory::NonspacingMark {
return;
}
if canonical_combining_class(d) != 0 {
ordered = false;
}
push_folded(&mut out, d, self.do_lower_case);
});
}
if isolate {
out.push(' ');
}
}
ordered.then_some(Cow::Owned(out))
}
#[cold]
fn fold_staged<'a>(&self, text: &'a str) -> Cow<'a, str> {
let text = if self.clean_text {
clean_text(text)
} else {
Cow::Borrowed(text)
};
let text = if self.handle_chinese_chars && text.chars().any(is_chinese_char) {
let mut s = String::with_capacity(text.len() + 8);
for c in text.chars() {
if is_chinese_char(c) {
s.push(' ');
s.push(c);
s.push(' ');
} else {
s.push(c);
}
}
Cow::Owned(s)
} else {
text
};
let text = match self.strip_accents {
true => strip_accents(text),
false => text,
};
match self.do_lower_case && super::normalizer::needs_lowercasing(&text) {
true => Cow::Owned(super::normalizer::lowercase(&text)),
false => text,
}
}
fn for_each_basic_token(text: &str, mut f: impl FnMut(&str)) {
for word in text.split_whitespace() {
let mut start = 0;
for (at, c) in word.char_indices() {
if !is_punctuation(c) {
continue;
}
if at > start {
f(&word[start..at]);
}
f(&word[at..at + c.len_utf8()]);
start = at + c.len_utf8();
}
if start < word.len() {
f(&word[start..]);
}
}
}
fn wordpiece_tokenize_into(&self, word: &str, out: &mut Vec<u32>) {
let key = word.as_bytes();
let hash = super::tokenizer::cache::ChunkCache::shard_hash(key);
if self.cache.extend_into(hash, key, out) {
return;
}
let mark = out.len();
self.segment_into(word, out);
self.cache.put(hash, key, &out[mark..]);
}
fn segment_into(&self, word: &str, out: &mut Vec<u32>) {
if word.chars().count() > self.max_word_len {
out.push(self.unk_token_id);
return;
}
let mark = out.len();
let bytes = word.as_bytes();
let mut start = 0;
while start < bytes.len() {
let trie = match (start, &self.continuation) {
(0, _) | (_, None) => &self.initial,
(_, Some(continuation)) => continuation,
};
match trie.longest_prefix(&bytes[start..]) {
Some((len, id)) => {
out.push(id);
start += len;
}
None => {
out.truncate(mark);
out.push(self.unk_token_id);
return;
}
}
}
}
}
impl WordPieceTokenizer {
pub fn encode_ordinary(&self, text: &str) -> Vec<u32> {
let prepared = self.basic_normalize(text);
let mut ids = Vec::new();
Self::for_each_basic_token(&prepared, |word| {
self.wordpiece_tokenize_into(word, &mut ids)
});
ids
}
pub fn encode_with(&self, text: &str, mode: &SpecialMode<'_>) -> Result<Vec<u32>, PolicyError> {
super::added::AddedTokens::dispatch_with_mode(&self.added, text, mode, |gap, out| {
out.extend(self.encode_ordinary(gap))
})
}
}
impl Tokenize for WordPieceTokenizer {
fn encode(&self, text: &str) -> Vec<u32> {
super::added::AddedTokens::dispatch(&self.added, text, |gap, out| {
out.extend(self.encode_ordinary(gap))
})
}
fn encode_with(&self, text: &str, mode: &SpecialMode<'_>) -> Result<Vec<u32>, PolicyError> {
self.encode_with(text, mode)
}
fn decode(&self, ids: &[u32]) -> Result<String, TokenizeError> {
self.decode(ids)
}
fn decode_with(&self, ids: &[u32], specials: SpecialDecode) -> Result<String, TokenizeError> {
WordPieceTokenizer::decode_with(self, ids, specials)
}
fn decode_lossy(&self, ids: &[u32]) -> String {
WordPieceTokenizer::decode_lossy(self, ids)
}
fn streaming_decoder(&self) -> Result<StreamingDecoder, TokenizeError> {
Ok(WordPieceTokenizer::streaming_decoder(self))
}
fn streaming_decoder_with(
&self,
specials: SpecialDecode,
) -> Result<StreamingDecoder, TokenizeError> {
Ok(WordPieceTokenizer::streaming_decoder_with(self, specials))
}
fn decode_token_bytes(&self, id: u32) -> Result<Vec<u8>, TokenizeError> {
let state = self.decode_state();
super::tokenize::token_bytes_of(state.render(), id)
}
fn decode_token(&self, id: u32) -> Result<String, TokenizeError> {
super::tokenize::token_text_of(Tokenize::decode_token_bytes(self, id)?)
}
fn vocab_size(&self) -> usize {
self.id_to_token.len()
}
}
impl WordPieceTokenizer {
pub fn token_surface(&self, id: u32) -> Option<String> {
self.id_to_token.get(id as usize).cloned()
}
fn decode_state(&self) -> DecodeState {
let separator = if self.continuation_prefix.is_empty() {
WordSeparator::EveryToken
} else {
WordSeparator::Continuation(self.continuation_prefix.clone())
};
DecodeState::new(
RenderRules::new(
Surfaces::ByIndex(Arc::clone(&self.id_to_token)),
Arc::new(rustc_hash::FxHashMap::default()),
Arc::new(self.special_decode.clone()),
ByteFallbackRule::None,
false,
false,
)
.with_word_separator(separator),
vec![DecodePost::CleanupTokenization],
)
}
pub fn streaming_decoder(&self) -> StreamingDecoder {
self.streaming_decoder_with(SpecialDecode::Skip)
}
pub fn streaming_decoder_with(&self, specials: SpecialDecode) -> StreamingDecoder {
StreamingDecoder::new(Arc::new(self.decode_state().with_special_decode(specials)))
}
pub fn decode(&self, ids: &[u32]) -> Result<String, TokenizeError> {
self.decode_with(ids, SpecialDecode::Skip)
}
pub fn decode_with(
&self,
ids: &[u32],
specials: SpecialDecode,
) -> Result<String, TokenizeError> {
self.drive(ids, specials, |id| Err(TokenizeError::InvalidTokenId(id)))
}
pub fn decode_lossy(&self, ids: &[u32]) -> String {
match self.drive(ids, SpecialDecode::Skip, |_| Ok::<(), Infallible>(())) {
Ok(text) => text,
Err(never) => match never {},
}
}
fn drive<E>(
&self,
ids: &[u32],
specials: SpecialDecode,
on_unknown: impl Fn(u32) -> Result<(), E>,
) -> Result<String, E> {
let state = self.decode_state().with_special_decode(specials);
let mut cursor = state.cursor_with_capacity(ids.len() * 4);
let mut text = cursor.feed(ids, on_unknown)?.unwrap_or_default();
text.push_str(&cursor.flush());
Ok(text)
}
}
fn is_special_token(token: &str) -> bool {
matches!(token, "[CLS]" | "[SEP]" | "[PAD]" | "[UNK]" | "[MASK]")
}
const ASCII_DROP: u8 = 0xFF;
fn ascii_fold_table(clean: bool, lower: bool) -> [u8; 128] {
let mut table = [0u8; 128];
for (b, slot) in table.iter_mut().enumerate() {
let mut c = b as u8;
if clean {
if matches!(c, b'\t' | b'\n' | b'\r') {
c = b' ';
} else if c < 0x20 || c == 0x7f {
*slot = ASCII_DROP;
continue;
}
}
*slot = if lower { c.to_ascii_lowercase() } else { c };
}
table
}
#[inline]
fn is_plain_ideograph(c: char) -> bool {
let cp = c as u32;
(0x4E00..=0x9FFF).contains(&cp)
|| (0x3400..=0x4DBF).contains(&cp)
|| (0x20000..=0x2A6DF).contains(&cp)
|| (0x2A700..=0x2B73F).contains(&cp)
|| (0x2B740..=0x2B81F).contains(&cp)
|| (0x2B820..=0x2CEAF).contains(&cp)
}
#[inline]
fn push_folded(out: &mut String, c: char, lower: bool) {
match lower {
true => out.extend(c.to_lowercase()),
false => out.push(c),
}
}
fn strip_accents(text: Cow<'_, str>) -> Cow<'_, str> {
use unicode_general_category::{get_general_category, GeneralCategory};
use unicode_normalization::{is_nfd_quick, IsNormalized, UnicodeNormalization};
if text.is_ascii() {
return text;
}
let unchanged = is_nfd_quick(text.chars()) == IsNormalized::Yes
&& !text
.chars()
.any(|c| get_general_category(c) == GeneralCategory::NonspacingMark);
if unchanged {
return text;
}
Cow::Owned(
text.nfd()
.filter(|c| get_general_category(*c) != GeneralCategory::NonspacingMark)
.collect(),
)
}
fn clean_text(text: &str) -> Cow<'_, str> {
use unicode_general_category::{get_general_category, GeneralCategory};
fn touched(c: char) -> bool {
if c == '\0' || c == '\u{fffd}' {
return true;
}
if matches!(c, '\t' | '\n' | '\r') {
return true; }
matches!(
get_general_category(c),
GeneralCategory::Control
| GeneralCategory::Format
| GeneralCategory::Surrogate
| GeneralCategory::PrivateUse
| GeneralCategory::SpaceSeparator
)
}
if !text.chars().any(touched) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
for c in text.chars() {
if c == '\0' || c == '\u{fffd}' {
continue;
}
let is_keepable_ws = matches!(c, '\t' | '\n' | '\r');
if !is_keepable_ws {
match get_general_category(c) {
GeneralCategory::Control
| GeneralCategory::Format
| GeneralCategory::Surrogate
| GeneralCategory::PrivateUse => continue,
_ => {}
}
}
if c == ' ' || is_keepable_ws || get_general_category(c) == GeneralCategory::SpaceSeparator
{
out.push(' ');
} else {
out.push(c);
}
}
Cow::Owned(out)
}
fn is_chinese_char(c: char) -> bool {
let cp = c as u32;
(0x4E00..=0x9FFF).contains(&cp)
|| (0x3400..=0x4DBF).contains(&cp)
|| (0x20000..=0x2A6DF).contains(&cp)
|| (0x2A700..=0x2B73F).contains(&cp)
|| (0x2B740..=0x2B81F).contains(&cp)
|| (0x2B820..=0x2CEAF).contains(&cp)
|| (0xF900..=0xFAFF).contains(&cp)
|| (0x2F800..=0x2FA1F).contains(&cp)
}
fn is_punctuation(c: char) -> bool {
matches!(c, '\x21'..='\x2F' | '\x3A'..='\x40' | '\x5B'..='\x60' | '\x7B'..='\x7E')
|| c.is_ascii_punctuation()
|| {
let cat = unicode_general_category::get_general_category(c);
matches!(
cat,
unicode_general_category::GeneralCategory::ConnectorPunctuation
| unicode_general_category::GeneralCategory::DashPunctuation
| unicode_general_category::GeneralCategory::ClosePunctuation
| unicode_general_category::GeneralCategory::FinalPunctuation
| unicode_general_category::GeneralCategory::InitialPunctuation
| unicode_general_category::GeneralCategory::OtherPunctuation
| unicode_general_category::GeneralCategory::OpenPunctuation
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn clean_text_keeps_unassigned_codepoints() {
assert_eq!(clean_text("a\u{05ff}b"), "a\u{05ff}b");
assert_eq!(clean_text("a\u{0378}b"), "a\u{0378}b");
assert_eq!(clean_text("a\u{0007}b"), "ab");
assert_eq!(clean_text("a\u{00ad}b"), "ab");
assert_eq!(clean_text("a\u{e000}b"), "ab");
assert_eq!(clean_text("a\u{fffd}b"), "ab");
assert_eq!(clean_text("a\tb\u{00a0}c"), "a b c");
}
fn make_tokenizer() -> WordPieceTokenizer {
let vocab = vec![
"[PAD]".to_string(), "[UNK]".to_string(), "[CLS]".to_string(), "[SEP]".to_string(), "hello".to_string(), "world".to_string(), "##ing".to_string(), "##s".to_string(), "un".to_string(), "##know".to_string(), "##n".to_string(), ",".to_string(), "the".to_string(), "a".to_string(), ];
WordPieceTokenizer::new(vocab, 1, 200, true)
}
#[test]
fn test_encode_basic() {
let tok = make_tokenizer();
let ids = tok.encode("hello world");
assert_eq!(ids, vec![4, 5]);
}
#[test]
fn test_encode_subwords() {
let tok = make_tokenizer();
let ids = tok.encode("unknown");
assert_eq!(ids, vec![8, 9, 10]);
}
#[test]
fn test_encode_punctuation() {
let tok = make_tokenizer();
let ids = tok.encode("hello, world");
assert_eq!(ids, vec![4, 11, 5]);
}
#[test]
fn test_decode_basic() {
let tok = make_tokenizer();
let text = tok.decode(&[4, 5]).unwrap();
assert_eq!(text, "hello world");
}
#[test]
fn test_decode_subwords() {
let tok = make_tokenizer();
let text = tok.decode(&[8, 9, 10]).unwrap();
assert_eq!(text, "unknown");
}
#[test]
fn test_decode_skips_special() {
let tok = make_tokenizer();
let text = tok.decode(&[2, 4, 5, 3]).unwrap();
assert_eq!(text, "hello world");
}
#[test]
fn standard_bracket_named_vocab_decodes_unchanged() {
let vocab = vec![
"[PAD]".to_string(), "[UNK]".to_string(), "[CLS]".to_string(), "[SEP]".to_string(), "[MASK]".to_string(), "hello".to_string(), "world".to_string(), "##ing".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 1, 200, true);
assert_eq!(tok.decode(&[2, 5, 6, 3]).unwrap(), "hello world");
assert_eq!(tok.decode(&[0, 1, 4]).unwrap(), "");
assert_eq!(tok.decode(&[5, 6, 7]).unwrap(), "hello worlding");
}
#[test]
fn declared_special_ids_join_the_resolved_names() {
let tok = make_tokenizer().with_special_decode_ids([13u32].into_iter().collect());
assert_eq!(tok.decode(&[2, 4, 13, 5, 3]).unwrap(), "hello world");
}
#[test]
fn unused_spelled_content_token_survives_decode() {
let vocab = vec![
"[UNK]".to_string(), "[unused7]".to_string(), "hello".to_string(), "world".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 0, 200, true);
assert_eq!(tok.decode(&[2, 1, 3]).unwrap(), "hello [unused7] world");
}
#[test]
fn declared_special_unused_token_is_dropped() {
let vocab = vec![
"[UNK]".to_string(), "[unused7]".to_string(), "hello".to_string(), "world".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 0, 200, true)
.with_special_decode_ids([1u32].into_iter().collect());
assert_eq!(tok.decode(&[2, 1, 3]).unwrap(), "hello world");
}
#[test]
fn test_vocab_size() {
let tok = make_tokenizer();
assert_eq!(tok.vocab_size(), 14);
}
#[test]
fn test_special_token_ids() {
let tok = make_tokenizer();
assert_eq!(tok.cls_token_id(), Some(2));
assert_eq!(tok.sep_token_id(), Some(3));
assert_eq!(tok.pad_token_id(), Some(0));
assert_eq!(tok.unk_token_id(), 1);
}
#[test]
fn clean_text_strips_control_and_format_chars() {
assert_eq!(
clean_text("a\u{200b}b\u{200c}\u{feff}c\0\u{fffd}d\te"),
"abcd e"
);
assert_eq!(clean_text("plain text"), "plain text");
}
#[test]
fn test_unknown_word() {
let tok = make_tokenizer();
assert_eq!(tok.encode("xyz"), vec![1]);
}
#[test]
fn test_handle_chinese_chars() {
let tok = make_tokenizer();
assert_eq!(tok.encode("hello世界world"), vec![4, 1, 1, 5]);
}
#[test]
fn test_lowercase() {
let tok = make_tokenizer();
let ids = tok.encode("Hello WORLD");
assert_eq!(ids, vec![4, 5]);
}
#[test]
fn test_case_sensitive() {
let vocab = vec![
"[UNK]".to_string(), "Hello".to_string(), "hello".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 0, 200, false);
let ids = tok.encode("Hello");
assert_eq!(ids, vec![1]);
let ids = tok.encode("hello");
assert_eq!(ids, vec![2]);
}
fn accent_vocab() -> Vec<String> {
vec![
"[UNK]".to_string(), "cafe".to_string(), "café".to_string(), "Cafe".to_string(), "Café".to_string(), "naive".to_string(), "naïve".to_string(), ]
}
#[test]
fn strip_accents_defaults_to_lowercasing() {
let tok = WordPieceTokenizer::new(accent_vocab(), 0, 200, true);
assert_eq!(tok.encode("Café"), vec![1]);
assert_eq!(tok.encode("café"), vec![1]);
assert_eq!(tok.encode("naïve"), vec![5]);
let cased = WordPieceTokenizer::new(accent_vocab(), 0, 200, false);
assert_eq!(cased.encode("Café"), vec![4]);
assert_eq!(cased.encode("naïve"), vec![6]);
}
#[test]
fn lowercasing_does_not_force_accent_stripping() {
let tok = WordPieceTokenizer::new(accent_vocab(), 0, 200, true).with_strip_accents(false);
assert_eq!(tok.encode("Café"), vec![2]);
assert_eq!(tok.encode("café"), vec![2]);
assert_eq!(tok.encode("naïve"), vec![6]);
assert_eq!(tok.encode("Cafe"), vec![1]);
}
#[test]
fn accent_stripping_does_not_force_lowercasing() {
let tok = WordPieceTokenizer::new(accent_vocab(), 0, 200, false).with_strip_accents(true);
assert_eq!(tok.encode("Café"), vec![3]);
assert_eq!(tok.encode("café"), vec![1]);
assert_eq!(tok.encode("naïve"), vec![5]);
}
#[test]
fn test_decode_invalid_id() {
let tok = make_tokenizer();
let result = tok.decode(&[999]);
assert!(result.is_err());
}
#[test]
fn decode_lossy_skips_an_unknown_id_that_decode_reports() {
let tok = make_tokenizer();
assert!(tok.decode(&[4, 999, 5]).is_err());
assert_eq!(tok.decode_lossy(&[4, 999, 5]), "hello world");
assert_eq!(tok.decode_lossy(&[2, 4, 5, 3]), "hello world");
}
fn stream_vocab() -> Vec<String> {
[
"[PAD]", "[UNK]", "[CLS]", "[SEP]", "hello", "world", "##ing", ",", ".", "a", "", "##.", "?", ]
.iter()
.map(|s| (*s).to_string())
.collect()
}
fn stream_tokenizer() -> WordPieceTokenizer {
WordPieceTokenizer::new(stream_vocab(), 1, 200, true)
}
fn bare_stream_tokenizer() -> WordPieceTokenizer {
WordPieceTokenizer::with_options(stream_vocab(), 1, 200, true, true, true, String::new())
}
fn drive_strict(tokenizer: &WordPieceTokenizer, ids: &[u32], chunk: usize) -> String {
let mut decoder = tokenizer.streaming_decoder();
let mut out = String::new();
for group in ids.chunks(chunk.max(1)) {
if let Some(text) = decoder.add_tokens(group).expect("ids are all known") {
out.push_str(&text);
}
}
out.push_str(&decoder.flush());
out
}
fn drive_lossy(tokenizer: &WordPieceTokenizer, ids: &[u32]) -> String {
let mut decoder = tokenizer.streaming_decoder();
let mut out = String::new();
for &id in ids {
if let Some(text) = decoder.add_token_lossy(id) {
out.push_str(&text);
}
}
out.push_str(&decoder.flush());
out
}
const STREAM_IDS: &[&[u32]] = &[
&[],
&[4, 5],
&[4, 6],
&[2, 4, 6, 7, 5, 3],
&[2, 4, 5, 8],
&[4, 7, 5, 12],
&[9, 10, 8],
&[9, 10, 10, 8],
&[9, 10, 11],
&[10, 4],
&[2, 3, 0],
];
#[test]
fn stream_matches_decode_at_every_chunk_size() {
for tokenizer in [stream_tokenizer(), bare_stream_tokenizer()] {
for ids in STREAM_IDS {
let expected = tokenizer.decode(ids).expect("ids are all known");
for chunk in 1..=ids.len().max(1) {
assert_eq!(
drive_strict(&tokenizer, ids, chunk),
expected,
"ids: {ids:?}, chunk: {chunk}"
);
}
assert_eq!(
drive_lossy(&tokenizer, ids),
tokenizer.decode_lossy(ids),
"ids: {ids:?}"
);
assert_eq!(tokenizer.decode_lossy(ids), expected, "ids: {ids:?}");
}
}
}
#[test]
fn a_leading_special_does_not_emit_a_separator() {
let tok = stream_tokenizer();
let mut decoder = tok.streaming_decoder();
assert_eq!(decoder.add_token(2).expect("[CLS] is known"), None);
assert_eq!(
decoder.add_token(4).expect("known id"),
Some("hello".to_string())
);
assert_eq!(drive_strict(&tok, &[2, 4, 5, 3], 1), "hello world");
assert_eq!(tok.decode(&[2, 4, 5, 3]).unwrap(), "hello world");
}
#[test]
fn punctuation_straddling_a_chunk_boundary_matches_decode() {
let tok = stream_tokenizer();
let ids = [9u32, 10, 11]; assert_eq!(tok.decode(&ids).unwrap(), "a.");
let mut decoder = tok.streaming_decoder();
let mut streamed = String::new();
for id in ids {
streamed.push_str(&decoder.add_token(id).expect("known id").unwrap_or_default());
}
streamed.push_str(&decoder.flush());
assert_eq!(streamed, "a.");
let ids = [4u32, 7];
assert_eq!(tok.decode(&ids).unwrap(), "hello,");
let mut decoder = tok.streaming_decoder();
let mut streamed = String::new();
for id in ids {
streamed.push_str(&decoder.add_token(id).expect("known id").unwrap_or_default());
}
streamed.push_str(&decoder.flush());
assert_eq!(streamed, "hello,");
}
#[test]
fn the_whole_trailing_space_run_is_held() {
let tok = stream_tokenizer();
let ids = [9u32, 10, 8];
assert_eq!(tok.decode(&ids).unwrap(), "a .");
for chunk in 1..=ids.len() {
assert_eq!(drive_strict(&tok, &ids, chunk), "a .", "chunk: {chunk}");
}
let ids = [9u32, 10, 10, 8];
assert_eq!(tok.decode(&ids).unwrap(), "a .");
for chunk in 1..=ids.len() {
assert_eq!(drive_strict(&tok, &ids, chunk), "a .", "chunk: {chunk}");
}
}
#[test]
fn a_held_space_run_survives_the_flush() {
let tok = stream_tokenizer();
let ids = [9u32, 10];
assert_eq!(tok.decode(&ids).unwrap(), "a ");
let mut decoder = tok.streaming_decoder();
let emitted = decoder
.add_tokens(&ids)
.expect("known ids")
.unwrap_or_default();
assert_eq!(emitted, "a", "the trailing space is still held");
assert_eq!(decoder.flush(), " ");
}
#[test]
fn the_prefixless_path_streams_like_decode() {
let tok = bare_stream_tokenizer();
assert_eq!(tok.decode(&[9, 11]).unwrap(), "a ##.");
assert_eq!(tok.decode(&[4, 6]).unwrap(), "hello ##ing");
assert_eq!(tok.decode(&[9, 10, 10, 8]).unwrap(), "a .");
for ids in [vec![9u32, 11], vec![4, 6], vec![9, 10, 10, 8]] {
let expected = tok.decode(&ids).expect("known ids");
for chunk in 1..=ids.len() {
assert_eq!(drive_strict(&tok, &ids, chunk), expected, "chunk: {chunk}");
}
}
}
proptest! {
#[test]
fn prop_chunking_matches_decode(
ids in prop::collection::vec(0u32..13, 0..32),
chunk in 1usize..8,
) {
let tokenizer = stream_tokenizer();
let expected = tokenizer.decode(&ids).expect("every id is in range");
prop_assert_eq!(drive_strict(&tokenizer, &ids, 1), expected.clone());
prop_assert_eq!(drive_strict(&tokenizer, &ids, chunk), expected);
}
#[test]
fn prop_chunking_matches_decode_without_prefix(
ids in prop::collection::vec(0u32..13, 0..32),
chunk in 1usize..8,
) {
let tokenizer = bare_stream_tokenizer();
let expected = tokenizer.decode(&ids).expect("every id is in range");
prop_assert_eq!(drive_strict(&tokenizer, &ids, 1), expected.clone());
prop_assert_eq!(drive_strict(&tokenizer, &ids, chunk), expected);
}
#[test]
fn prop_arbitrary_ids_match_decode_lossy(
ids in prop::collection::vec(0u32..40, 0..48),
) {
let tokenizer = stream_tokenizer();
prop_assert_eq!(drive_lossy(&tokenizer, &ids), tokenizer.decode_lossy(&ids));
}
#[test]
fn prop_reset_matches_a_fresh_decoder(
dirty in prop::collection::vec(0u32..40, 0..16),
ids in prop::collection::vec(0u32..40, 0..32),
) {
let tokenizer = stream_tokenizer();
let mut reused = tokenizer.streaming_decoder();
reused.add_tokens_lossy(&dirty);
reused.reset();
prop_assert!(!reused.has_pending());
prop_assert_eq!(reused.pending_bytes(), 0);
let mut fresh = tokenizer.streaming_decoder();
let mut from_reused = String::new();
let mut from_fresh = String::new();
for &id in &ids {
let a = reused.add_token_lossy(id);
let b = fresh.add_token_lossy(id);
prop_assert_eq!(&a, &b);
prop_assert_eq!(reused.pending_bytes(), fresh.pending_bytes());
from_reused.push_str(&a.unwrap_or_default());
from_fresh.push_str(&b.unwrap_or_default());
}
from_reused.push_str(&reused.flush());
from_fresh.push_str(&fresh.flush());
prop_assert_eq!(from_reused, from_fresh);
}
}
#[test]
fn decode_token_bytes_separates_content_skip_and_unknown() {
let tok = make_tokenizer();
assert_eq!(tok.decode_token_bytes(4).unwrap(), b"hello".to_vec());
assert_eq!(tok.decode_token(4).unwrap(), "hello");
assert_eq!(tok.decode_token(6).unwrap(), "ing");
for skipped in [2, 3] {
assert_eq!(tok.decode_token_bytes(skipped).unwrap(), Vec::<u8>::new());
assert_eq!(tok.decode_token(skipped).unwrap(), "");
}
assert!(matches!(
tok.decode_token_bytes(999),
Err(TokenizeError::InvalidTokenId(999))
));
assert!(matches!(
tok.decode_token(999),
Err(TokenizeError::InvalidTokenId(999))
));
}
#[test]
fn concatenated_token_bytes_equal_the_decoded_sequence_without_separators() {
let tok = make_tokenizer();
let glued = [2, 8, 9, 10, 3];
let joined: Vec<u8> = glued
.iter()
.flat_map(|&id| tok.decode_token_bytes(id).expect("every id is known"))
.collect();
assert_eq!(joined, tok.decode_lossy(&glued).into_bytes());
assert_eq!(String::from_utf8(joined).unwrap(), "unknown");
let separated = [4, 5];
let joined: String = separated
.iter()
.map(|&id| tok.decode_token(id).expect("every id is known"))
.collect();
assert_eq!(joined, "helloworld");
assert_eq!(tok.decode_lossy(&separated), "hello world");
}
#[test]
fn trait_decode_lossy_and_streaming_decoder_match_the_inherent_pair() {
let tok = make_tokenizer();
let ids = [2, 4, 999, 5, 3];
assert_eq!(Tokenize::decode_lossy(&tok, &ids), "hello world");
assert_eq!(
Tokenize::decode_lossy(&tok, &ids),
WordPieceTokenizer::decode_lossy(&tok, &ids)
);
let mut streamed = Tokenize::streaming_decoder(&tok).expect("WordPiece always streams");
let mut out = streamed.add_tokens_lossy(&ids).unwrap_or_default();
out.push_str(&streamed.flush());
assert_eq!(out, "hello world");
}
}
#[cfg(test)]
mod fold_order_tests {
use super::*;
#[test]
fn accent_stripping_can_create_a_split_point() {
let vocab = vec![
"[UNK]".to_string(), "a".to_string(), "=".to_string(), "b".to_string(), "##=".to_string(), "##b".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 0, 200, true);
assert_eq!(
tok.encode("a\u{2260}b"),
vec![1, 2, 3],
"the `=` that accent stripping exposed must split the word, not \
continue it"
);
}
#[test]
fn a_compatibility_ideograph_still_decomposes() {
let vocab = vec![
"[UNK]".to_string(), "a".to_string(), "\u{5140}".to_string(), "\u{fa0c}".to_string(), ];
let tok = WordPieceTokenizer::new(vocab, 0, 200, true);
assert_eq!(
tok.encode("a\u{fa0c}"),
vec![1, 2],
"the compatibility ideograph must decompose, and be its own word"
);
}
}