gigatoken 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +3016 -0
  3. data/Cargo.toml +135 -0
  4. data/LICENSE +21 -0
  5. data/README.md +141 -0
  6. data/exe/gigatoken +9 -0
  7. data/ext/gigatoken/Cargo.toml +24 -0
  8. data/ext/gigatoken/extconf.rb +19 -0
  9. data/ext/gigatoken/src/error.rs +20 -0
  10. data/ext/gigatoken/src/gvl.rs +122 -0
  11. data/ext/gigatoken/src/lib.rs +50 -0
  12. data/ext/gigatoken/src/sentencepiece.rs +205 -0
  13. data/ext/gigatoken/src/sources.rs +207 -0
  14. data/ext/gigatoken/src/tokenizer.rs +571 -0
  15. data/lib/gigatoken/cli/bench.rb +64 -0
  16. data/lib/gigatoken/cli/support.rb +132 -0
  17. data/lib/gigatoken/cli/validate.rb +55 -0
  18. data/lib/gigatoken/cli.rb +18 -0
  19. data/lib/gigatoken/hub.rb +242 -0
  20. data/lib/gigatoken/packed_result.rb +48 -0
  21. data/lib/gigatoken/tokenizer.rb +117 -0
  22. data/lib/gigatoken/version.rb +5 -0
  23. data/lib/gigatoken.rb +29 -0
  24. data/rust-toolchain.toml +8 -0
  25. data/src/batch.rs +1808 -0
  26. data/src/bindings/bridge.rs +396 -0
  27. data/src/bindings/hub.rs +42 -0
  28. data/src/bindings/matcher.rs +114 -0
  29. data/src/bindings/mod.rs +14 -0
  30. data/src/bindings/padding.rs +177 -0
  31. data/src/bindings/pretokenize.rs +53 -0
  32. data/src/bindings/sources.rs +273 -0
  33. data/src/bindings/train.rs +125 -0
  34. data/src/bpe/mod.rs +1217 -0
  35. data/src/bpe/pretoken_cache.rs +495 -0
  36. data/src/bpe/sentencepiece.rs +1485 -0
  37. data/src/bpe/tiktoken.rs +2555 -0
  38. data/src/bpe_train.rs +351 -0
  39. data/src/input/decompress.rs +11 -0
  40. data/src/input/file_source.rs +514 -0
  41. data/src/input/jsonl.rs +94 -0
  42. data/src/input/mod.rs +333 -0
  43. data/src/input/parquet.rs +303 -0
  44. data/src/lib.rs +578 -0
  45. data/src/load_tokenizer/hf.rs +1036 -0
  46. data/src/load_tokenizer/hub.rs +344 -0
  47. data/src/load_tokenizer/mod.rs +3 -0
  48. data/src/load_tokenizer/tiktoken.rs +87 -0
  49. data/src/main.rs +95 -0
  50. data/src/pretokenize/fast/cl100k.rs +426 -0
  51. data/src/pretokenize/fast/cl100k_family.rs +891 -0
  52. data/src/pretokenize/fast/deepseek_v3.rs +605 -0
  53. data/src/pretokenize/fast/kimi.rs +281 -0
  54. data/src/pretokenize/fast/mask.rs +1486 -0
  55. data/src/pretokenize/fast/mod.rs +446 -0
  56. data/src/pretokenize/fast/nemotron.rs +138 -0
  57. data/src/pretokenize/fast/o200k.rs +347 -0
  58. data/src/pretokenize/fast/o200k_family.rs +1734 -0
  59. data/src/pretokenize/fast/olmo3.rs +505 -0
  60. data/src/pretokenize/fast/qwen2.rs +429 -0
  61. data/src/pretokenize/fast/qwen3_5.rs +541 -0
  62. data/src/pretokenize/fast/r50k.rs +1250 -0
  63. data/src/pretokenize/mod.rs +1079 -0
  64. data/src/pretokenize/options.rs +188 -0
  65. data/src/pretokenize/pretoken.rs +20 -0
  66. data/src/pretokenize/pretokenize_traits.rs +49 -0
  67. data/src/pretokenize/reference/avx512.rs +522 -0
  68. data/src/pretokenize/reference/combinator.rs +572 -0
  69. data/src/pretokenize/reference/mod.rs +28 -0
  70. data/src/pretokenize/reference/simd.rs +852 -0
  71. data/src/pretokenize/reference/state_machine.rs +365 -0
  72. data/src/pretokenize/unicode.rs +546 -0
  73. data/src/test_hub.rs +28 -0
  74. data/src/token.rs +42 -0
  75. metadata +161 -0
@@ -0,0 +1,572 @@
1
+ //! Implement the regex
2
+ //! '(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"
3
+ //! using winnow parser combinators.
4
+ use crate::pretokenize::{Pretoken, unicode};
5
+ use std::cmp::min;
6
+
7
+ use eyre::Context;
8
+ use itertools::Itertools;
9
+ use rayon::prelude::*;
10
+ use winnow::Parser;
11
+ use winnow::combinator::{alt, iterator, trace};
12
+ use winnow::prelude::*;
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // NEON scan utilities — find the end of a run of matching ASCII bytes,
16
+ // processing 16 bytes at a time with precise exit via trailing_zeros.
17
+ // ---------------------------------------------------------------------------
18
+
19
+ #[cfg(target_arch = "aarch64")]
20
+ mod neon {
21
+ use std::arch::aarch64::*;
22
+
23
+ /// Return the byte index (relative to the chunk start) of the first lane
24
+ /// where `mask` is non-zero, or 16 if all lanes are zero.
25
+ #[inline(always)]
26
+ unsafe fn first_nonzero_lane(mask: uint8x16_t) -> usize {
27
+ unsafe {
28
+ let lo = vgetq_lane_u64::<0>(vreinterpretq_u64_u8(mask));
29
+ if lo != 0 {
30
+ return (lo.trailing_zeros() as usize) / 8;
31
+ }
32
+ let hi = vgetq_lane_u64::<1>(vreinterpretq_u64_u8(mask));
33
+ if hi != 0 {
34
+ return 8 + (hi.trailing_zeros() as usize) / 8;
35
+ }
36
+ 16
37
+ }
38
+ }
39
+
40
+ /// Scan ASCII letters: `(b | 0x20) - 'a' < 26`.
41
+ #[inline]
42
+ pub unsafe fn scan_ascii_letters(bytes: &[u8], start: usize) -> usize {
43
+ unsafe { scan_generic(bytes, start, is_ascii_letter_mask) }
44
+ }
45
+
46
+ /// Scan ASCII digits: `b - '0' < 10`.
47
+ #[inline]
48
+ pub unsafe fn scan_ascii_digits(bytes: &[u8], start: usize) -> usize {
49
+ unsafe { scan_generic(bytes, start, is_ascii_digit_mask) }
50
+ }
51
+
52
+ /// Scan ASCII "other" bytes: not letter, not digit, not whitespace, not >= 0x80.
53
+ #[inline]
54
+ pub unsafe fn scan_ascii_other(bytes: &[u8], start: usize) -> usize {
55
+ unsafe { scan_generic(bytes, start, is_ascii_other_mask) }
56
+ }
57
+
58
+ /// Generic NEON scanner. `classify` returns a mask with 0xFF for matching bytes.
59
+ /// Returns the index of the first non-matching byte.
60
+ #[inline(always)]
61
+ unsafe fn scan_generic(
62
+ bytes: &[u8],
63
+ start: usize,
64
+ classify: unsafe fn(uint8x16_t) -> uint8x16_t,
65
+ ) -> usize {
66
+ let mut i = start;
67
+ unsafe {
68
+ while i + 16 <= bytes.len() {
69
+ let chunk = vld1q_u8(bytes.as_ptr().add(i));
70
+ let is_match = classify(chunk);
71
+ let not_match = vmvnq_u8(is_match);
72
+ let pos = first_nonzero_lane(not_match);
73
+ if pos < 16 {
74
+ return i + pos;
75
+ }
76
+ i += 16;
77
+ }
78
+ }
79
+ i // caller handles the scalar tail
80
+ }
81
+
82
+ #[inline(always)]
83
+ unsafe fn is_ascii_letter_mask(chunk: uint8x16_t) -> uint8x16_t {
84
+ unsafe {
85
+ let lower = vorrq_u8(chunk, vdupq_n_u8(0x20));
86
+ let sub = vsubq_u8(lower, vdupq_n_u8(b'a'));
87
+ vcgtq_u8(vdupq_n_u8(26), sub) // 0xFF if letter
88
+ }
89
+ }
90
+
91
+ #[inline(always)]
92
+ unsafe fn is_ascii_digit_mask(chunk: uint8x16_t) -> uint8x16_t {
93
+ unsafe {
94
+ let sub = vsubq_u8(chunk, vdupq_n_u8(b'0'));
95
+ vcgtq_u8(vdupq_n_u8(10), sub)
96
+ }
97
+ }
98
+
99
+ #[inline(always)]
100
+ unsafe fn is_ascii_other_mask(chunk: uint8x16_t) -> uint8x16_t {
101
+ unsafe {
102
+ // "other" = NOT letter AND NOT digit AND NOT whitespace AND NOT non-ASCII
103
+ let is_letter = is_ascii_letter_mask(chunk);
104
+ let is_digit = is_ascii_digit_mask(chunk);
105
+ // Whitespace: 9-13 or 32
106
+ let ws_sub = vsubq_u8(chunk, vdupq_n_u8(9));
107
+ let is_ws_ctrl = vcgeq_u8(vdupq_n_u8(4), ws_sub); // 9..=13
108
+ let is_space = vceqq_u8(chunk, vdupq_n_u8(b' '));
109
+ let is_ws = vorrq_u8(is_ws_ctrl, is_space);
110
+ // Non-ASCII (>= 0x80)
111
+ let is_high = vcgeq_u8(chunk, vdupq_n_u8(0x80));
112
+ // Other = none of the above
113
+ let any_exclude = vorrq_u8(vorrq_u8(is_letter, is_digit), vorrq_u8(is_ws, is_high));
114
+ vmvnq_u8(any_exclude) // 0xFF where "other"
115
+ }
116
+ }
117
+ }
118
+
119
+ // Portable fallbacks
120
+ #[cfg(not(target_arch = "aarch64"))]
121
+ mod neon {
122
+ #[inline]
123
+ pub unsafe fn scan_ascii_letters(bytes: &[u8], start: usize) -> usize {
124
+ start // fall through to scalar immediately
125
+ }
126
+ #[inline]
127
+ pub unsafe fn scan_ascii_digits(bytes: &[u8], start: usize) -> usize {
128
+ start
129
+ }
130
+ #[inline]
131
+ pub unsafe fn scan_ascii_other(bytes: &[u8], start: usize) -> usize {
132
+ start
133
+ }
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Scalar helpers
138
+ // ---------------------------------------------------------------------------
139
+
140
+ #[inline(always)]
141
+ fn backtrack() -> winnow::error::ErrMode<winnow::error::ContextError> {
142
+ winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new())
143
+ }
144
+
145
+ /// Decode one char from a non-ASCII position in a byte slice.
146
+ /// Caller guarantees `bytes[0] >= 0x80` and `bytes` is valid UTF-8.
147
+ #[inline(always)]
148
+ unsafe fn decode_non_ascii(bytes: &[u8]) -> char {
149
+ unsafe {
150
+ std::str::from_utf8_unchecked(bytes)
151
+ .chars()
152
+ .next()
153
+ .unwrap_unchecked()
154
+ }
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // Individual parsers — each returns () and advances *input on success.
159
+ // ---------------------------------------------------------------------------
160
+
161
+ fn contraction(input: &mut &str) -> ModalResult<()> {
162
+ let bytes = input.as_bytes();
163
+ if bytes.first() != Some(&b'\'') {
164
+ return Err(backtrack());
165
+ }
166
+ let advance = match bytes.get(1) {
167
+ Some(b's' | b'd' | b'm' | b't') => 2,
168
+ Some(b'l') if bytes.get(2) == Some(&b'l') => 3,
169
+ Some(b'v') if bytes.get(2) == Some(&b'e') => 3,
170
+ Some(b'r') if bytes.get(2) == Some(&b'e') => 3,
171
+ _ => return Err(backtrack()),
172
+ };
173
+ *input = &input[advance..];
174
+ Ok(())
175
+ }
176
+
177
+ fn letter_run(input: &mut &str) -> ModalResult<()> {
178
+ let before = *input;
179
+ let bytes = before.as_bytes();
180
+ let mut i = if bytes.first() == Some(&b' ') { 1 } else { 0 };
181
+ let letter_start = i;
182
+
183
+ // NEON fast path (returns precise position of first non-ASCII-letter)
184
+ i = unsafe { neon::scan_ascii_letters(bytes, i) };
185
+ // Scalar tail for remaining < 16 bytes
186
+ while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
187
+ i += 1;
188
+ }
189
+
190
+ // Handle non-ASCII unicode letters, resuming fast scan after each one
191
+ while i < bytes.len() && bytes[i] >= 0x80 {
192
+ let c = unsafe { decode_non_ascii(&bytes[i..]) };
193
+ if unicode::is_letter(c) {
194
+ i += c.len_utf8();
195
+ i = unsafe { neon::scan_ascii_letters(bytes, i) };
196
+ while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
197
+ i += 1;
198
+ }
199
+ } else {
200
+ break;
201
+ }
202
+ }
203
+
204
+ if i == letter_start {
205
+ return Err(backtrack());
206
+ }
207
+ *input = &before[i..];
208
+ Ok(())
209
+ }
210
+
211
+ fn number_run(input: &mut &str) -> ModalResult<()> {
212
+ let before = *input;
213
+ let bytes = before.as_bytes();
214
+ let mut i = if bytes.first() == Some(&b' ') { 1 } else { 0 };
215
+ let start = i;
216
+
217
+ i = unsafe { neon::scan_ascii_digits(bytes, i) };
218
+ while i < bytes.len() && bytes[i].is_ascii_digit() {
219
+ i += 1;
220
+ }
221
+
222
+ while i < bytes.len() && bytes[i] >= 0x80 {
223
+ let c = unsafe { decode_non_ascii(&bytes[i..]) };
224
+ if unicode::is_number(c) {
225
+ i += c.len_utf8();
226
+ i = unsafe { neon::scan_ascii_digits(bytes, i) };
227
+ while i < bytes.len() && bytes[i].is_ascii_digit() {
228
+ i += 1;
229
+ }
230
+ } else {
231
+ break;
232
+ }
233
+ }
234
+
235
+ if i == start {
236
+ return Err(backtrack());
237
+ }
238
+ *input = &before[i..];
239
+ Ok(())
240
+ }
241
+
242
+ fn other_run(input: &mut &str) -> ModalResult<()> {
243
+ let before = *input;
244
+ let bytes = before.as_bytes();
245
+ let mut i = if bytes.first() == Some(&b' ') { 1 } else { 0 };
246
+ let start = i;
247
+
248
+ i = unsafe { neon::scan_ascii_other(bytes, i) };
249
+ while i < bytes.len() {
250
+ let b = bytes[i];
251
+ if b < 0x80 {
252
+ if !b.is_ascii_alphanumeric() && !b.is_ascii_whitespace() {
253
+ i += 1;
254
+ } else {
255
+ break;
256
+ }
257
+ } else {
258
+ break;
259
+ }
260
+ }
261
+
262
+ // Handle non-ASCII "other" chars (unicode symbols, etc.)
263
+ while i < bytes.len() && bytes[i] >= 0x80 {
264
+ let c = unsafe { decode_non_ascii(&bytes[i..]) };
265
+ if unicode::is_other_complete(c) {
266
+ i += c.len_utf8();
267
+ i = unsafe { neon::scan_ascii_other(bytes, i) };
268
+ while i < bytes.len() {
269
+ let b = bytes[i];
270
+ if b < 0x80 && !b.is_ascii_alphanumeric() && !b.is_ascii_whitespace() {
271
+ i += 1;
272
+ } else {
273
+ break;
274
+ }
275
+ }
276
+ } else {
277
+ break;
278
+ }
279
+ }
280
+
281
+ if i == start {
282
+ return Err(backtrack());
283
+ }
284
+ *input = &before[i..];
285
+ Ok(())
286
+ }
287
+
288
+ fn whitespace_run(input: &mut &str) -> ModalResult<()> {
289
+ let before = *input;
290
+ let bytes = before.as_bytes();
291
+ let mut i = 0;
292
+ while i < bytes.len() {
293
+ let b = bytes[i];
294
+ if b.is_ascii_whitespace() {
295
+ i += 1;
296
+ } else if b >= 0x80 {
297
+ let c = unsafe { decode_non_ascii(&bytes[i..]) };
298
+ if unicode::is_whitespace(c) {
299
+ i += c.len_utf8();
300
+ } else {
301
+ break;
302
+ }
303
+ } else {
304
+ break;
305
+ }
306
+ }
307
+ if i == 0 {
308
+ return Err(backtrack());
309
+ }
310
+ // At end of input the `(?!\S)` lookahead succeeds, so `\s+(?!\S)` consumes
311
+ // the entire trailing whitespace run as one pretoken.
312
+ if i >= bytes.len() {
313
+ *input = &before[i..];
314
+ return Ok(());
315
+ }
316
+ // Find start of last whitespace char
317
+ let mut last_start = i - 1;
318
+ while last_start > 0 && bytes[last_start] & 0xC0 == 0x80 {
319
+ last_start -= 1;
320
+ }
321
+ // Need at least 2 ws chars (last_start > 0 means there's content before the last char)
322
+ if last_start == 0 {
323
+ return Err(backtrack());
324
+ }
325
+ // Leave last ws char for single_whitespace
326
+ *input = &before[last_start..];
327
+ Ok(())
328
+ }
329
+
330
+ fn single_whitespace(input: &mut &str) -> ModalResult<()> {
331
+ let bytes = input.as_bytes();
332
+ let b = *bytes.first().ok_or_else(backtrack)?;
333
+ if b.is_ascii_whitespace() {
334
+ *input = &input[1..];
335
+ Ok(())
336
+ } else if b >= 0x80 {
337
+ let c = unsafe { decode_non_ascii(bytes) };
338
+ if unicode::is_whitespace(c) {
339
+ *input = &input[c.len_utf8()..];
340
+ Ok(())
341
+ } else {
342
+ Err(backtrack())
343
+ }
344
+ } else {
345
+ Err(backtrack())
346
+ }
347
+ }
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // Top-level pretoken parser — direct dispatch on first byte instead of alt().
351
+ // ---------------------------------------------------------------------------
352
+
353
+ fn pretoken<'a>(input: &mut &'a str) -> ModalResult<Pretoken<'a>> {
354
+ let before = *input;
355
+ let bytes = before.as_bytes();
356
+ let first = *bytes.first().ok_or_else(backtrack)?;
357
+
358
+ if first < 0x80 {
359
+ if first.is_ascii_alphabetic() {
360
+ letter_run(input)?;
361
+ } else if first == b' ' {
362
+ // Peek at second byte to avoid cascading alt() failures
363
+ match bytes.get(1) {
364
+ Some(&b) if b.is_ascii_alphabetic() => letter_run(input)?,
365
+ Some(&b) if b.is_ascii_digit() => number_run(input)?,
366
+ Some(&b) if !b.is_ascii_whitespace() && b < 0x80 => {
367
+ other_run(input)?
368
+ }
369
+ Some(&b) if b >= 0x80 => {
370
+ let c = unsafe { decode_non_ascii(&bytes[1..]) };
371
+ if unicode::is_letter(c) {
372
+ letter_run(input)?;
373
+ } else if unicode::is_number(c) {
374
+ number_run(input)?;
375
+ } else if unicode::is_whitespace(c) {
376
+ ws_or_single(input)?;
377
+ } else {
378
+ other_run(input)?;
379
+ }
380
+ }
381
+ _ => ws_or_single(input)?,
382
+ }
383
+ } else if first.is_ascii_digit() {
384
+ number_run(input)?;
385
+ } else if first == b'\'' {
386
+ contraction(input).or_else(|_| other_run(input))?;
387
+ } else if first.is_ascii_whitespace() {
388
+ ws_or_single(input)?;
389
+ } else {
390
+ other_run(input)?;
391
+ }
392
+ } else {
393
+ let c = unsafe { decode_non_ascii(bytes) };
394
+ if unicode::is_letter(c) {
395
+ letter_run(input)?;
396
+ } else if unicode::is_number(c) {
397
+ number_run(input)?;
398
+ } else if unicode::is_whitespace(c) {
399
+ ws_or_single(input)?;
400
+ } else {
401
+ other_run(input)?;
402
+ }
403
+ }
404
+
405
+ let consumed = before.len() - input.len();
406
+ Ok(Pretoken(&before.as_bytes()[..consumed]))
407
+ }
408
+
409
+ /// Try whitespace_run first (2+ ws chars with non-ws following), fall back to single.
410
+ #[inline]
411
+ fn ws_or_single(input: &mut &str) -> ModalResult<()> {
412
+ whitespace_run(input).or_else(|_| single_whitespace(input))
413
+ }
414
+
415
+ // ---------------------------------------------------------------------------
416
+ // Public API
417
+ // ---------------------------------------------------------------------------
418
+
419
+ pub struct PretokenIterator<'a> {
420
+ input: &'a [u8],
421
+ }
422
+
423
+ /// Parse the next pretoken from `input`, advancing the slice past the consumed bytes.
424
+ /// Returns `None` when `input` is empty.
425
+ pub fn pretoken_next<'a>(input: &mut &'a str) -> Option<Pretoken<'a>> {
426
+ if input.is_empty() {
427
+ return None;
428
+ }
429
+ pretoken(input).ok()
430
+ }
431
+
432
+ // The `impl FnMut` parser type is unnameable, so this signature can't be
433
+ // factored into a type alias.
434
+ #[allow(clippy::type_complexity)]
435
+ pub fn pretokens_iterator<'a>(
436
+ input: &'a mut &'a str,
437
+ ) -> winnow::combinator::ParserIterator<
438
+ 'a,
439
+ impl FnMut(
440
+ &mut &'a str,
441
+ )
442
+ -> std::result::Result<Pretoken<'a>, winnow::error::ErrMode<winnow::error::ContextError>>
443
+ + 'a,
444
+ &'a str,
445
+ Pretoken<'a>,
446
+ winnow::error::ErrMode<winnow::error::ContextError>,
447
+ > {
448
+ iterator(input, pretoken)
449
+ }
450
+
451
+ #[cfg(test)]
452
+ mod tests {
453
+ use super::*;
454
+
455
+ /// Trailing-whitespace and mixed-whitespace edge cases, checked against the
456
+ /// reference GPT-2 regex. Catches the `\s+(?!\S)` end-of-input case: the
457
+ /// lookahead succeeds at EOF, so a trailing run like "\n\n\n" must be a
458
+ /// single pretoken, not one per character.
459
+ #[test]
460
+ fn whitespace_edge_cases_match_regex() {
461
+ let re = fancy_regex::Regex::new(
462
+ r"'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+",
463
+ )
464
+ .unwrap();
465
+ let cases = [
466
+ "\n\n\n",
467
+ "\n\n\nhello",
468
+ "a\n\n\nb",
469
+ "hello \n world",
470
+ " \t\n ",
471
+ "x ",
472
+ "x \n",
473
+ " \n x",
474
+ "\t\t\tword",
475
+ "end\n\n",
476
+ "end\n",
477
+ " ",
478
+ "\u{a0}\u{a0}",
479
+ "word\u{2028}\u{2028}",
480
+ ];
481
+ for case in cases {
482
+ let expected: Vec<&str> = re.find_iter(case).map(|m| m.unwrap().as_str()).collect();
483
+
484
+ let mut input = case;
485
+ let mut combinator: Vec<String> = Vec::new();
486
+ while let Some(p) = pretoken_next(&mut input) {
487
+ combinator.push(String::from_utf8(p.0.to_vec()).unwrap());
488
+ }
489
+ assert_eq!(combinator, expected, "combinator mismatch for {case:?}");
490
+
491
+ let mut fast: Vec<String> = Vec::new();
492
+ let mut it =
493
+ crate::pretokenize::fast::FastR50kPretokenizer::new(case.as_bytes());
494
+ for p in it {
495
+ fast.push(String::from_utf8(p.as_ref().to_vec()).unwrap());
496
+ }
497
+ assert_eq!(fast, expected, "fast mismatch for {case:?}");
498
+
499
+ let state_machine: Vec<String> =
500
+ crate::pretokenize::PretokenizerIter::new(case.as_bytes())
501
+ .map(|p| String::from_utf8(p.0.to_vec()).unwrap())
502
+ .collect();
503
+ assert_eq!(
504
+ state_machine, expected,
505
+ "state machine mismatch for {case:?}"
506
+ );
507
+ }
508
+ }
509
+
510
+ #[test]
511
+ fn combinator_compare() {
512
+ let data_dir = std::env::home_dir().unwrap().join("data");
513
+ let input = std::fs::read_to_string(data_dir.join("TinyStoriesV2-GPT4-valid.txt")).unwrap();
514
+ let input_bytes = input.as_bytes();
515
+ let standard_iterator = crate::pretokenize::pretokenize_as_iter(input_bytes);
516
+ let mut input_slice = input.as_str();
517
+ let mut combinator_iterator = pretokens_iterator(&mut input_slice);
518
+ for eorb in standard_iterator.zip_longest(&mut combinator_iterator) {
519
+ match eorb {
520
+ itertools::EitherOrBoth::Both(a, b) => {
521
+ if a.0 != b.0 {
522
+ eprintln!(
523
+ "Mismatch: {:?} != {:?}",
524
+ String::from_utf8_lossy(a.0),
525
+ String::from_utf8_lossy(b.0)
526
+ );
527
+
528
+ // Find text before and after the mismatch by comparing pointers from a.0 and input_bytes
529
+ let a_start = a.0.as_ptr() as usize;
530
+ let b_start = b.0.as_ptr() as usize;
531
+ let input_start = input_bytes.as_ptr() as usize;
532
+ let a_offset = a_start - input_start;
533
+
534
+ let region = &input_bytes
535
+ [a_offset.saturating_sub(32)..min(input_bytes.len(), a_offset + 32)];
536
+ eprintln!("Context: {:?}", String::from_utf8_lossy(region));
537
+
538
+ panic!("combinator pretoken differs from standard pretokenizer");
539
+ }
540
+ }
541
+ itertools::EitherOrBoth::Left(a) => {
542
+ eprintln!("Left only: {:?}", String::from_utf8_lossy(a.0));
543
+
544
+ // Find text before and after the mismatch by comparing pointers from a.0 and input_bytes
545
+ let a_start = a.0.as_ptr() as usize;
546
+ let input_start = input_bytes.as_ptr() as usize;
547
+ let a_offset = a_start - input_start;
548
+
549
+ let region = &input_bytes
550
+ [a_offset.saturating_sub(32)..min(input_bytes.len(), a_offset + 32)];
551
+ eprintln!("Context: {:?}", String::from_utf8_lossy(region));
552
+
553
+ panic!("standard pretokenizer produced an extra pretoken");
554
+ }
555
+ itertools::EitherOrBoth::Right(b) => {
556
+ eprintln!("Right only: {:?}", String::from_utf8_lossy(b.0));
557
+
558
+ // Find text before and after the mismatch by comparing pointers from b and input_bytes
559
+ let b_start = b.as_ptr() as usize;
560
+ let input_start = input_bytes.as_ptr() as usize;
561
+ let b_offset = b_start - input_start;
562
+
563
+ let region = &input_bytes
564
+ [b_offset.saturating_sub(32)..min(input_bytes.len(), b_offset + 32)];
565
+ eprintln!("Context: {:?}", String::from_utf8_lossy(region));
566
+
567
+ panic!("combinator pretokenizer produced an extra pretoken");
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
@@ -0,0 +1,28 @@
1
+ //! Reference pretokenizers: benchmark baselines and test oracles only.
2
+ //!
3
+ //! Nothing in here runs in the library's encode path — the production
4
+ //! pretokenizers are the mask scanners in [`super::fast`] (see
5
+ //! `pretokenize_as_iter` / `PretokenizerType`). These implementations are
6
+ //! kept so `benches/pretokenize.rs` can race the production scanners
7
+ //! against the designs they replaced, and as independent oracles in
8
+ //! differential tests:
9
+ //!
10
+ //! - [`state_machine`]: the byte-class DFA, the original correctness
11
+ //! reference (also exercised from other modules' tests).
12
+ //! - [`combinator`]: winnow parser-combinator implementation.
13
+ //! - [`simd`]: first-generation portable-SIMD prototype.
14
+ //! - [`avx512`]: hand-rolled AVX-512 prototype. Compile-time gated on
15
+ //! AVX-512BW/VL (build with `-C target-cpu=native` or equivalent to
16
+ //! include it) — as a baseline it is deliberately not runtime-dispatched,
17
+ //! unlike the production scanners, which detect their tier at runtime on
18
+ //! any build.
19
+
20
+ #[cfg(all(
21
+ target_arch = "x86_64",
22
+ target_feature = "avx512bw",
23
+ target_feature = "avx512vl"
24
+ ))]
25
+ pub mod avx512;
26
+ pub mod combinator;
27
+ pub mod simd;
28
+ pub mod state_machine;