use super::render::{ByteFallbackRule, Lead, RenderRules, Rendered, Surfaces};
use super::utf8::{InvalidUtf8, Utf8Buffer};
use crate::core::decoder::wordpiece_cleanup;
use crate::core::policy::SpecialDecode;
use rustc_hash::{FxHashMap, FxHashSet};
use std::borrow::Borrow;
use std::sync::Arc;
pub(crate) enum DecodePost {
MetaspaceToSpace,
StripLeadingSpace,
CleanupTokenization,
}
fn cleanup_tokenization(s: &str) -> String {
s.replace(" .", ".")
.replace(" ?", "?")
.replace(" !", "!")
.replace(" ,", ",")
}
pub(crate) struct DecodeState {
render: RenderRules,
post: Vec<DecodePost>,
}
impl DecodeState {
pub(crate) fn new(render: RenderRules, post: Vec<DecodePost>) -> Self {
Self { render, post }
}
pub(crate) fn for_piece_vocab(
id_to_token: &Arc<Vec<String>>,
skipped: FxHashSet<u32>,
add_prefix_space: bool,
) -> Self {
let post = if add_prefix_space {
vec![DecodePost::StripLeadingSpace]
} else {
Vec::new()
};
Self::new(
RenderRules::new(
Surfaces::ByIndex(Arc::clone(id_to_token)),
Arc::new(FxHashMap::default()),
Arc::new(skipped),
ByteFallbackRule::ParseSurface,
false,
true,
),
post,
)
}
pub(crate) fn with_special_decode(mut self, specials: SpecialDecode) -> Self {
self.render = match specials {
SpecialDecode::Skip => self.render,
SpecialDecode::Render => self.render.rendering_specials(),
};
self
}
pub(crate) fn render(&self) -> &RenderRules {
&self.render
}
pub(crate) fn cursor_with_capacity(&self, capacity: usize) -> DecodeCursor<&Self> {
DecodeCursor::with_capacity(self, capacity)
}
fn postprocess(&self, text: String, at_start: &mut bool, held_spaces: &mut String) -> String {
if text.is_empty() {
return text;
}
let text = self.post.iter().fold(text, |text, op| match op {
DecodePost::MetaspaceToSpace => text.replace('\u{2581}', " "),
DecodePost::StripLeadingSpace if *at_start => match text.strip_prefix(' ') {
Some(rest) => rest.to_string(),
None => text,
},
DecodePost::StripLeadingSpace => text,
DecodePost::CleanupTokenization => {
let mut combined = std::mem::take(held_spaces);
combined.push_str(&text);
let mut cleaned = cleanup_tokenization(&combined);
let kept = cleaned.trim_end_matches(' ').len();
held_spaces.push_str(&cleaned[kept..]);
cleaned.truncate(kept);
cleaned
}
});
*at_start = false;
text
}
}
pub(crate) struct DecodeCursor<S> {
state: S,
bytes: Utf8Buffer,
at_start: bool,
rendered_a_token: bool,
held_spaces: String,
byte_run: Vec<u8>,
}
#[inline]
fn end_byte_run(run: &mut Vec<u8>, buffer: &mut Utf8Buffer) {
if run.is_empty() {
return;
}
match std::str::from_utf8(run) {
Ok(text) => buffer.push(text.as_bytes()),
Err(_) => {
for _ in 0..run.len() {
buffer.push("\u{fffd}".as_bytes());
}
}
}
run.clear();
}
impl<S: Borrow<DecodeState>> DecodeCursor<S> {
pub(crate) fn new(state: S) -> Self {
Self {
state,
bytes: Utf8Buffer::new(),
at_start: true,
rendered_a_token: false,
held_spaces: String::new(),
byte_run: Vec::new(),
}
}
pub(crate) fn with_capacity(state: S, capacity: usize) -> Self {
Self {
state,
bytes: Utf8Buffer::with_capacity(capacity),
at_start: true,
rendered_a_token: false,
held_spaces: String::new(),
byte_run: Vec::new(),
}
}
fn render_into<E>(
&mut self,
ids: &[u32],
on_unknown: impl Fn(u32) -> Result<(), E>,
) -> Result<(), E> {
{
let rules = self.state.borrow().render();
if let Some(map) = rules.plain_by_id() {
for &id in ids {
if rules.skips(id) {
continue;
}
match map.get(&id) {
Some(bytes) => {
self.bytes.push(bytes);
self.rendered_a_token = true;
}
None => match rules.special_surface(id) {
Some(text) => {
self.bytes.push(text.as_bytes());
self.rendered_a_token = true;
}
None => on_unknown(id)?,
},
}
}
return Ok(());
}
}
self.render_into_general(ids, on_unknown)
}
fn render_into_general<E>(
&mut self,
ids: &[u32],
on_unknown: impl Fn(u32) -> Result<(), E>,
) -> Result<(), E> {
let rules = self.state.borrow().render();
for &id in ids {
match rules.render(id) {
Rendered::Skipped => {}
Rendered::Bytes { lead, bytes } => {
end_byte_run(&mut self.byte_run, &mut self.bytes);
let separated = match lead {
Lead::None => false,
Lead::SpaceUnlessFirst => self.rendered_a_token,
};
if rules.unit_cleanup() {
let text = String::from_utf8_lossy(&bytes);
let mut unit = String::with_capacity(text.len() + 1);
if separated {
unit.push(' ');
}
unit.push_str(&text);
self.bytes.push(wordpiece_cleanup(&unit).as_bytes());
} else {
if separated {
self.bytes.push(b" ");
}
self.bytes.push(&bytes);
}
self.rendered_a_token = true;
}
Rendered::RunByte(byte) => {
self.byte_run.push(byte);
self.rendered_a_token = true;
}
Rendered::Unknown => on_unknown(id)?,
}
}
Ok(())
}
pub(crate) fn feed<E>(
&mut self,
ids: &[u32],
on_unknown: impl Fn(u32) -> Result<(), E>,
) -> Result<Option<String>, E> {
self.render_into(ids, on_unknown)?;
match self.bytes.take_complete() {
Some(text) => Ok(Some(self.postprocess(text))),
None => Ok(None),
}
}
fn postprocess(&mut self, text: String) -> String {
let state = self.state.borrow();
state.postprocess(text, &mut self.at_start, &mut self.held_spaces)
}
pub(crate) fn feed_strict<E>(
&mut self,
ids: &[u32],
on_unknown: impl Fn(u32) -> Result<(), E>,
on_invalid_utf8: impl Fn() -> E,
) -> Result<Option<String>, E> {
self.render_into(ids, on_unknown)?;
match self.bytes.take_complete_strict() {
Ok(Some(text)) => Ok(Some(self.postprocess(text))),
Ok(None) => Ok(None),
Err(InvalidUtf8) => Err(on_invalid_utf8()),
}
}
pub(crate) fn flush(&mut self) -> String {
end_byte_run(&mut self.byte_run, &mut self.bytes);
let text = self.bytes.flush();
let mut text = self.postprocess(text);
text.push_str(&self.take_held_spaces());
text
}
fn take_held_spaces(&mut self) -> String {
std::mem::take(&mut self.held_spaces)
}
pub(crate) fn finish_strict<E>(
&mut self,
on_invalid_utf8: impl Fn() -> E,
) -> Result<String, E> {
end_byte_run(&mut self.byte_run, &mut self.bytes);
match self.bytes.flush_strict() {
Ok(text) => {
let mut text = self.postprocess(text);
text.push_str(&self.take_held_spaces());
Ok(text)
}
Err(InvalidUtf8) => Err(on_invalid_utf8()),
}
}
pub(crate) fn reset(&mut self) {
self.bytes.clear();
self.at_start = true;
self.rendered_a_token = false;
self.held_spaces.clear();
self.byte_run.clear();
}
pub(crate) fn has_pending(&self) -> bool {
self.bytes.has_pending() || !self.byte_run.is_empty() || !self.held_spaces.is_empty()
}
pub(crate) fn pending_bytes(&self) -> usize {
self.bytes.pending_len() + self.byte_run.len() + self.held_spaces.len()
}
}
#[cfg(test)]
mod tests {
use super::super::render::{ByteFallbackRule, RenderRules, Surfaces};
use super::{DecodeCursor, DecodeState};
use crate::core::tokenizer::Tokenizer;
use rustc_hash::{FxHashMap, FxHashSet};
use std::cell::RefCell;
use std::convert::Infallible;
use std::sync::Arc;
fn make_test_tokenizer() -> Tokenizer {
let mut encoder = FxHashMap::default();
for b in 0u8..=255 {
encoder.insert(vec![b], b as u32);
}
encoder.insert("Hello".as_bytes().to_vec(), 256);
encoder.insert("世界".as_bytes().to_vec(), 257);
Tokenizer::new(encoder, FxHashMap::default(), r".").expect("the test pattern compiles")
}
fn drive(tokenizer: &Tokenizer, ids: &[u32], chunk: usize) -> String {
let state = tokenizer.decode_state();
let mut cursor = DecodeCursor::new(&state);
let mut out = String::new();
for group in ids.chunks(chunk.max(1)) {
let emitted = match cursor.feed(group, |_| Ok::<(), Infallible>(())) {
Ok(text) => text,
Err(never) => match never {},
};
out.push_str(&emitted.unwrap_or_default());
}
out.push_str(&cursor.flush());
out
}
#[test]
fn one_shot_drive_equals_token_by_token_drive() {
let tokenizer = make_test_tokenizer();
for text in ["", "Hello", "Hello 世界!", "héllo — ünïcode 🎉"] {
let ids = tokenizer.encode(text);
let one_shot = drive(&tokenizer, &ids, ids.len().max(1));
assert_eq!(one_shot, drive(&tokenizer, &ids, 1), "text: {text:?}");
for chunk in 1..=ids.len() {
assert_eq!(one_shot, drive(&tokenizer, &ids, chunk), "text: {text:?}");
}
assert_eq!(one_shot, tokenizer.decode_lossy(&ids), "text: {text:?}");
}
}
#[test]
fn parse_surface_byte_fallback_is_strict_two_hex_digits() {
let pieces = Arc::new(vec![
"<0x41>".to_string(),
"<0x4a>".to_string(),
"<0x1>".to_string(),
"<0x041>".to_string(),
"<0xG1>".to_string(),
]);
let state = DecodeState::for_piece_vocab(&pieces, FxHashSet::default(), false);
let render = state.render();
assert_eq!(render.token_bytes(0), Some(vec![0x41]));
assert_eq!(render.token_bytes(1), Some(vec![0x4a]));
assert_eq!(render.token_bytes(2), Some(b"<0x1>".to_vec()));
assert_eq!(render.token_bytes(3), Some(b"<0x041>".to_vec()));
assert_eq!(render.token_bytes(4), Some(b"<0xG1>".to_vec()));
let ids: Vec<u32> = (0..pieces.len() as u32).collect();
for chunk in 1..=ids.len() {
let mut cursor = state.cursor_with_capacity(ids.len() * 4);
let mut out = String::new();
for group in ids.chunks(chunk) {
let emitted = match cursor.feed(group, |_| Ok::<(), Infallible>(())) {
Ok(text) => text,
Err(never) => match never {},
};
out.push_str(&emitted.unwrap_or_default());
}
out.push_str(&cursor.flush());
assert_eq!(out, "AJ<0x1><0x041><0xG1>", "in chunks of {chunk}");
}
}
#[derive(Clone, Copy)]
enum Loop {
Specialized,
General,
}
fn plain_state() -> DecodeState {
let mut surfaces = FxHashMap::default();
surfaces.insert(1u32, b"He".to_vec());
surfaces.insert(2u32, b"llo".to_vec());
surfaces.insert(3u32, Vec::new());
let mut specials = FxHashMap::default();
specials.insert(5u32, "<eos>".to_string());
let skip: FxHashSet<u32> = [4u32].into_iter().collect();
DecodeState::new(
RenderRules::new(
Surfaces::ById(Arc::new(surfaces)),
Arc::new(specials),
Arc::new(skip),
ByteFallbackRule::None,
false,
false,
),
Vec::new(),
)
}
fn render_with(state: &DecodeState, ids: &[u32], which: Loop) -> (String, bool, Vec<u32>) {
let mut cursor = DecodeCursor::new(state);
let unknown = RefCell::new(Vec::new());
let record = |id: u32| {
unknown.borrow_mut().push(id);
Ok::<(), Infallible>(())
};
let outcome = match which {
Loop::Specialized => cursor.render_into(ids, record),
Loop::General => cursor.render_into_general(ids, record),
};
match outcome {
Ok(()) => {}
Err(never) => match never {},
}
let rendered_a_token = cursor.rendered_a_token;
(cursor.flush(), rendered_a_token, unknown.into_inner())
}
#[test]
fn specialized_loop_agrees_with_general_loop() {
let state = plain_state();
assert!(
state.render().plain_by_id().is_some(),
"the fixture must take the specialized path, or this proves nothing"
);
for ids in [
vec![1],
vec![3],
vec![4],
vec![5],
vec![9],
vec![],
vec![3, 1, 4, 2, 5, 9, 4, 3, 1],
vec![4, 9, 5, 1],
] {
let specialized = render_with(&state, &ids, Loop::Specialized);
let general = render_with(&state, &ids, Loop::General);
assert_eq!(specialized, general, "ids: {ids:?}");
}
}
#[test]
fn specialized_loop_renders_the_expected_text() {
let state = plain_state();
let ids = [3, 1, 4, 2, 5, 9, 4, 3, 1];
let (text, rendered_a_token, unknown) = render_with(&state, &ids, Loop::Specialized);
assert_eq!(text, "Hello<eos>He");
assert!(rendered_a_token);
assert_eq!(unknown, vec![9]);
let (text, rendered_a_token, unknown) = render_with(&state, &[3], Loop::Specialized);
assert_eq!(text, "");
assert!(rendered_a_token);
assert!(unknown.is_empty());
let (_, rendered_a_token, _) = render_with(&state, &[4], Loop::Specialized);
assert!(!rendered_a_token);
}
}