interscript_rs/imf/tokens.rs
1//! The canonical ByT5 byte table (SPEC.md §4) — fixed, no vocab files.
2//!
3//! UTF-8 byte `b` maps to id `b + 3`; one trailing EOS (id 1) is
4//! appended on encode; pad = 0, unk = 2. Ids are NOT raw bytes —
5//! feeding `text.as_bytes()` directly produces silent garbage on real
6//! models (the classic ByT5 interop bug; SPEC.md §4 warning).
7
8/// Byte offset: id = byte + 3.
9pub const BYTE_OFFSET: i64 = 3;
10/// Special id 0: padding (skipped on decode).
11pub const PAD_ID: i64 = 0;
12/// Special id 1: end of sequence (stops decode; appended on encode).
13pub const EOS_ID: i64 = 1;
14/// Special id 2: unknown (skipped on decode).
15pub const UNK_ID: i64 = 2;
16
17/// `tokens.encode`: UTF-8 bytes b -> ids b+3, then one trailing EOS.
18pub fn encode(text: &str) -> Vec<i64> {
19 let mut ids: Vec<i64> = text
20 .as_bytes()
21 .iter()
22 .map(|&b| i64::from(b) + BYTE_OFFSET)
23 .collect();
24 ids.push(EOS_ID);
25 ids
26}
27
28/// `tokens.decode`: stop at EOS; skip pad/unk; `(id - 3) mod 256` per
29/// byte; reassemble as UTF-8 (lossy, like the reference loader's
30/// `errors="replace"`).
31pub fn decode(token_ids: &[i64]) -> String {
32 let mut bytes = Vec::with_capacity(token_ids.len());
33 for &token in token_ids {
34 if token == EOS_ID {
35 break;
36 }
37 if token == PAD_ID || token == UNK_ID {
38 continue;
39 }
40 bytes.push((token - BYTE_OFFSET).rem_euclid(256) as u8);
41 }
42 String::from_utf8_lossy(&bytes).into_owned()
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn encode_appends_offset_and_eos() {
51 // "a" -> [97 + 3, EOS]
52 assert_eq!(encode("a"), vec![100, EOS_ID]);
53 // empty string still carries the trailing EOS (the reference
54 // loader short-circuits on that shape).
55 assert_eq!(encode(""), vec![EOS_ID]);
56 }
57
58 #[test]
59 fn encode_multibyte_is_per_byte() {
60 // Khmer "ខ" = U+1781 = UTF-8 0xE1 0x9E 0x81
61 assert_eq!(encode("ខ"), vec![0xE1 + 3, 0x9E + 3, 0x81 + 3, EOS_ID]);
62 }
63
64 #[test]
65 fn decode_roundtrip_ascii_and_multibyte() {
66 let text = "ភាសា en ک";
67 assert_eq!(decode(&encode(text)), text);
68 }
69
70 #[test]
71 fn decode_stops_at_eos_and_skips_pad_unk() {
72 // pad 0 / unk 2 are dropped, EOS truncates.
73 // (token 100 -> byte 97 'a', token 101 -> byte 98 'b')
74 assert_eq!(decode(&[PAD_ID, 100, UNK_ID, 101, EOS_ID, 102]), "ab");
75 }
76
77 #[test]
78 fn decode_ids_below_offset_mod_256() {
79 // (id - 3) mod 256: id 324 wraps to byte 65 'A'.
80 assert_eq!(decode(&[324, EOS_ID]), "A");
81 }
82}