Skip to main content

ferrox_models/grammar/
utf8.rs

1//! UTF-8 decoding for the grammar engine, transcribed from the two
2//! `decode_utf8` overloads in llama.cpp's `src/llama-grammar.cpp`.
3//!
4//! Both are deliberately *not* `str::chars()`. Two reasons:
5//!
6//! 1. A token piece can end mid-codepoint. Multi-byte characters are split
7//!    across tokens by every BPE vocabulary, so the decoder has to carry a
8//!    [`PartialUtf8`] between pieces and the constraint has to be able to
9//!    ask "could some continuation of these bits still satisfy this
10//!    character class?" ([`super::machine`]'s `match_partial_char`).
11//! 2. The lead-byte length tables here are llama.cpp's, including their
12//!    quirks: the parser's table maps a continuation byte to length 1 and
13//!    decodes it as a 7-bit character, while the piece decoder maps the
14//!    same byte to length 0 and reports an invalid sequence. Replacing
15//!    either with a correct UTF-8 decoder would change which grammars
16//!    parse and which tokens are rejected.
17
18/// Bits of a UTF-8 sequence decoded so far, carried between token pieces.
19///
20/// `llama_partial_utf8`. `n_remain` is the number of continuation bytes
21/// still expected: `0` means "nothing pending", `-1` means "the byte
22/// stream was not valid UTF-8".
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub struct PartialUtf8 {
25    /// Bit value so far, unshifted.
26    pub value: u32,
27    /// Continuation bytes still expected; `-1` marks an invalid sequence.
28    pub n_remain: i32,
29}
30
31impl PartialUtf8 {
32    pub const fn new(value: u32, n_remain: i32) -> Self {
33        Self { value, n_remain }
34    }
35}
36
37/// llama.cpp's lead-byte length table for [`decode_char`], indexed by the
38/// top four bits. Note indices 8..=11 (continuation bytes) map to 1, not 0:
39/// the parser assumes it is handed valid UTF-8 and only guards overrun.
40const CHAR_LOOKUP: [usize; 16] = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 3, 4];
41
42/// llama.cpp's lead-byte length table for [`decode_piece`]. Here indices
43/// 8..=11 map to 0, which becomes `n_remain = -1` and aborts the decode:
44/// a token piece is untrusted input and a stray continuation byte is a
45/// real error.
46const PIECE_LOOKUP: [i32; 16] = [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 3, 4];
47
48/// Byte at `i`, or `0` past the end.
49///
50/// llama.cpp walks NUL-terminated `const char *`, so every one of its loop
51/// guards is `*pos != 0`. Returning `0` past the end reproduces that
52/// exactly, including for an embedded NUL byte, which upstream also treats
53/// as end of input.
54#[inline]
55pub(crate) fn byte_at(src: &[u8], i: usize) -> u8 {
56    if i < src.len() {
57        src[i]
58    } else {
59        0
60    }
61}
62
63/// Decode one code point starting at `pos`, returning it and the offset
64/// just past it.
65///
66/// `static std::pair<uint32_t, const char *> decode_utf8(const char * src)`.
67/// Assumes valid UTF-8 but does not run off the end of the buffer.
68pub(crate) fn decode_char(src: &[u8], pos: usize) -> (u32, usize) {
69    let first_byte = byte_at(src, pos);
70    let highbits = (first_byte >> 4) as usize;
71    let len = CHAR_LOOKUP[highbits];
72    // `(1 << (8 - len)) - 1` keeps one more bit than the strict UTF-8 mask,
73    // but that bit is always 0 in a well-formed lead byte of this length.
74    let mask = (1u32 << (8 - len)) - 1;
75    let mut value = first_byte as u32 & mask;
76    let end = pos + len; // may overrun the buffer; the guard below stops us
77    let mut p = pos + 1;
78    while p < end && byte_at(src, p) != 0 {
79        value = (value << 6) + (byte_at(src, p) & 0x3F) as u32;
80        p += 1;
81    }
82    (value, p)
83}
84
85/// Decode a token piece into code points, continuing an earlier partial
86/// sequence and reporting whatever partial sequence is left over.
87///
88/// `static std::pair<std::vector<uint32_t>, llama_partial_utf8>
89/// decode_utf8(const std::string &, llama_partial_utf8)`.
90///
91/// The returned vector is **always terminated by a `0`**, which callers
92/// rely on: `reject_candidates_for_stack` uses `*code_points == 0` to mean
93/// "this token's complete code points are exhausted".
94pub(crate) fn decode_piece(src: &[u8], partial_start: PartialUtf8) -> (Vec<u32>, PartialUtf8) {
95    let mut pos = 0usize;
96    // Common English pieces have as many code points as bytes; `+1` for the
97    // terminating 0.
98    let mut code_points: Vec<u32> = Vec::with_capacity(src.len() + 1);
99
100    let mut value = partial_start.value;
101    let mut n_remain = partial_start.n_remain;
102
103    // Continue the previous decode, if applicable.
104    while byte_at(src, pos) != 0 && n_remain > 0 {
105        let next_byte = byte_at(src, pos);
106        if (next_byte >> 6) != 2 {
107            // Not a continuation byte: invalid sequence, abort.
108            code_points.push(0);
109            return (code_points, PartialUtf8::new(0, -1));
110        }
111        value = (value << 6) + (next_byte & 0x3F) as u32;
112        pos += 1;
113        n_remain -= 1;
114    }
115
116    if partial_start.n_remain > 0 && n_remain == 0 {
117        code_points.push(value);
118    }
119
120    // Decode subsequent sequences, the last of which may be incomplete.
121    while byte_at(src, pos) != 0 {
122        let first_byte = byte_at(src, pos);
123        let highbits = (first_byte >> 4) as usize;
124        n_remain = PIECE_LOOKUP[highbits] - 1;
125
126        if n_remain < 0 {
127            // Invalid sequence, abort. Upstream drops the code points it
128            // had already decoded here; keeping them would let a prefix of
129            // a malformed piece advance the parse.
130            code_points.clear();
131            code_points.push(0);
132            return (code_points, PartialUtf8::new(0, n_remain));
133        }
134
135        let mask = (1u32 << (7 - n_remain)) - 1;
136        value = first_byte as u32 & mask;
137
138        pos += 1;
139        while byte_at(src, pos) != 0 && n_remain > 0 {
140            value = (value << 6) + (byte_at(src, pos) & 0x3F) as u32;
141            pos += 1;
142            n_remain -= 1;
143        }
144        if n_remain == 0 {
145            code_points.push(value);
146        }
147    }
148    code_points.push(0);
149
150    (code_points, PartialUtf8::new(value, n_remain))
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn ascii_piece_decodes_to_its_bytes_plus_a_terminator() {
159        let (v, p) = decode_piece(b"abc", PartialUtf8::default());
160        assert_eq!(v, vec![0x61, 0x62, 0x63, 0]);
161        assert_eq!(p.n_remain, 0);
162    }
163
164    #[test]
165    fn empty_piece_is_just_the_terminator() {
166        let (v, p) = decode_piece(b"", PartialUtf8::default());
167        assert_eq!(v, vec![0]);
168        assert_eq!(p, PartialUtf8::default());
169    }
170
171    #[test]
172    fn multibyte_piece_decodes_whole() {
173        // "é" U+00E9 (2 bytes), "€" U+20AC (3), "𝄞" U+1D11E (4).
174        let (v, p) = decode_piece("é€𝄞".as_bytes(), PartialUtf8::default());
175        assert_eq!(v, vec![0xE9, 0x20AC, 0x1D11E, 0]);
176        assert_eq!(p.n_remain, 0);
177    }
178
179    #[test]
180    fn a_codepoint_split_across_two_pieces_is_carried_and_completed() {
181        // "€" is E2 82 AC. Split it after the first byte, the way a BPE
182        // vocabulary splits a multi-byte character across two tokens.
183        let euro = "€".as_bytes();
184        let (v1, p1) = decode_piece(&euro[..1], PartialUtf8::default());
185        assert_eq!(v1, vec![0], "no complete code point yet");
186        assert_eq!(p1.n_remain, 2, "two continuation bytes still expected");
187
188        let (v2, p2) = decode_piece(&euro[1..2], p1);
189        assert_eq!(v2, vec![0], "still incomplete after one continuation byte");
190        assert_eq!(p2.n_remain, 1);
191
192        let (v3, p3) = decode_piece(&euro[2..], p2);
193        assert_eq!(v3, vec![0x20AC, 0], "completes on the last byte");
194        assert_eq!(p3.n_remain, 0);
195    }
196
197    #[test]
198    fn a_split_codepoint_followed_by_more_text_decodes_both() {
199        let bytes = "€!".as_bytes();
200        let (_, p1) = decode_piece(&bytes[..1], PartialUtf8::default());
201        let (v, p) = decode_piece(&bytes[1..], p1);
202        assert_eq!(v, vec![0x20AC, b'!' as u32, 0]);
203        assert_eq!(p.n_remain, 0);
204    }
205
206    #[test]
207    fn a_lead_byte_where_a_continuation_was_due_is_an_invalid_sequence() {
208        let p1 = PartialUtf8::new(0x02, 1); // mid "é"
209        let (v, p) = decode_piece(b"A", p1);
210        assert_eq!(v, vec![0]);
211        assert_eq!(p, PartialUtf8::new(0, -1), "n_remain = -1 marks invalid");
212    }
213
214    #[test]
215    fn a_stray_continuation_byte_clears_everything_already_decoded() {
216        // Upstream calls code_points.clear() here: the leading "ab" is
217        // thrown away, not returned. A decoder that kept it would let a
218        // malformed piece advance the parse by two characters.
219        let (v, p) = decode_piece(&[b'a', b'b', 0x80], PartialUtf8::default());
220        assert_eq!(v, vec![0]);
221        assert_eq!(p.n_remain, -1);
222    }
223
224    #[test]
225    fn a_truncated_lead_byte_leaves_a_partial_not_an_error() {
226        let (v, p) = decode_piece(&[0xE2], PartialUtf8::default());
227        assert_eq!(v, vec![0]);
228        assert_eq!(p, PartialUtf8::new(0x02, 2));
229    }
230
231    #[test]
232    fn an_embedded_nul_ends_the_piece_as_it_does_in_c() {
233        let (v, _) = decode_piece(b"ab\0cd", PartialUtf8::default());
234        assert_eq!(v, vec![b'a' as u32, b'b' as u32, 0]);
235    }
236
237    #[test]
238    fn decode_char_reads_one_codepoint_and_reports_its_width() {
239        assert_eq!(decode_char(b"a", 0), (0x61, 1));
240        assert_eq!(decode_char("é".as_bytes(), 0), (0xE9, 2));
241        assert_eq!(decode_char("€".as_bytes(), 0), (0x20AC, 3));
242        assert_eq!(decode_char("𝄞".as_bytes(), 0), (0x1D11E, 4));
243        // Offset into the middle of a string.
244        assert_eq!(decode_char("aé".as_bytes(), 1), (0xE9, 3));
245    }
246
247    #[test]
248    fn decode_char_does_not_run_past_a_truncated_sequence() {
249        // Lead byte claims 3 bytes, only 1 is present. llama.cpp's loop
250        // guard is `pos < end && *pos`; both must hold.
251        let (value, end) = decode_char(&[0xE2], 0);
252        assert_eq!(end, 1, "stopped at the buffer end, not at pos+3");
253        assert_eq!(value, 0x02);
254    }
255
256    #[test]
257    fn the_two_lookup_tables_disagree_on_continuation_bytes_and_that_is_deliberate() {
258        // Parser table: 0x80 >> 4 == 8 -> length 1, decoded as a character.
259        assert_eq!(CHAR_LOOKUP[8], 1);
260        assert_eq!(decode_char(&[0x80], 0), (0x00, 1));
261        // Piece table: same byte -> length 0 -> n_remain -1 -> invalid.
262        assert_eq!(PIECE_LOOKUP[8], 0);
263    }
264}