pub(crate) mod cl100k_family;
pub(crate) mod mask;
pub(crate) mod o200k_family;
pub mod cl100k;
pub mod deepseek_v3;
pub mod kimi;
pub mod nemotron;
pub mod o200k;
pub mod olmo3;
pub mod qwen2;
pub mod qwen3_5;
pub mod r50k;
pub use cl100k::FastCl100kPretokenizer;
pub use deepseek_v3::FastDeepSeekV3Pretokenizer;
pub use kimi::FastKimiPretokenizer;
pub use nemotron::FastNemotronPretokenizer;
pub use o200k::FastO200kPretokenizer;
pub use olmo3::FastOlmo3Pretokenizer;
pub use qwen2::FastQwen2Pretokenizer;
pub use qwen3_5::FastQwen35Pretokenizer;
pub use r50k::FastR50kPretokenizer;
use crate::pretokenize::SpanBatch;
use crate::pretokenize::unicode;
#[inline(never)]
pub(crate) fn fill_spans_keyed_mask<'a, S: mask::MaskScheme>(
bytes: &'a [u8],
state: &mut mask::MaskState,
batch: &mut SpanBatch<'a>,
prefetch: &impl Fn(u64),
) -> usize {
#[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))]
if mask::simd_scanner_available() {
return state.fill_spans_two_phase::<S>(bytes, batch, prefetch);
}
crate::pretokenize::fill_spans_keyed_with_buf(
bytes,
|| state.next_span::<S>(bytes),
batch,
prefetch,
)
}
macro_rules! impl_mask_pretokenizer {
($pretokenizer:ident, $scheme:ty) => {
impl<'a> $pretokenizer<'a> {
#[inline]
pub fn new(bytes: &'a [u8]) -> Self {
Self::with_pos(bytes, 0)
}
#[inline]
pub fn with_pos(bytes: &'a [u8], pos: usize) -> Self {
Self {
bytes,
state: crate::pretokenize::fast::mask::MaskState::new(pos),
}
}
#[inline]
pub fn pos(&self) -> usize {
self.state.pos
}
}
impl<'a> Iterator for $pretokenizer<'a> {
type Item = crate::pretokenize::Pretoken<'a>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (start, end) = self.state.next_span::<$scheme>(self.bytes)?;
Some(crate::pretokenize::Pretoken(&self.bytes[start..end]))
}
}
unsafe impl<'a> crate::pretokenize::PretokenSpans<'a> for $pretokenizer<'a> {
#[inline]
fn fill_spans_keyed(
&mut self,
batch: &mut crate::pretokenize::SpanBatch<'a>,
prefetch: &impl Fn(u64),
) -> usize {
crate::pretokenize::fast::fill_spans_keyed_mask::<$scheme>(
self.bytes,
&mut self.state,
batch,
prefetch,
)
}
}
};
}
pub(crate) use impl_mask_pretokenizer;
#[inline(always)]
pub(crate) fn is_letter(b: u8) -> bool {
(b | 0x20).wrapping_sub(b'a') < 26
}
#[inline(always)]
pub(crate) fn is_digit(b: u8) -> bool {
b.wrapping_sub(b'0') < 10
}
#[inline(always)]
pub(crate) fn is_ascii_ws(b: u8) -> bool {
b == b' ' || b.wrapping_sub(9) < 5
}
#[inline(always)]
pub(crate) unsafe fn decode_non_ascii(bytes: &[u8]) -> char {
unsafe {
std::str::from_utf8_unchecked(bytes)
.chars()
.next()
.unwrap_unchecked()
}
}
#[inline(always)]
pub(crate) unsafe fn decode_cp(bytes: &[u8], pos: usize) -> (u32, usize) {
if pos + 4 > bytes.len() {
return decode_cp_near_end(bytes, pos);
}
unsafe { decode_cp_inbounds(bytes, pos) }
}
#[inline(always)]
pub(crate) unsafe fn decode_cp_inbounds(bytes: &[u8], pos: usize) -> (u32, usize) {
unsafe {
let b0 = *bytes.get_unchecked(pos) as u32;
let b1 = (*bytes.get_unchecked(pos + 1) & 0x3F) as u32;
if b0 < 0xE0 {
return (((b0 & 0x1F) << 6) | b1, 2);
}
let b2 = (*bytes.get_unchecked(pos + 2) & 0x3F) as u32;
if b0 < 0xF0 {
return (((b0 & 0x0F) << 12) | (b1 << 6) | b2, 3);
}
let b3 = (*bytes.get_unchecked(pos + 3) & 0x3F) as u32;
(
(((b0 & 0x07) << 18) | (b1 << 12) | (b2 << 6) | b3).min(CP_INVALID),
4,
)
}
}
pub(crate) const CP_INVALID: u32 = 0x10FFFF;
#[cold]
#[inline(never)]
fn decode_cp_near_end(bytes: &[u8], pos: usize) -> (u32, usize) {
let len = bytes.len();
let b0 = bytes[pos] as u32;
let need = if b0 < 0xE0 {
2
} else if b0 < 0xF0 {
3
} else {
4
};
if pos + need > len {
return (CP_INVALID, len - pos);
}
let b1 = (bytes[pos + 1] & 0x3F) as u32;
if need == 2 {
return (((b0 & 0x1F) << 6) | b1, 2);
}
let b2 = (bytes[pos + 2] & 0x3F) as u32;
if need == 3 {
return (((b0 & 0x0F) << 12) | (b1 << 6) | b2, 3);
}
let b3 = (bytes[pos + 3] & 0x3F) as u32;
(
(((b0 & 0x07) << 18) | (b1 << 12) | (b2 << 6) | b3).min(CP_INVALID),
4,
)
}
#[inline(always)]
pub(crate) fn scan_newlines(bytes: &[u8], mut pos: usize) -> usize {
while pos < bytes.len() {
let b = unsafe { *bytes.get_unchecked(pos) };
if b == b'\r' || b == b'\n' {
pos += 1;
} else {
break;
}
}
pos
}
#[inline(always)]
pub(crate) fn whitespace_token_end<const NEWLINE_AT_EOS: bool>(
bytes: &[u8],
start: usize,
is_unicode_whitespace: impl Fn(u32) -> bool,
) -> usize {
let len = bytes.len();
let mut pos = start;
let mut last_newline_end = 0usize;
let mut last_char_start = start;
while pos < len {
let byte = unsafe { *bytes.get_unchecked(pos) };
if byte == b'\r' || byte == b'\n' {
last_char_start = pos;
pos += 1;
last_newline_end = pos;
} else if is_ascii_ws(byte) {
last_char_start = pos;
pos += 1;
} else if byte >= 0x80 {
let (codepoint, width) = unsafe { decode_cp(bytes, pos) };
if is_unicode_whitespace(codepoint) {
last_char_start = pos;
pos += width;
} else {
break;
}
} else {
break;
}
}
if NEWLINE_AT_EOS && last_newline_end != 0 {
return last_newline_end;
}
if pos >= len {
return pos;
}
if last_newline_end != 0 {
return last_newline_end;
}
if last_char_start > start {
return last_char_start;
}
pos
}
#[inline(always)]
pub(crate) fn contraction_end(bytes: &[u8], apostrophe: usize) -> Option<usize> {
if bytes.get(apostrophe) != Some(&b'\'') {
return None;
}
match bytes.get(apostrophe + 1).map(u8::to_ascii_lowercase) {
Some(b's' | b'd' | b'm' | b't') => Some(apostrophe + 2),
Some(b'l') if bytes.get(apostrophe + 2).map(u8::to_ascii_lowercase) == Some(b'l') => {
Some(apostrophe + 3)
}
Some(b'v') if bytes.get(apostrophe + 2).map(u8::to_ascii_lowercase) == Some(b'e') => {
Some(apostrophe + 3)
}
Some(b'r') if bytes.get(apostrophe + 2).map(u8::to_ascii_lowercase) == Some(b'e') => {
Some(apostrophe + 3)
}
Some(0xC5) if bytes.get(apostrophe + 2) == Some(&0xBF) => Some(apostrophe + 3),
_ => None,
}
}
#[inline(always)]
pub(crate) fn letter_end_at(bytes: &[u8], pos: usize) -> Option<usize> {
let &b = bytes.get(pos)?;
if is_letter(b) {
return Some(pos + 1);
}
if b >= 0x80 {
let (cp, l) = unsafe { decode_cp(bytes, pos) };
if unicode::class_of(cp) == unicode::CharClass::Letter {
return Some(pos + l);
}
}
None
}
pub(crate) const HI: u64 = 0x8080_8080_8080_8080;
#[inline(always)]
pub(crate) fn swar64_letter_nonmask(word: u64) -> u64 {
let lowered = word | 0x2020_2020_2020_2020;
let ge_a = (lowered | HI).wrapping_sub(0x6161_6161_6161_6161);
let le_z = 0xFAFA_FAFA_FAFA_FAFA_u64.wrapping_sub(lowered);
!(ge_a & le_z) & HI
}
#[inline(always)]
pub(crate) fn swar_scan_letters(bytes: &[u8], mut pos: usize) -> usize {
let len = bytes.len();
while pos + 8 <= len {
let word = unsafe { (bytes.as_ptr().add(pos) as *const u64).read_unaligned() };
if word & HI != 0 {
break;
}
let nonletter = swar64_letter_nonmask(word);
if nonletter != 0 {
return pos + nonletter.to_le().trailing_zeros() as usize / 8;
}
pos += 8;
}
while pos < len {
let b = unsafe { *bytes.get_unchecked(pos) };
if is_letter(b) {
pos += 1;
} else {
break;
}
}
pos
}
#[cfg(target_arch = "aarch64")]
#[allow(dead_code)]
#[inline(always)]
pub(crate) fn neon_scan_letters(bytes: &[u8], mut pos: usize) -> usize {
use std::arch::aarch64::*;
let len = bytes.len();
while pos + 16 <= len {
unsafe {
let v = vld1q_u8(bytes.as_ptr().add(pos));
let lowered = vorrq_u8(v, vdupq_n_u8(0x20));
let ge_a = vcgeq_u8(lowered, vdupq_n_u8(b'a'));
let le_z = vcleq_u8(lowered, vdupq_n_u8(b'z'));
let nonletter = vmvnq_u8(vandq_u8(ge_a, le_z));
let mask = vget_lane_u64::<0>(vreinterpret_u64_u8(vshrn_n_u16::<4>(
vreinterpretq_u16_u8(nonletter),
)));
if mask != 0 {
return pos + (mask.trailing_zeros() >> 2) as usize;
}
}
pos += 16;
}
while pos < len {
let b = unsafe { *bytes.get_unchecked(pos) };
if is_letter(b) {
pos += 1;
} else {
break;
}
}
pos
}
#[inline(always)]
pub(crate) fn scan_numbers_max3(bytes: &[u8], mut pos: usize, mut consumed: u32) -> usize {
let len = bytes.len();
while consumed < 3 && pos < len {
let b = unsafe { *bytes.get_unchecked(pos) };
if is_digit(b) {
pos += 1;
consumed += 1;
continue;
}
if b >= 0x80 {
let (cp, l) = unsafe { decode_cp(bytes, pos) };
if unicode::class_of(cp) == unicode::CharClass::Number {
pos += l;
consumed += 1;
continue;
}
}
break;
}
pos
}
#[inline(always)]
pub(crate) fn scan_letters_from(bytes: &[u8], pos: usize) -> usize {
let len = bytes.len();
let mut p = pos;
loop {
p = swar_scan_letters(bytes, p);
if p < len && unsafe { *bytes.get_unchecked(p) } >= 0x80 {
let (cp, l) = unsafe { decode_cp(bytes, p) };
if unicode::class_of(cp) == unicode::CharClass::Letter {
p += l;
continue;
}
}
return p;
}
}
#[inline(always)]
pub(crate) fn scan_digits_from(bytes: &[u8], pos: usize) -> usize {
let len = bytes.len();
let mut p = pos;
loop {
while p < len && is_digit(unsafe { *bytes.get_unchecked(p) }) {
p += 1;
}
if p < len && unsafe { *bytes.get_unchecked(p) } >= 0x80 {
let (cp, l) = unsafe { decode_cp(bytes, p) };
if unicode::class_of(cp) == unicode::CharClass::Number {
p += l;
continue;
}
}
return p;
}
}
#[inline(always)]
pub(crate) fn scan_other_from(bytes: &[u8], pos: usize) -> usize {
let len = bytes.len();
let mut p = pos;
loop {
while p < len {
let b = unsafe { *bytes.get_unchecked(p) };
if b >= 0x80 {
break;
}
if is_letter(b) || is_digit(b) || is_ascii_ws(b) {
return p;
}
p += 1;
}
if p < len {
let (cp, l) = unsafe { decode_cp(bytes, p) };
if unicode::class_of(cp) == unicode::CharClass::Other {
p += l;
continue;
}
}
return p;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pretokenize::{Pretoken, PretokenSpans, SpanBatch};
fn pieces<'a>(scanner: impl Iterator<Item = Pretoken<'a>>) -> Vec<&'a [u8]> {
scanner.map(|pretoken| pretoken.0).collect()
}
#[test]
fn every_mask_adapter_preserves_boundaries_when_resumed() {
macro_rules! assert_resumable {
($scanner:ident) => {{
let input = "we'RE 123\n\u{2003}漢字".as_bytes();
let full = pieces($scanner::new(input));
assert_eq!(full.concat(), input, stringify!($scanner));
let first_end = full[0].len();
let mut cursor = $scanner::new(input);
assert_eq!(cursor.next().unwrap().0, full[0], stringify!($scanner));
assert_eq!(cursor.pos(), first_end, stringify!($scanner));
let resumed = pieces($scanner::with_pos(input, first_end));
assert_eq!(resumed.as_slice(), &full[1..], stringify!($scanner));
let mut chunked = $scanner::new(input);
let mut batch = SpanBatch::new();
let count = chunked.fill_spans_keyed(&mut batch, &|_| {});
let chunked_spans = (0..count)
.map(|index| unsafe { batch.span(index) })
.collect::<Vec<_>>();
assert_eq!(chunked_spans, full, stringify!($scanner));
assert_eq!(
chunked.fill_spans_keyed(&mut batch, &|_| {}),
0,
stringify!($scanner)
);
}};
}
assert_resumable!(FastR50kPretokenizer);
assert_resumable!(FastCl100kPretokenizer);
assert_resumable!(FastQwen2Pretokenizer);
assert_resumable!(FastQwen35Pretokenizer);
assert_resumable!(FastOlmo3Pretokenizer);
assert_resumable!(FastO200kPretokenizer);
assert_resumable!(FastNemotronPretokenizer);
assert_resumable!(FastKimiPretokenizer);
}
#[test]
fn shared_whitespace_walker_preserves_regex_priority() {
let trailing = b" \n ";
let ascii_only = |_| false;
assert_eq!(
whitespace_token_end::<false>(trailing, 0, ascii_only),
trailing.len(),
"cl100k's end-of-input alternative keeps trailing whitespace together"
);
assert_eq!(
whitespace_token_end::<true>(trailing, 0, ascii_only),
2,
"Qwen/OLMo/o200k newline alternatives win at end of input"
);
assert_eq!(
pieces(FastCl100kPretokenizer::new(trailing)),
[trailing.as_slice()]
);
assert_eq!(
pieces(FastQwen2Pretokenizer::new(trailing)),
[&trailing[..2], &trailing[2..]]
);
}
#[test]
fn shared_contraction_matcher_covers_ascii_case_and_long_s() {
for token in ["'s", "'T", "'re", "'VE", "'m", "'Ll", "'d"] {
assert_eq!(contraction_end(token.as_bytes(), 0), Some(token.len()));
}
assert_eq!(contraction_end(b"'\xC5\xBF", 0), Some(3));
assert_eq!(contraction_end(b"'x", 0), None);
assert_eq!(contraction_end(b"word", 0), None);
assert_eq!(
pieces(FastCl100kPretokenizer::new(b"we'RE")),
[b"we".as_slice(), b"'RE".as_slice()]
);
assert_eq!(
pieces(FastO200kPretokenizer::new(b"we'RE")),
[b"we'RE".as_slice()]
);
}
}