Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/realizar/src/tokenizer.rs
Line
Count
Source
1
//! Tokenizer for text encoding and decoding
2
//!
3
//! Implements tokenization for transformer models:
4
//! - BPE (Byte Pair Encoding) - Used by GPT models
5
//! - Vocabulary management
6
//! - Special token handling
7
//!
8
//! ## Example
9
//!
10
//! ```rust,ignore
11
//! use realizar::Tokenizer;
12
//!
13
//! let tokenizer = Tokenizer::from_vocab(vocab);
14
//! let token_ids = tokenizer.encode("Hello, world!")?;
15
//! let text = tokenizer.decode(&token_ids)?;
16
//! ```
17
18
use std::collections::HashMap;
19
20
use crate::error::{RealizarError, Result};
21
22
/// Vocabulary mapping between tokens and IDs
23
#[derive(Debug, Clone)]
24
pub struct Vocabulary {
25
    /// Token to ID mapping
26
    token_to_id: HashMap<String, u32>,
27
    /// ID to token mapping
28
    id_to_token: HashMap<u32, String>,
29
}
30
31
impl Vocabulary {
32
    /// Create a new vocabulary from token list
33
    ///
34
    /// # Arguments
35
    ///
36
    /// * `tokens` - List of tokens in order (index = token ID)
37
    ///
38
    /// # Errors
39
    ///
40
    /// Returns error if tokens list is empty or contains duplicates
41
    ///
42
    /// # Examples
43
    ///
44
    /// ```rust,ignore
45
    /// let vocab = Vocabulary::from_tokens(vec![
46
    ///     "<unk>".to_string(),
47
    ///     "hello".to_string(),
48
    ///     "world".to_string(),
49
    /// ])?;
50
    /// ```
51
11
    pub fn from_tokens(tokens: Vec<String>) -> Result<Self> {
52
11
        if tokens.is_empty() {
53
1
            return Err(RealizarError::UnsupportedOperation {
54
1
                operation: "create_vocabulary".to_string(),
55
1
                reason: "Vocabulary cannot be empty".to_string(),
56
1
            });
57
10
        }
58
59
10
        let mut token_to_id = HashMap::new();
60
10
        let mut id_to_token = HashMap::new();
61
62
26
        for (id, token) in 
tokens10
.
into_iter10
().
enumerate10
() {
63
26
            let id = u32::try_from(id).map_err(|_| RealizarError::UnsupportedOperation {
64
0
                operation: "convert_token_id".to_string(),
65
0
                reason: format!("Token ID {id} exceeds u32 limit"),
66
0
            })?;
67
68
26
            if token_to_id.contains_key(&token) {
69
1
                return Err(RealizarError::UnsupportedOperation {
70
1
                    operation: "create_vocabulary".to_string(),
71
1
                    reason: format!("Duplicate token: {token}"),
72
1
                });
73
25
            }
74
75
25
            token_to_id.insert(token.clone(), id);
76
25
            id_to_token.insert(id, token);
77
        }
78
79
9
        Ok(Self {
80
9
            token_to_id,
81
9
            id_to_token,
82
9
        })
83
11
    }
84
85
    /// Get token ID for a token
86
    ///
87
    /// Returns `None` if token not in vocabulary
88
    #[must_use]
89
33
    pub fn get_id(&self, token: &str) -> Option<u32> {
90
33
        self.token_to_id.get(token).copied()
91
33
    }
92
93
    /// Get token for a token ID
94
    ///
95
    /// Returns `None` if ID not in vocabulary
96
    #[must_use]
97
20
    pub fn get_token(&self, id: u32) -> Option<&str> {
98
20
        self.id_to_token.get(&id).map(String::as_str)
99
20
    }
100
101
    /// Get vocabulary size
102
    #[must_use]
103
2
    pub fn size(&self) -> usize {
104
2
        self.token_to_id.len()
105
2
    }
106
}
107
108
/// Tokenizer for encoding and decoding text
109
#[derive(Debug, Clone)]
110
pub struct Tokenizer {
111
    /// Vocabulary
112
    vocab: Vocabulary,
113
    /// Unknown token ID
114
    unk_token_id: u32,
115
}
116
117
/// BPE (Byte Pair Encoding) tokenizer
118
///
119
/// Implements subword tokenization using byte pair encoding algorithm.
120
/// Used by GPT-2, GPT-3, and many other models.
121
#[derive(Debug, Clone)]
122
pub struct BPETokenizer {
123
    /// Token to ID mapping
124
    token_to_id: HashMap<String, u32>,
125
    /// ID to token mapping
126
    id_to_token: HashMap<u32, String>,
127
    /// Merge rules: pairs to merge in order of priority
128
    merges: Vec<(String, String)>,
129
    /// Unknown token ID
130
    unk_token_id: u32,
131
}
132
133
impl BPETokenizer {
134
    /// Create a new BPE tokenizer
135
    ///
136
    /// # Arguments
137
    ///
138
    /// * `vocab` - List of tokens (index = token ID)
139
    /// * `merges` - List of merge pairs in priority order
140
    /// * `unk_token` - Unknown token string
141
    ///
142
    /// # Errors
143
    ///
144
    /// Returns error if vocabulary is empty or unknown token not found
145
196
    pub fn new(vocab: Vec<String>, merges: Vec<(String, String)>, unk_token: &str) -> Result<Self> {
146
196
        if vocab.is_empty() {
147
1
            return Err(RealizarError::UnsupportedOperation {
148
1
                operation: "create_bpe_tokenizer".to_string(),
149
1
                reason: "Vocabulary cannot be empty".to_string(),
150
1
            });
151
195
        }
152
153
195
        let mut token_to_id = HashMap::new();
154
195
        let mut id_to_token = HashMap::new();
155
156
16.3k
        for (id, token) in 
vocab195
.
into_iter195
().
enumerate195
() {
157
16.3k
            let id = u32::try_from(id).map_err(|_| RealizarError::UnsupportedOperation {
158
0
                operation: "convert_token_id".to_string(),
159
0
                reason: format!("Token ID {id} exceeds u32 limit"),
160
0
            })?;
161
16.3k
            token_to_id.insert(token.clone(), id);
162
16.3k
            id_to_token.insert(id, token);
163
        }
164
165
194
        let unk_token_id =
166
195
            *token_to_id
167
195
                .get(unk_token)
168
195
                .ok_or_else(|| RealizarError::UnsupportedOperation {
169
1
                    operation: "create_bpe_tokenizer".to_string(),
170
1
                    reason: format!("Unknown token '{unk_token}' not in vocabulary"),
171
1
                })?;
172
173
194
        Ok(Self {
174
194
            token_to_id,
175
194
            id_to_token,
176
194
            merges,
177
194
            unk_token_id,
178
194
        })
179
196
    }
180
181
    /// Encode text to token IDs using greedy longest match
182
    ///
183
    /// Uses GPT-2 style encoding where spaces become Ġ (U+0120) and
184
    /// newlines become Ċ (U+010A).
185
    ///
186
    /// # Arguments
187
    ///
188
    /// * `text` - Input text to encode
189
    ///
190
    /// # Returns
191
    ///
192
    /// Vector of token IDs
193
    #[must_use]
194
74
    pub fn encode(&self, text: &str) -> Vec<u32> {
195
74
        if text.is_empty() {
196
8
            return Vec::new();
197
66
        }
198
199
        // Convert to GPT-2 encoding: space -> Ġ, newline -> Ċ
200
66
        let processed: String = text
201
66
            .chars()
202
412
            .
map66
(|c| match c {
203
16
                ' ' => 'Ġ',  // U+0120
204
0
                '\n' => 'Ċ', // U+010A
205
0
                '\r' => 'Ḃ', // U+1E02
206
396
                _ => c,
207
412
            })
208
66
            .collect();
209
210
66
        let mut tokens = Vec::new();
211
66
        let mut remaining = processed.as_str();
212
213
344
        while !remaining.is_empty() {
214
            // Greedy longest match
215
278
            let mut best_len = 0;
216
278
            let mut best_id = None;
217
218
            // Collect character byte offsets for proper slicing
219
278
            let char_indices: Vec<usize> = remaining
220
278
                .char_indices()
221
278
                .map(|(i, _)| i)
222
278
                .chain(std::iter::once(remaining.len()))
223
278
                .collect();
224
225
            // Try all prefixes up to 32 chars
226
1.87k
            for char_count in 1..=
char_indices278
.
len278
().
saturating_sub278
(1).
min278
(32) {
227
1.87k
                let byte_end = char_indices[char_count];
228
1.87k
                let prefix = &remaining[..byte_end];
229
1.87k
                if let Some(&
id56
) = self.token_to_id.get(prefix) {
230
56
                    best_len = byte_end;
231
56
                    best_id = Some(id);
232
1.81k
                }
233
            }
234
235
278
            if let Some(
id44
) = best_id {
236
44
                tokens.push(id);
237
44
                remaining = &remaining[best_len..];
238
44
            } else {
239
                // No match - try byte tokens like <0x48>
240
234
                let ch = remaining.chars().next().expect("non-empty");
241
234
                let ch_len = ch.len_utf8();
242
243
249
                for byte in 
remaining[..ch_len]234
.
bytes234
() {
244
249
                    let byte_token = format!("<0x{byte:02X}>");
245
249
                    if let Some(&
id0
) = self.token_to_id.get(&byte_token) {
246
0
                        tokens.push(id);
247
249
                    } else {
248
249
                        tokens.push(self.unk_token_id);
249
249
                    }
250
                }
251
234
                remaining = &remaining[ch_len..];
252
            }
253
        }
254
255
66
        tokens
256
74
    }
257
258
    /// Apply a single merge rule to token list
259
0
    fn apply_merge(tokens: &[String], first: &str, second: &str) -> Vec<String> {
260
0
        if tokens.len() < 2 {
261
0
            return tokens.to_vec();
262
0
        }
263
264
0
        let mut result = Vec::new();
265
0
        let mut i = 0;
266
267
0
        while i < tokens.len() {
268
0
            if i + 1 < tokens.len() && tokens[i] == first && tokens[i + 1] == second {
269
0
                // Merge the pair
270
0
                result.push(format!("{first}{second}"));
271
0
                i += 2;
272
0
            } else {
273
0
                result.push(tokens[i].clone());
274
0
                i += 1;
275
0
            }
276
        }
277
278
0
        result
279
0
    }
280
281
    /// Decode token IDs to text
282
    ///
283
    /// # Arguments
284
    ///
285
    /// * `token_ids` - Token IDs to decode
286
    ///
287
    /// # Errors
288
    ///
289
    /// Returns error if any token ID is invalid
290
47
    pub fn decode(&self, token_ids: &[u32]) -> Result<String> {
291
47
        let mut bytes: Vec<u8> = Vec::new();
292
293
870
        for &
id824
in token_ids {
294
823
            let token =
295
824
                self.id_to_token
296
824
                    .get(&id)
297
824
                    .ok_or_else(|| RealizarError::UnsupportedOperation {
298
1
                        operation: "decode_bpe_token".to_string(),
299
1
                        reason: format!("Invalid token ID: {id}"),
300
1
                    })?;
301
302
            // Skip special tokens
303
823
            if token.starts_with("<|") && 
token.ends_with("|>")1
{
304
1
                continue;
305
822
            }
306
822
            if token == "<s>" || 
token == "</s>"821
||
token == "<unk>"820
||
token == "<pad>"314
{
307
509
                continue;
308
313
            }
309
310
            // Handle byte tokens like <0xE6>
311
313
            if token.starts_with("<0x") && 
token.ends_with('>')4
&&
token.len() == 64
{
312
4
                if let Ok(
byte_val3
) = u8::from_str_radix(&token[3..5], 16) {
313
3
                    bytes.push(byte_val);
314
3
                    continue;
315
1
                }
316
309
            }
317
318
            // Decode GPT-2 style byte-level BPE
319
2.01k
            for c in 
token310
.
chars310
() {
320
2.01k
                match c {
321
3
                    'Ġ' => bytes.push(b' '),  // U+0120 -> space
322
2
                    'Ċ' => bytes.push(b'\n'), // U+010A -> newline
323
1
                    'ċ' => bytes.push(b'\n'), // lowercase variant
324
1
                    'Ḃ' => bytes.push(b'\r'), // U+1E02 -> carriage return
325
1
                    '▁' => bytes.push(b' '),  // U+2581 SentencePiece -> space
326
                    _ => {
327
                        // Try GPT-2 unicode-to-byte mapping
328
2.01k
                        if let Some(
byte2.00k
) = Self::gpt2_char_to_byte(c) {
329
2.00k
                            bytes.push(byte);
330
2.00k
                        } else {
331
5
                            // Regular UTF-8 character
332
5
                            let mut buf = [0u8; 4];
333
5
                            let encoded = c.encode_utf8(&mut buf);
334
5
                            bytes.extend_from_slice(encoded.as_bytes());
335
5
                        }
336
                    },
337
                }
338
            }
339
        }
340
341
        // Decode as UTF-8, replacing invalid sequences
342
46
        Ok(String::from_utf8_lossy(&bytes).into_owned())
343
47
    }
344
345
    /// Convert GPT-2 unicode character to original byte value
346
2.01k
    fn gpt2_char_to_byte(c: char) -> Option<u8> {
347
        // GPT-2 maps bytes 0-255 to unicode characters
348
        // Printable ASCII (33-126) maps to itself
349
        // Other bytes map to unicode range starting at U+0100
350
2.01k
        let code = c as u32;
351
2.01k
        if (33..=126).contains(&code) || 
code == 329
{
352
2.00k
            Some(code as u8)
353
8
        } else if (0x100..=0x100 + 255).contains(&code) {
354
            // GPT-2 remapped bytes
355
3
            let byte = (code - 0x100) as u8;
356
            // Map back based on GPT-2's byte_encoder
357
3
            match byte {
358
3
                0..=32 => 
Some(byte)0
, // Control chars + space
359
3
                127..=160 => 
Some(byte)2
, // DEL + extended ASCII
360
1
                173 => Some(173),        // Soft hyphen
361
0
                _ => None,
362
            }
363
        } else {
364
5
            None
365
        }
366
2.01k
    }
367
368
    /// Get vocabulary size
369
    #[must_use]
370
2
    pub fn vocab_size(&self) -> usize {
371
2
        self.token_to_id.len()
372
2
    }
373
374
    /// Get token ID for a token
375
    #[must_use]
376
2
    pub fn get_token_id(&self, token: &str) -> Option<u32> {
377
2
        self.token_to_id.get(token).copied()
378
2
    }
379
380
    /// Get token for a token ID
381
    #[must_use]
382
2
    pub fn get_token(&self, id: u32) -> Option<&str> {
383
2
        self.id_to_token.get(&id).map(String::as_str)
384
2
    }
385
}
386
387
/// Viterbi algorithm state: `(best_score, best_token)` at each position
388
type ViterbiState = (Vec<f32>, Vec<Option<String>>);
389
390
/// `SentencePiece` tokenizer (Unigram model)
391
///
392
/// Implements subword tokenization using unigram language model.
393
/// Used by `LLaMA`, T5, ALBERT, and many other models.
394
///
395
/// Unlike BPE which uses greedy merges, `SentencePiece` finds the
396
/// most likely segmentation using token scores (log probabilities).
397
#[derive(Debug, Clone)]
398
pub struct SentencePieceTokenizer {
399
    /// Token to ID mapping
400
    token_to_id: HashMap<String, u32>,
401
    /// ID to token mapping
402
    id_to_token: HashMap<u32, String>,
403
    /// Token scores (log probabilities)
404
    scores: HashMap<String, f32>,
405
    /// Unknown token ID
406
    unk_token_id: u32,
407
}
408
409
impl SentencePieceTokenizer {
410
    /// Create a new `SentencePiece` tokenizer
411
    ///
412
    /// # Arguments
413
    ///
414
    /// * `vocab` - List of (token, score) pairs where score is log probability
415
    /// * `unk_token` - Unknown token string
416
    ///
417
    /// # Errors
418
    ///
419
    /// Returns error if vocabulary is empty or unknown token not found
420
15
    pub fn new(vocab: Vec<(String, f32)>, unk_token: &str) -> Result<Self> {
421
15
        if vocab.is_empty() {
422
1
            return Err(RealizarError::UnsupportedOperation {
423
1
                operation: "create_sentencepiece_tokenizer".to_string(),
424
1
                reason: "Vocabulary cannot be empty".to_string(),
425
1
            });
426
14
        }
427
428
14
        let mut token_to_id = HashMap::new();
429
14
        let mut id_to_token = HashMap::new();
430
14
        let mut scores = HashMap::new();
431
432
46
        for (id, (token, score)) in 
vocab14
.
into_iter14
().
enumerate14
() {
433
46
            let id = u32::try_from(id).map_err(|_| RealizarError::UnsupportedOperation {
434
0
                operation: "convert_token_id".to_string(),
435
0
                reason: format!("Token ID {id} exceeds u32 limit"),
436
0
            })?;
437
46
            token_to_id.insert(token.clone(), id);
438
46
            id_to_token.insert(id, token.clone());
439
46
            scores.insert(token, score);
440
        }
441
442
13
        let unk_token_id =
443
14
            *token_to_id
444
14
                .get(unk_token)
445
14
                .ok_or_else(|| RealizarError::UnsupportedOperation {
446
1
                    operation: "create_sentencepiece_tokenizer".to_string(),
447
1
                    reason: format!("Unknown token '{unk_token}' not in vocabulary"),
448
1
                })?;
449
450
13
        Ok(Self {
451
13
            token_to_id,
452
13
            id_to_token,
453
13
            scores,
454
13
            unk_token_id,
455
13
        })
456
15
    }
457
458
    /// Encode text to token IDs using Viterbi algorithm
459
    ///
460
    /// Finds the most likely segmentation based on token scores.
461
    ///
462
    /// # Arguments
463
    ///
464
    /// * `text` - Input text to encode
465
    ///
466
    /// # Returns
467
    ///
468
    /// Vector of token IDs
469
    #[must_use]
470
7
    pub fn encode(&self, text: &str) -> Vec<u32> {
471
7
        if text.is_empty() {
472
1
            return Vec::new();
473
6
        }
474
475
6
        let chars: Vec<char> = text.chars().collect();
476
6
        let (_best_score, best_token) = self.viterbi_forward(&chars);
477
6
        let tokens = Self::viterbi_backtrack(&chars, &best_token);
478
479
        // Convert tokens to IDs
480
6
        tokens
481
6
            .into_iter()
482
11
            .
map6
(|t| {
483
11
                self.token_to_id
484
11
                    .get(&t)
485
11
                    .copied()
486
11
                    .unwrap_or(self.unk_token_id)
487
11
            })
488
6
            .collect()
489
7
    }
490
491
    /// Viterbi forward pass: find best score and token at each position
492
    ///
493
    /// # Arguments
494
    ///
495
    /// * `chars` - Character array of input text
496
    ///
497
    /// # Returns
498
    ///
499
    /// Tuple of `(best_score, best_token)` vectors
500
6
    fn viterbi_forward(&self, chars: &[char]) -> ViterbiState {
501
6
        let n = chars.len();
502
6
        let mut best_score = vec![f32::NEG_INFINITY; n + 1];
503
6
        let mut best_token: Vec<Option<String>> = vec![None; n + 1];
504
6
        best_score[0] = 0.0;
505
506
34
        for end in 1..=
n6
{
507
            // Try all possible start positions for token ending at `end`
508
132
            for start in 0..
end34
{
509
132
                let substr: String = chars[start..end].iter().collect();
510
132
                if let Some(&
score27
) = self.scores.get(&substr) {
511
27
                    let new_score = best_score[start] + score;
512
27
                    if new_score > best_score[end] {
513
21
                        best_score[end] = new_score;
514
21
                        best_token[end] = Some(substr);
515
21
                    
}6
516
105
                }
517
            }
518
519
            // If no token found ending at this position, use single character as unknown
520
34
            if best_token[end].is_none() && 
best_score[end - 1] > f32::NEG_INFINITY13
{
521
13
                let char_str: String = chars[end - 1..end].iter().collect();
522
13
                best_score[end] = best_score[end - 1] - 100.0; // Penalty for unknown
523
13
                best_token[end] = Some(char_str);
524
21
            }
525
        }
526
527
6
        (best_score, best_token)
528
6
    }
529
530
    /// Viterbi backtracking: reconstruct token sequence from `best_token`
531
    ///
532
    /// # Arguments
533
    ///
534
    /// * `chars` - Character array of input text
535
    /// * `best_token` - Best token at each position (from forward pass)
536
    ///
537
    /// # Returns
538
    ///
539
    /// Vector of tokens in forward order
540
6
    fn viterbi_backtrack(chars: &[char], best_token: &[Option<String>]) -> Vec<String> {
541
6
        let n = chars.len();
542
6
        let mut tokens = Vec::new();
543
6
        let mut pos = n;
544
545
17
        while pos > 0 {
546
11
            if let Some(token) = &best_token[pos] {
547
11
                tokens.push(token.clone());
548
11
                pos -= token.chars().count();
549
11
            } else {
550
0
                // Fallback: single character
551
0
                let char_str: String = chars[pos - 1..pos].iter().collect();
552
0
                tokens.push(char_str);
553
0
                pos -= 1;
554
0
            }
555
        }
556
557
6
        tokens.reverse();
558
6
        tokens
559
6
    }
560
561
    /// Decode token IDs to text
562
    ///
563
    /// # Arguments
564
    ///
565
    /// * `token_ids` - Token IDs to decode
566
    ///
567
    /// # Errors
568
    ///
569
    /// Returns error if any token ID is invalid
570
4
    pub fn decode(&self, token_ids: &[u32]) -> Result<String> {
571
4
        let mut result = String::new();
572
573
8
        for &
id5
in token_ids {
574
4
            let token =
575
5
                self.id_to_token
576
5
                    .get(&id)
577
5
                    .ok_or_else(|| RealizarError::UnsupportedOperation {
578
1
                        operation: "decode_sentencepiece_token".to_string(),
579
1
                        reason: format!("Invalid token ID: {id}"),
580
1
                    })?;
581
4
            result.push_str(token);
582
        }
583
584
3
        Ok(result)
585
4
    }
586
587
    /// Get vocabulary size
588
    #[must_use]
589
2
    pub fn vocab_size(&self) -> usize {
590
2
        self.token_to_id.len()
591
2
    }
592
593
    /// Get token ID for a token
594
    #[must_use]
595
2
    pub fn get_token_id(&self, token: &str) -> Option<u32> {
596
2
        self.token_to_id.get(token).copied()
597
2
    }
598
599
    /// Get token for a token ID
600
    #[must_use]
601
2
    pub fn get_token(&self, id: u32) -> Option<&str> {
602
2
        self.id_to_token.get(&id).map(String::as_str)
603
2
    }
604
605
    /// Get score for a token
606
    #[must_use]
607
2
    pub fn get_score(&self, token: &str) -> Option<f32> {
608
2
        self.scores.get(token).copied()
609
2
    }
610
}
611
612
impl Tokenizer {
613
    /// Create a new tokenizer from vocabulary
614
    ///
615
    /// # Arguments
616
    ///
617
    /// * `vocab` - Vocabulary mapping
618
    /// * `unk_token` - Unknown token (default: `"<unk>"`)
619
    ///
620
    /// # Errors
621
    ///
622
    /// Returns error if unknown token not in vocabulary
623
    ///
624
    /// # Examples
625
    ///
626
    /// ```rust,ignore
627
    /// let vocab = Vocabulary::from_tokens(vec!["<unk>".to_string(), "hello".to_string()])?;
628
    /// let tokenizer = Tokenizer::new(vocab, "<unk>")?;
629
    /// ```
630
7
    pub fn new(vocab: Vocabulary, unk_token: &str) -> Result<Self> {
631
6
        let unk_token_id =
632
7
            vocab
633
7
                .get_id(unk_token)
634
7
                .ok_or_else(|| RealizarError::UnsupportedOperation {
635
1
                    operation: "create_tokenizer".to_string(),
636
1
                    reason: format!("Unknown token '{unk_token}' not in vocabulary"),
637
1
                })?;
638
639
6
        Ok(Self {
640
6
            vocab,
641
6
            unk_token_id,
642
6
        })
643
7
    }
644
645
    /// Encode text to token IDs (simple word-level tokenization)
646
    ///
647
    /// # Arguments
648
    ///
649
    /// * `text` - Input text to encode
650
    ///
651
    /// # Returns
652
    ///
653
    /// Vector of token IDs
654
    ///
655
    /// # Examples
656
    ///
657
    /// ```rust,ignore
658
    /// let token_ids = tokenizer.encode("hello world")?;
659
    /// assert_eq!(token_ids, vec![1, 2]);
660
    /// ```
661
    #[must_use]
662
6
    pub fn encode(&self, text: &str) -> Vec<u32> {
663
6
        text.split_whitespace()
664
22
            .
map6
(|word| self.vocab.get_id(word).unwrap_or(self.unk_token_id))
665
6
            .collect()
666
6
    }
667
668
    /// Decode token IDs to text
669
    ///
670
    /// # Arguments
671
    ///
672
    /// * `token_ids` - Token IDs to decode
673
    ///
674
    /// # Returns
675
    ///
676
    /// Decoded text string
677
    ///
678
    /// # Errors
679
    ///
680
    /// Returns error if any token ID is invalid
681
    ///
682
    /// # Examples
683
    ///
684
    /// ```rust,ignore
685
    /// let text = tokenizer.decode(&[1, 2])?;
686
    /// assert_eq!(text, "hello world");
687
    /// ```
688
5
    pub fn decode(&self, token_ids: &[u32]) -> Result<String> {
689
5
        let tokens: Result<Vec<&str>> = token_ids
690
5
            .iter()
691
16
            .
map5
(|&id| {
692
16
                self.vocab
693
16
                    .get_token(id)
694
16
                    .ok_or_else(|| RealizarError::UnsupportedOperation {
695
1
                        operation: "decode_token".to_string(),
696
1
                        reason: format!("Invalid token ID: {id}"),
697
1
                    })
698
16
            })
699
5
            .collect();
700
701
5
        Ok(tokens
?1
.
join4
(
" "4
))
702
5
    }
703
704
    /// Get vocabulary size
705
    #[must_use]
706
1
    pub fn vocab_size(&self) -> usize {
707
1
        self.vocab.size()
708
1
    }
709
}
710
711
#[cfg(test)]
712
mod tests {
713
    use super::*;
714
715
    #[test]
716
1
    fn test_vocabulary_from_tokens() {
717
1
        let tokens = vec![
718
1
            "<unk>".to_string(),
719
1
            "hello".to_string(),
720
1
            "world".to_string(),
721
        ];
722
723
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
724
1
        assert_eq!(vocab.size(), 3);
725
1
        assert_eq!(vocab.get_id("<unk>"), Some(0));
726
1
        assert_eq!(vocab.get_id("hello"), Some(1));
727
1
        assert_eq!(vocab.get_id("world"), Some(2));
728
1
        assert_eq!(vocab.get_token(0), Some("<unk>"));
729
1
        assert_eq!(vocab.get_token(1), Some("hello"));
730
1
        assert_eq!(vocab.get_token(2), Some("world"));
731
1
    }
732
733
    #[test]
734
1
    fn test_vocabulary_empty_error() {
735
1
        let result = Vocabulary::from_tokens(vec![]);
736
1
        assert!(result.is_err());
737
1
    }
738
739
    #[test]
740
1
    fn test_vocabulary_duplicate_error() {
741
1
        let tokens = vec![
742
1
            "hello".to_string(),
743
1
            "world".to_string(),
744
1
            "hello".to_string(), // Duplicate
745
        ];
746
1
        let result = Vocabulary::from_tokens(tokens);
747
1
        assert!(result.is_err());
748
1
    }
749
750
    #[test]
751
1
    fn test_vocabulary_get_missing() {
752
1
        let tokens = vec!["hello".to_string()];
753
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
754
1
        assert_eq!(vocab.get_id("world"), None);
755
1
        assert_eq!(vocab.get_token(999), None);
756
1
    }
757
758
    #[test]
759
1
    fn test_tokenizer_encode_decode() {
760
1
        let tokens = vec![
761
1
            "<unk>".to_string(),
762
1
            "hello".to_string(),
763
1
            "world".to_string(),
764
        ];
765
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
766
1
        let tokenizer = Tokenizer::new(vocab, "<unk>").expect("test");
767
768
        // Encode known tokens
769
1
        let encoded = tokenizer.encode("hello world");
770
1
        assert_eq!(encoded, vec![1, 2]);
771
772
        // Decode back
773
1
        let decoded = tokenizer.decode(&encoded).expect("test");
774
1
        assert_eq!(decoded, "hello world");
775
1
    }
776
777
    #[test]
778
1
    fn test_tokenizer_unknown_token() {
779
1
        let tokens = vec!["<unk>".to_string(), "hello".to_string()];
780
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
781
1
        let tokenizer = Tokenizer::new(vocab, "<unk>").expect("test");
782
783
        // Unknown token should map to <unk> (ID 0)
784
1
        let encoded = tokenizer.encode("hello foo");
785
1
        assert_eq!(encoded, vec![1, 0]);
786
1
    }
787
788
    #[test]
789
1
    fn test_tokenizer_invalid_unk_token() {
790
1
        let tokens = vec!["hello".to_string()];
791
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
792
1
        let result = Tokenizer::new(vocab, "<unk>");
793
1
        assert!(result.is_err());
794
1
    }
795
796
    #[test]
797
1
    fn test_tokenizer_decode_invalid_id() {
798
1
        let tokens = vec!["<unk>".to_string(), "hello".to_string()];
799
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
800
1
        let tokenizer = Tokenizer::new(vocab, "<unk>").expect("test");
801
802
1
        let result = tokenizer.decode(&[1, 999]); // 999 is invalid
803
1
        assert!(result.is_err());
804
1
    }
805
806
    #[test]
807
1
    fn test_tokenizer_empty_string() {
808
1
        let tokens = vec!["<unk>".to_string()];
809
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
810
1
        let tokenizer = Tokenizer::new(vocab, "<unk>").expect("test");
811
812
1
        let encoded = tokenizer.encode("");
813
1
        assert_eq!(encoded, Vec::<u32>::new());
814
815
1
        let decoded = tokenizer.decode(&[]).expect("test");
816
1
        assert_eq!(decoded, "");
817
1
    }
818
819
    #[test]
820
1
    fn test_tokenizer_vocab_size() {
821
1
        let tokens = vec![
822
1
            "<unk>".to_string(),
823
1
            "hello".to_string(),
824
1
            "world".to_string(),
825
        ];
826
1
        let vocab = Vocabulary::from_tokens(tokens).expect("test");
827
1
        let tokenizer = Tokenizer::new(vocab, "<unk>").expect("test");
828
829
1
        assert_eq!(tokenizer.vocab_size(), 3);
830
1
    }
831
832
    // BPE Tokenizer tests
833
834
    #[test]
835
1
    fn test_bpe_tokenizer_creation() {
836
1
        let vocab = vec![
837
1
            "<unk>".to_string(),
838
1
            "h".to_string(),
839
1
            "e".to_string(),
840
1
            "l".to_string(),
841
1
            "o".to_string(),
842
1
            "he".to_string(),
843
1
            "ll".to_string(),
844
1
            "hel".to_string(),
845
1
            "hello".to_string(),
846
        ];
847
1
        let merges = vec![
848
1
            ("h".to_string(), "e".to_string()),
849
1
            ("l".to_string(), "l".to_string()),
850
1
            ("he".to_string(), "l".to_string()),
851
1
            ("hel".to_string(), "lo".to_string()),
852
        ];
853
854
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
855
1
        assert_eq!(tokenizer.vocab_size(), 9);
856
1
    }
857
858
    #[test]
859
1
    fn test_bpe_tokenizer_empty_vocab_error() {
860
1
        let result = BPETokenizer::new(vec![], vec![], "<unk>");
861
1
        assert!(result.is_err());
862
1
    }
863
864
    #[test]
865
1
    fn test_bpe_tokenizer_invalid_unk_token_error() {
866
1
        let vocab = vec!["hello".to_string()];
867
1
        let result = BPETokenizer::new(vocab, vec![], "<unk>");
868
1
        assert!(result.is_err());
869
1
    }
870
871
    #[test]
872
1
    fn test_bpe_encode_no_merges() {
873
        // Simple character-level tokenization without merges
874
1
        let vocab = vec!["<unk>".to_string(), "h".to_string(), "i".to_string()];
875
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
876
877
1
        let encoded = tokenizer.encode("hi");
878
1
        assert_eq!(encoded, vec![1, 2]); // h=1, i=2
879
1
    }
880
881
    #[test]
882
1
    fn test_bpe_encode_with_merges() {
883
1
        let vocab = vec![
884
1
            "<unk>".to_string(),
885
1
            "h".to_string(),
886
1
            "e".to_string(),
887
1
            "l".to_string(),
888
1
            "o".to_string(),
889
1
            "he".to_string(),
890
1
            "ll".to_string(),
891
        ];
892
1
        let merges = vec![
893
1
            ("h".to_string(), "e".to_string()),
894
1
            ("l".to_string(), "l".to_string()),
895
        ];
896
897
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
898
1
        let encoded = tokenizer.encode("hello");
899
        // h+e -> he, l+l -> ll, o stays
900
        // so: he, ll, o = [5, 6, 4]
901
1
        assert_eq!(encoded, vec![5, 6, 4]);
902
1
    }
903
904
    #[test]
905
1
    fn test_bpe_encode_unknown_char() {
906
1
        let vocab = vec!["<unk>".to_string(), "h".to_string(), "i".to_string()];
907
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
908
909
        // 'x' is not in vocab, should map to <unk>
910
1
        let encoded = tokenizer.encode("hix");
911
1
        assert_eq!(encoded, vec![1, 2, 0]);
912
1
    }
913
914
    #[test]
915
1
    fn test_bpe_encode_empty_string() {
916
1
        let vocab = vec!["<unk>".to_string()];
917
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
918
919
1
        let encoded = tokenizer.encode("");
920
1
        assert!(encoded.is_empty());
921
1
    }
922
923
    #[test]
924
1
    fn test_bpe_encode_multiple_words() {
925
        // BPE uses GPT-2 encoding: space -> Ġ (U+0120)
926
1
        let vocab = vec![
927
1
            "<unk>".to_string(),
928
1
            "h".to_string(),
929
1
            "i".to_string(),
930
1
            "Ġ".to_string(),  // GPT-2 space encoding
931
1
            "Ġh".to_string(), // GPT-2 space + h
932
        ];
933
1
        let merges = vec![("Ġ".to_string(), "h".to_string())];
934
935
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
936
        // "hi hi" -> "hi" + " hi" (space becomes Ġ)
937
        // "hi" -> h, i
938
        // "Ġhi" -> "Ġ" + "h" -> "Ġh", then "i"
939
1
        let encoded = tokenizer.encode("hi hi");
940
1
        assert_eq!(encoded, vec![1, 2, 4, 2]); // h, i, "Ġh", i
941
1
    }
942
943
    #[test]
944
1
    fn test_bpe_decode() {
945
1
        let vocab = vec!["<unk>".to_string(), "hel".to_string(), "lo".to_string()];
946
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
947
948
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
949
1
        assert_eq!(decoded, "hello");
950
1
    }
951
952
    #[test]
953
1
    fn test_bpe_decode_empty() {
954
1
        let vocab = vec!["<unk>".to_string()];
955
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
956
957
1
        let decoded = tokenizer.decode(&[]).expect("test");
958
1
        assert_eq!(decoded, "");
959
1
    }
960
961
    #[test]
962
1
    fn test_bpe_decode_invalid_id_error() {
963
1
        let vocab = vec!["<unk>".to_string(), "hi".to_string()];
964
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
965
966
1
        let result = tokenizer.decode(&[1, 999]);
967
1
        assert!(result.is_err());
968
1
    }
969
970
    #[test]
971
1
    fn test_bpe_encode_decode_roundtrip() {
972
1
        let vocab = vec![
973
1
            "<unk>".to_string(),
974
1
            "h".to_string(),
975
1
            "e".to_string(),
976
1
            "l".to_string(),
977
1
            "o".to_string(),
978
1
            "he".to_string(),
979
1
            "ll".to_string(),
980
1
            "lo".to_string(),
981
1
            "hel".to_string(),
982
1
            "hello".to_string(),
983
        ];
984
1
        let merges = vec![
985
1
            ("h".to_string(), "e".to_string()),
986
1
            ("l".to_string(), "l".to_string()),
987
1
            ("l".to_string(), "o".to_string()),
988
1
            ("he".to_string(), "l".to_string()),
989
1
            ("hel".to_string(), "lo".to_string()),
990
        ];
991
992
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
993
994
1
        let encoded = tokenizer.encode("hello");
995
1
        let decoded = tokenizer.decode(&encoded).expect("test");
996
1
        assert_eq!(decoded, "hello");
997
1
    }
998
999
    #[test]
1000
1
    fn test_bpe_get_token_methods() {
1001
1
        let vocab = vec!["<unk>".to_string(), "hello".to_string()];
1002
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1003
1004
1
        assert_eq!(tokenizer.get_token_id("hello"), Some(1));
1005
1
        assert_eq!(tokenizer.get_token_id("world"), None);
1006
1
        assert_eq!(tokenizer.get_token(1), Some("hello"));
1007
1
        assert_eq!(tokenizer.get_token(999), None);
1008
1
    }
1009
1010
    #[test]
1011
1
    fn test_bpe_multiple_consecutive_merges() {
1012
        // Test that multiple merges are applied correctly
1013
1
        let vocab = vec![
1014
1
            "<unk>".to_string(),
1015
1
            "a".to_string(),
1016
1
            "b".to_string(),
1017
1
            "ab".to_string(),
1018
1
            "abab".to_string(),
1019
        ];
1020
1
        let merges = vec![
1021
1
            ("a".to_string(), "b".to_string()),
1022
1
            ("ab".to_string(), "ab".to_string()),
1023
        ];
1024
1025
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
1026
1
        let encoded = tokenizer.encode("abab");
1027
        // First: a+b -> ab, a+b -> ab giving [ab, ab]
1028
        // Then: ab+ab -> abab giving [abab]
1029
1
        assert_eq!(encoded, vec![4]);
1030
1
    }
1031
1032
    // SentencePiece Tokenizer tests
1033
1034
    #[test]
1035
1
    fn test_sentencepiece_tokenizer_creation() {
1036
1
        let vocab = vec![
1037
1
            ("<unk>".to_string(), 0.0),
1038
1
            ("hello".to_string(), -1.0),
1039
1
            ("world".to_string(), -1.5),
1040
        ];
1041
1042
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1043
1
        assert_eq!(tokenizer.vocab_size(), 3);
1044
1
    }
1045
1046
    #[test]
1047
1
    fn test_sentencepiece_empty_vocab_error() {
1048
1
        let result = SentencePieceTokenizer::new(vec![], "<unk>");
1049
1
        assert!(result.is_err());
1050
1
    }
1051
1052
    #[test]
1053
1
    fn test_sentencepiece_invalid_unk_token_error() {
1054
1
        let vocab = vec![("hello".to_string(), -1.0)];
1055
1
        let result = SentencePieceTokenizer::new(vocab, "<unk>");
1056
1
        assert!(result.is_err());
1057
1
    }
1058
1059
    #[test]
1060
1
    fn test_sentencepiece_encode_empty() {
1061
1
        let vocab = vec![("<unk>".to_string(), 0.0)];
1062
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1063
1064
1
        let encoded = tokenizer.encode("");
1065
1
        assert!(encoded.is_empty());
1066
1
    }
1067
1068
    #[test]
1069
1
    fn test_sentencepiece_encode_single_token() {
1070
1
        let vocab = vec![("<unk>".to_string(), 0.0), ("hello".to_string(), -1.0)];
1071
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1072
1073
1
        let encoded = tokenizer.encode("hello");
1074
1
        assert_eq!(encoded, vec![1]);
1075
1
    }
1076
1077
    #[test]
1078
1
    fn test_sentencepiece_encode_prefers_higher_score() {
1079
        // "hello" as single token has score -1.0
1080
        // "hel" + "lo" would have score -2.0 + -2.0 = -4.0
1081
        // So "hello" should be preferred
1082
1
        let vocab = vec![
1083
1
            ("<unk>".to_string(), 0.0),
1084
1
            ("h".to_string(), -5.0),
1085
1
            ("e".to_string(), -5.0),
1086
1
            ("l".to_string(), -5.0),
1087
1
            ("o".to_string(), -5.0),
1088
1
            ("hel".to_string(), -2.0),
1089
1
            ("lo".to_string(), -2.0),
1090
1
            ("hello".to_string(), -1.0),
1091
        ];
1092
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1093
1094
1
        let encoded = tokenizer.encode("hello");
1095
        // Should prefer single "hello" token (score -1.0) over subwords
1096
1
        assert_eq!(encoded, vec![7]);
1097
1
    }
1098
1099
    #[test]
1100
1
    fn test_sentencepiece_encode_subwords() {
1101
        // Only subwords available, not full word
1102
1
        let vocab = vec![
1103
1
            ("<unk>".to_string(), 0.0),
1104
1
            ("h".to_string(), -1.0),
1105
1
            ("e".to_string(), -1.0),
1106
1
            ("l".to_string(), -1.0),
1107
1
            ("o".to_string(), -1.0),
1108
1
            ("he".to_string(), -0.5),
1109
1
            ("llo".to_string(), -0.5),
1110
        ];
1111
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1112
1113
1
        let encoded = tokenizer.encode("hello");
1114
        // "he" (-0.5) + "llo" (-0.5) = -1.0 is better than "h" + "e" + "l" + "l" + "o" = -5.0
1115
1
        assert_eq!(encoded, vec![5, 6]);
1116
1
    }
1117
1118
    #[test]
1119
1
    fn test_sentencepiece_decode() {
1120
1
        let vocab = vec![
1121
1
            ("<unk>".to_string(), 0.0),
1122
1
            ("hel".to_string(), -1.0),
1123
1
            ("lo".to_string(), -1.0),
1124
        ];
1125
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1126
1127
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
1128
1
        assert_eq!(decoded, "hello");
1129
1
    }
1130
1131
    #[test]
1132
1
    fn test_sentencepiece_decode_empty() {
1133
1
        let vocab = vec![("<unk>".to_string(), 0.0)];
1134
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1135
1136
1
        let decoded = tokenizer.decode(&[]).expect("test");
1137
1
        assert_eq!(decoded, "");
1138
1
    }
1139
1140
    #[test]
1141
1
    fn test_sentencepiece_decode_invalid_id_error() {
1142
1
        let vocab = vec![("<unk>".to_string(), 0.0), ("hi".to_string(), -1.0)];
1143
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1144
1145
1
        let result = tokenizer.decode(&[1, 999]);
1146
1
        assert!(result.is_err());
1147
1
    }
1148
1149
    #[test]
1150
1
    fn test_sentencepiece_encode_decode_roundtrip() {
1151
1
        let vocab = vec![
1152
1
            ("<unk>".to_string(), 0.0),
1153
1
            ("h".to_string(), -2.0),
1154
1
            ("e".to_string(), -2.0),
1155
1
            ("l".to_string(), -2.0),
1156
1
            ("o".to_string(), -2.0),
1157
1
            ("hello".to_string(), -1.0),
1158
        ];
1159
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1160
1161
1
        let encoded = tokenizer.encode("hello");
1162
1
        let decoded = tokenizer.decode(&encoded).expect("test");
1163
1
        assert_eq!(decoded, "hello");
1164
1
    }
1165
1166
    #[test]
1167
1
    fn test_sentencepiece_get_methods() {
1168
1
        let vocab = vec![("<unk>".to_string(), 0.0), ("hello".to_string(), -1.5)];
1169
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1170
1171
1
        assert_eq!(tokenizer.get_token_id("hello"), Some(1));
1172
1
        assert_eq!(tokenizer.get_token_id("world"), None);
1173
1
        assert_eq!(tokenizer.get_token(1), Some("hello"));
1174
1
        assert_eq!(tokenizer.get_token(999), None);
1175
1
        assert!((tokenizer.get_score("hello").expect("test") - (-1.5)).abs() < 1e-6);
1176
1
        assert_eq!(tokenizer.get_score("world"), None);
1177
1
    }
1178
1179
    #[test]
1180
1
    fn test_sentencepiece_unknown_character() {
1181
        // Character not in vocabulary should use unknown penalty
1182
1
        let vocab = vec![
1183
1
            ("<unk>".to_string(), 0.0),
1184
1
            ("h".to_string(), -1.0),
1185
1
            ("i".to_string(), -1.0),
1186
        ];
1187
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1188
1189
        // 'x' is not in vocab, should be tokenized with penalty
1190
1
        let encoded = tokenizer.encode("hix");
1191
1
        assert_eq!(encoded.len(), 3);
1192
1
        assert_eq!(encoded[0], 1); // h
1193
1
        assert_eq!(encoded[1], 2); // i
1194
                                   // x should map to unk
1195
1
        assert_eq!(encoded[2], 0);
1196
1
    }
1197
1198
    #[test]
1199
1
    fn test_sentencepiece_multiple_words() {
1200
1
        let vocab = vec![
1201
1
            ("<unk>".to_string(), 0.0),
1202
1
            ("hello".to_string(), -1.0),
1203
1
            (" ".to_string(), -0.5),
1204
1
            ("world".to_string(), -1.0),
1205
        ];
1206
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1207
1208
1
        let encoded = tokenizer.encode("hello world");
1209
1
        assert_eq!(encoded, vec![1, 2, 3]); // hello, space, world
1210
1
    }
1211
1212
    // -------------------------------------------------------------------------
1213
    // Additional BPE Decode Tests (95% coverage push)
1214
    // -------------------------------------------------------------------------
1215
1216
    #[test]
1217
1
    fn test_bpe_decode_special_tokens() {
1218
1
        let vocab = vec![
1219
1
            "<unk>".to_string(),
1220
1
            "<|endoftext|>".to_string(),
1221
1
            "<s>".to_string(),
1222
1
            "</s>".to_string(),
1223
1
            "<pad>".to_string(),
1224
1
            "hello".to_string(),
1225
        ];
1226
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1227
1228
        // Special tokens should be skipped in decode
1229
1
        let decoded = tokenizer.decode(&[1, 2, 3, 4, 5]).expect("test");
1230
1
        assert_eq!(decoded, "hello"); // Only "hello" should remain
1231
1
    }
1232
1233
    #[test]
1234
1
    fn test_bpe_decode_byte_tokens() {
1235
1
        let vocab = vec![
1236
1
            "<unk>".to_string(),
1237
1
            "<0xE6>".to_string(), // UTF-8 first byte of some CJK chars
1238
1
            "<0x97>".to_string(),
1239
1
            "<0xA5>".to_string(),
1240
        ];
1241
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1242
1243
        // These three bytes form the UTF-8 sequence for "日" (U+65E5)
1244
1
        let decoded = tokenizer.decode(&[1, 2, 3]).expect("test");
1245
1
        assert_eq!(decoded, "日");
1246
1
    }
1247
1248
    #[test]
1249
1
    fn test_bpe_decode_gpt2_space() {
1250
1
        let vocab = vec![
1251
1
            "<unk>".to_string(),
1252
1
            "Ġhello".to_string(), // Ġ = space prefix in GPT-2
1253
        ];
1254
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1255
1256
1
        let decoded = tokenizer.decode(&[1]).expect("test");
1257
1
        assert_eq!(decoded, " hello");
1258
1
    }
1259
1260
    #[test]
1261
1
    fn test_bpe_decode_gpt2_newline() {
1262
1
        let vocab = vec![
1263
1
            "<unk>".to_string(),
1264
1
            "Ċ".to_string(), // newline in GPT-2
1265
1
            "ċ".to_string(), // lowercase variant
1266
        ];
1267
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1268
1269
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
1270
1
        assert_eq!(decoded, "\n\n");
1271
1
    }
1272
1273
    #[test]
1274
1
    fn test_bpe_decode_sentencepiece_space() {
1275
1
        let vocab = vec![
1276
1
            "<unk>".to_string(),
1277
1
            "▁hello".to_string(), // SentencePiece space
1278
        ];
1279
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1280
1281
1
        let decoded = tokenizer.decode(&[1]).expect("test");
1282
1
        assert_eq!(decoded, " hello");
1283
1
    }
1284
1285
    #[test]
1286
1
    fn test_bpe_decode_gpt2_carriage_return() {
1287
1
        let vocab = vec![
1288
1
            "<unk>".to_string(),
1289
1
            "Ḃ".to_string(), // carriage return in GPT-2
1290
1
            "a".to_string(),
1291
        ];
1292
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1293
1294
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
1295
1
        assert_eq!(decoded, "\ra");
1296
1
    }
1297
1298
    #[test]
1299
1
    fn test_bpe_decode_regular_utf8() {
1300
1
        let vocab = vec![
1301
1
            "<unk>".to_string(),
1302
1
            "こんにちは".to_string(), // Japanese
1303
        ];
1304
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1305
1306
1
        let decoded = tokenizer.decode(&[1]).expect("test");
1307
1
        assert_eq!(decoded, "こんにちは");
1308
1
    }
1309
1310
    #[test]
1311
1
    fn test_bpe_decode_invalid_byte_token() {
1312
1
        let vocab = vec![
1313
1
            "<unk>".to_string(),
1314
1
            "<0xGG>".to_string(), // Invalid hex
1315
        ];
1316
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1317
1318
        // Should not panic, just treat as regular token
1319
1
        let decoded = tokenizer.decode(&[1]).expect("test");
1320
1
        assert!(decoded.contains("<0xGG>"));
1321
1
    }
1322
1323
    #[test]
1324
1
    fn test_bpe_decode_mixed_tokens() {
1325
1
        let vocab = vec![
1326
1
            "<unk>".to_string(),
1327
1
            "Ġhello".to_string(), // GPT-2 space + word
1328
1
            "Ċ".to_string(),      // newline
1329
1
            "world".to_string(),
1330
        ];
1331
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1332
1333
1
        let decoded = tokenizer.decode(&[1, 2, 3]).expect("test");
1334
1
        assert_eq!(decoded, " hello\nworld");
1335
1
    }
1336
1337
    #[test]
1338
1
    fn test_bpe_gpt2_char_to_byte_printable_ascii() {
1339
        // Test that printable ASCII is preserved
1340
1
        let vocab = vec!["<unk>".to_string(), "a".to_string(), "!".to_string()];
1341
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1342
1343
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
1344
1
        assert_eq!(decoded, "a!");
1345
1
    }
1346
1347
    #[test]
1348
1
    fn test_bpe_gpt2_char_to_byte_space() {
1349
        // Space (ASCII 32) should be preserved
1350
1
        let vocab = vec!["<unk>".to_string(), " ".to_string(), "a".to_string()];
1351
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1352
1353
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
1354
1
        assert_eq!(decoded, " a");
1355
1
    }
1356
1357
    #[test]
1358
1
    fn test_sentencepiece_vocab_size() {
1359
1
        let vocab = vec![
1360
1
            ("<unk>".to_string(), 0.0),
1361
1
            ("a".to_string(), -1.0),
1362
1
            ("b".to_string(), -1.0),
1363
        ];
1364
1
        let tokenizer = SentencePieceTokenizer::new(vocab, "<unk>").expect("test");
1365
1366
1
        assert_eq!(tokenizer.vocab_size(), 3);
1367
1
    }
1368
1369
    // =========================================================================
1370
    // Additional 95% Coverage Tests
1371
    // =========================================================================
1372
1373
    #[test]
1374
1
    fn test_cov95_bpe_apply_merge_single_token() {
1375
        // Test apply_merge with single token (early return path)
1376
1
        let vocab = vec![
1377
1
            "<unk>".to_string(),
1378
1
            "a".to_string(),
1379
1
            "b".to_string(),
1380
1
            "ab".to_string(),
1381
        ];
1382
1
        let merges = vec![("a".to_string(), "b".to_string())];
1383
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
1384
1385
        // Encode single char - no merge possible
1386
1
        let encoded = tokenizer.encode("a");
1387
1
        assert_eq!(encoded, vec![1]);
1388
1
    }
1389
1390
    #[test]
1391
1
    fn test_cov95_bpe_apply_merge_no_match() {
1392
        // Test apply_merge when merge pair doesn't match
1393
1
        let vocab = vec![
1394
1
            "<unk>".to_string(),
1395
1
            "x".to_string(),
1396
1
            "y".to_string(),
1397
1
            "z".to_string(),
1398
1
            "ab".to_string(),
1399
        ];
1400
1
        let merges = vec![("a".to_string(), "b".to_string())]; // merge won't match "xyz"
1401
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
1402
1403
        // "xyz" has no matching merge pairs
1404
1
        let encoded = tokenizer.encode("xyz");
1405
1
        assert_eq!(encoded, vec![1, 2, 3]);
1406
1
    }
1407
1408
    #[test]
1409
1
    fn test_cov95_bpe_apply_merge_consecutive() {
1410
        // Test apply_merge with consecutive matches
1411
1
        let vocab = vec![
1412
1
            "<unk>".to_string(),
1413
1
            "a".to_string(),
1414
1
            "b".to_string(),
1415
1
            "ab".to_string(),
1416
        ];
1417
1
        let merges = vec![("a".to_string(), "b".to_string())];
1418
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
1419
1420
        // "abab" should merge to "ab" + "ab"
1421
1
        let encoded = tokenizer.encode("abab");
1422
1
        assert_eq!(encoded, vec![3, 3]); // "ab", "ab"
1423
1
    }
1424
1425
    #[test]
1426
1
    fn test_cov95_bpe_decode_gpt2_remapped_bytes() {
1427
        // Test decode with GPT-2 remapped byte tokens (0x100+ range)
1428
        // GPT-2 uses chars starting at U+0100 for raw bytes
1429
1
        let vocab = vec![
1430
1
            "<unk>".to_string(),
1431
1
            "\u{0100}".to_string(), // maps to byte 0
1432
1
            "\u{0101}".to_string(), // maps to byte 1
1433
1
            "\u{0120}".to_string(), // maps to byte 32 (space in GPT-2)
1434
        ];
1435
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1436
1437
        // Decode tokens with GPT-2 byte remapping
1438
1
        let decoded = tokenizer.decode(&[3]).expect("test");
1439
        // U+0120 should decode to space (byte 32)
1440
1
        assert!(decoded.contains('\u{0120}') || decoded.contains(' '));
1441
1
    }
1442
1443
    #[test]
1444
1
    fn test_cov95_bpe_decode_high_unicode_byte() {
1445
        // Test decode with high unicode that maps to byte via GPT-2 encoding
1446
        // GPT-2 remaps bytes 127-160 to U+0100 + offset
1447
1
        let vocab = vec![
1448
1
            "<unk>".to_string(),
1449
1
            "\u{017F}".to_string(), // Should map to byte 127 in GPT-2 encoding
1450
1
            "\u{01A0}".to_string(), // Should map to byte 160 in GPT-2 encoding
1451
        ];
1452
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1453
1454
        // These tokens should decode (possibly with replacement chars)
1455
1
        let decoded = tokenizer.decode(&[1, 2]).expect("test");
1456
1
        assert!(!decoded.is_empty());
1457
1
    }
1458
1459
    #[test]
1460
1
    fn test_cov95_bpe_decode_soft_hyphen() {
1461
        // Test decode with soft hyphen (byte 173)
1462
1
        let vocab = vec![
1463
1
            "<unk>".to_string(),
1464
1
            "\u{01AD}".to_string(), // U+0100 + 173 = U+01AD for soft hyphen in GPT-2
1465
        ];
1466
1
        let tokenizer = BPETokenizer::new(vocab, vec![], "<unk>").expect("test");
1467
1468
1
        let decoded = tokenizer.decode(&[1]).expect("test");
1469
1
        assert!(!decoded.is_empty());
1470
1
    }
1471
1472
    #[test]
1473
1
    fn test_cov95_bpe_encode_with_multiple_merges() {
1474
        // Test encoding that exercises multiple merge iterations
1475
1
        let vocab = vec![
1476
1
            "<unk>".to_string(),
1477
1
            "a".to_string(),
1478
1
            "b".to_string(),
1479
1
            "c".to_string(),
1480
1
            "ab".to_string(),
1481
1
            "abc".to_string(),
1482
        ];
1483
1
        let merges = vec![
1484
1
            ("a".to_string(), "b".to_string()),
1485
1
            ("ab".to_string(), "c".to_string()),
1486
        ];
1487
1
        let tokenizer = BPETokenizer::new(vocab, merges, "<unk>").expect("test");
1488
1489
1
        let encoded = tokenizer.encode("abc");
1490
        // First merge: a+b -> ab, then ab+c -> abc
1491
1
        assert_eq!(encoded, vec![5]); // "abc"
1492
1
    }
1493
}