use crate::tokenizer::bpe::BpeTokenizer;
use std::collections::HashMap;
pub(crate) fn build_byte_decoder() -> HashMap<char, u8> {
let byte_encoder = bytes_to_unicode();
let mut byte_decoder: HashMap<char, u8> = HashMap::new();
for (byte_val, &ch) in byte_encoder.iter().enumerate() {
byte_decoder.insert(ch, byte_val as u8);
}
byte_decoder
}
fn append_token_bytes(
tokenizer: &BpeTokenizer,
id: u32,
byte_decoder: &HashMap<char, u8>,
out: &mut Vec<u8>,
) {
tokenizer.append_token_bytes(id, byte_decoder, out);
}
pub(crate) fn decode_tokens(tokenizer: &BpeTokenizer, ids: &[u32]) -> String {
let byte_decoder = build_byte_decoder();
let mut bytes = Vec::new();
for &id in ids {
append_token_bytes(tokenizer, id, &byte_decoder, &mut bytes);
}
String::from_utf8_lossy(&bytes).to_string()
}
const DETOK_RETAINED_BYTE_CAPACITY: usize = 8;
pub(crate) struct IncrementalDetokenizer {
byte_decoder: HashMap<char, u8>,
pending: Vec<u8>,
}
impl IncrementalDetokenizer {
pub(crate) fn new() -> Self {
Self {
byte_decoder: build_byte_decoder(),
pending: Vec::with_capacity(DETOK_RETAINED_BYTE_CAPACITY),
}
}
pub(crate) fn push(&mut self, tokenizer: &BpeTokenizer, id: u32) -> String {
append_token_bytes(tokenizer, id, &self.byte_decoder, &mut self.pending);
self.flush_complete()
}
fn flush_complete(&mut self) -> String {
let mut out = String::new();
let mut consumed = 0usize;
loop {
match std::str::from_utf8(&self.pending[consumed..]) {
Ok(s) => {
out.push_str(s);
consumed = self.pending.len();
break;
}
Err(e) => {
let valid = e.valid_up_to();
if valid > 0 {
let end = consumed + valid;
out.push_str(
String::from_utf8_lossy(&self.pending[consumed..end]).as_ref(),
);
consumed = end;
}
match e.error_len() {
None => break,
Some(len) => {
out.push('\u{FFFD}');
consumed += len;
}
}
}
}
}
if consumed > 0 {
self.pending.drain(..consumed);
}
self.compact_pending_capacity();
out
}
pub(crate) fn finish(&mut self) -> String {
if self.pending.is_empty() {
return String::new();
}
let tail = String::from_utf8_lossy(&self.pending).into_owned();
self.pending.clear();
self.compact_pending_capacity();
tail
}
fn compact_pending_capacity(&mut self) {
if self.pending.capacity() > DETOK_RETAINED_BYTE_CAPACITY
&& self.pending.len() <= DETOK_RETAINED_BYTE_CAPACITY
{
let mut compact = Vec::with_capacity(DETOK_RETAINED_BYTE_CAPACITY);
compact.extend_from_slice(&self.pending);
self.pending = compact;
}
}
#[cfg(test)]
fn retained_byte_len(&self) -> usize {
self.pending.len()
}
#[cfg(test)]
fn retained_byte_capacity(&self) -> usize {
self.pending.capacity()
}
}
pub fn bytes_to_unicode() -> Vec<char> {
let mut bs = Vec::new();
bs.extend(33u16..=126);
bs.extend(161u16..=172);
bs.extend(174u16..=255);
let mut cs = bs.clone();
let mut n = 0u16;
for b in 0u16..=255u16 {
if !bs.contains(&b) {
bs.push(b);
cs.push(256 + n);
n += 1;
}
}
let mut table = vec!['\0'; 256];
for (b, c) in bs.into_iter().zip(cs) {
table[b as usize] =
char::from_u32(c as u32).expect("invariant: byte-to-unicode codepoint is valid");
}
table
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tokenizer::bpe::BpeTokenizer;
use std::collections::HashMap;
#[test]
fn incremental_flush_reconstructs_split_codepoints() {
let chunks: &[&[u8]] = &[&[0xE5, 0xA5], &[0xBD], &[0xF0, 0x9F], &[0x98, 0x80], b"ok"];
let mut d = IncrementalDetokenizer::new();
let mut out = String::new();
for c in chunks {
d.pending.extend_from_slice(c);
out.push_str(&d.flush_complete());
}
out.push_str(&d.finish());
assert_eq!(out, "好😀ok");
assert_eq!(d.retained_byte_len(), 0);
}
#[test]
fn incremental_flush_truncated_mid_codepoint_matches_lossy() {
let mut d = IncrementalDetokenizer::new();
let mut out = String::new();
d.pending.extend_from_slice(&[b'h', b'i', 0xE5, 0xA5]);
out.push_str(&d.flush_complete());
out.push_str(&d.finish());
assert_eq!(out, String::from_utf8_lossy(&[b'h', b'i', 0xE5, 0xA5]));
assert_eq!(d.retained_byte_len(), 0);
}
#[test]
fn incremental_flush_invalid_byte_does_not_stall_stream() {
let mut d = IncrementalDetokenizer::new();
d.pending.extend_from_slice(&[0x80]);
let first = d.flush_complete();
assert_eq!(first, "\u{FFFD}", "invalid byte must flush immediately");
d.pending.extend_from_slice(b"A");
let second = d.flush_complete();
assert_eq!(second, "A", "valid byte after invalid one must flush");
assert_eq!(
format!("{first}{second}"),
String::from_utf8_lossy(&[0x80, b'A'])
);
assert_eq!(d.retained_byte_len(), 0);
}
#[test]
fn incremental_flush_invalid_between_valid_matches_lossy() {
let raw = [b'h', b'i', 0xFF, b'y', b'o'];
let mut d = IncrementalDetokenizer::new();
d.pending.extend_from_slice(&raw);
let out = d.flush_complete();
assert_eq!(out, String::from_utf8_lossy(&raw));
assert_eq!(d.retained_byte_len(), 0);
}
#[test]
fn incremental_flush_ascii_is_exact_per_chunk() {
let mut d = IncrementalDetokenizer::new();
let mut out = String::new();
for c in [b"He".as_slice(), b"llo", b"!"] {
d.pending.extend_from_slice(c);
out.push_str(&d.flush_complete());
}
out.push_str(&d.finish());
assert_eq!(out, "Hello!");
assert_eq!(d.retained_byte_len(), 0);
}
#[test]
fn incremental_detokenizer_retention_bounded_for_long_ascii_stream() {
let byte_encoder = bytes_to_unicode();
let mut vocab = HashMap::new();
vocab.insert(byte_encoder[b'a' as usize].to_string(), 0u32);
let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, Vec::new())
.expect("synthetic BPE tokenizer builds");
let mut detok = IncrementalDetokenizer::new();
let mut out = String::new();
for _ in 0..8_192 {
out.push_str(&detok.push(&tokenizer, 0));
assert_eq!(detok.retained_byte_len(), 0);
assert!(
detok.retained_byte_capacity() <= DETOK_RETAINED_BYTE_CAPACITY,
"capacity must remain bounded, got {}",
detok.retained_byte_capacity()
);
}
out.push_str(&detok.finish());
assert_eq!(out.len(), 8_192);
assert_eq!(detok.retained_byte_len(), 0);
assert!(detok.retained_byte_capacity() <= DETOK_RETAINED_BYTE_CAPACITY);
}
#[test]
fn incremental_detokenizer_retention_compacts_after_oversized_single_push() {
let byte_encoder = bytes_to_unicode();
let oversized_token: String =
std::iter::repeat_n(byte_encoder[b'a' as usize], 12).collect();
let mut vocab = HashMap::new();
vocab.insert(oversized_token, 0u32);
let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, Vec::new())
.expect("synthetic BPE tokenizer builds");
let mut detok = IncrementalDetokenizer::new();
let delta = detok.push(&tokenizer, 0);
assert_eq!(delta, "a".repeat(12));
assert_eq!(detok.retained_byte_len(), 0);
assert!(
detok.retained_byte_capacity() <= DETOK_RETAINED_BYTE_CAPACITY,
"a single push whose decoded bytes (12) exceed DETOK_RETAINED_BYTE_CAPACITY \
(8) must still leave the retained allocation compacted back down, got \
capacity {}",
detok.retained_byte_capacity()
);
}
#[test]
fn incremental_detokenizer_utf8_parity_matches_full_decode_for_split_stream() {
let chunks: &[&[u8]] = &[
b"H",
&[0xE5],
&[0xA5, 0xBD],
b" ",
&[0xF0, 0x9F],
&[0x98],
&[0x80],
b" ",
b"e",
&[0xCC],
&[0x81],
b" ",
&[0xF0, 0x9F, 0x91],
&[0xA9, 0xE2],
&[0x80, 0x8D],
&[0xF0, 0x9F, 0x92],
&[0xBB],
];
let byte_encoder = bytes_to_unicode();
let mut vocab = HashMap::new();
let mut ids = Vec::with_capacity(chunks.len());
let mut next_id = 0u32;
for chunk in chunks {
let token_str: String = chunk.iter().map(|&b| byte_encoder[b as usize]).collect();
let id = *vocab.entry(token_str).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
ids.push(id);
}
let tokenizer = BpeTokenizer::from_vocab_and_merges(vocab, Vec::new())
.expect("synthetic BPE tokenizer builds");
let reference = decode_tokens(&tokenizer, &ids);
assert_eq!(
reference,
"H\u{597D} \u{1F600} e\u{301} \u{1F469}\u{200D}\u{1F4BB}"
);
let mut detok = IncrementalDetokenizer::new();
let mut streamed = String::new();
for &id in &ids {
streamed.push_str(&detok.push(&tokenizer, id));
assert!(detok.retained_byte_len() <= 3);
}
streamed.push_str(&detok.finish());
assert_eq!(streamed, reference);
assert_eq!(detok.retained_byte_len(), 0);
assert!(detok.retained_byte_capacity() <= DETOK_RETAINED_BYTE_CAPACITY);
}
}