use unicode_general_category::{get_general_category, GeneralCategory};
#[derive(Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
enum Class {
Punct = 0,
Letter = 1,
Digit = 2,
Space = 3,
Newline = 4,
Lead = 5,
}
const CLASS: [Class; 256] = {
let mut table = [Class::Punct; 256];
let mut b = 0usize;
while b < 256 {
table[b] = if b >= 0x80 {
Class::Lead
} else if (b >= b'a' as usize && b <= b'z' as usize)
|| (b >= b'A' as usize && b <= b'Z' as usize)
{
Class::Letter
} else if b >= b'0' as usize && b <= b'9' as usize {
Class::Digit
} else if b == b'\r' as usize || b == b'\n' as usize {
Class::Newline
} else if b == b' ' as usize || b == b'\t' as usize || b == 0x0b || b == 0x0c {
Class::Space
} else {
Class::Punct
};
b += 1;
}
table
};
const ONES: u64 = 0x0101_0101_0101_0101;
const HIGH: u64 = 0x8080_8080_8080_8080;
#[inline(always)]
fn lanes_below(x: u64, n: u8) -> u64 {
let guarded = x | HIGH;
!guarded.wrapping_sub(ONES.wrapping_mul(n as u64)) & HIGH
}
#[inline(always)]
fn ascii_letter_lanes(word: u64) -> u64 {
let lowered = word | 0x2020_2020_2020_2020;
let below_a = lanes_below(lowered, b'a');
let at_most_z = lanes_below(lowered, b'z' + 1);
at_most_z & !below_a & HIGH
}
#[inline(always)]
fn word_at(bytes: &[u8], pos: usize) -> Option<u64> {
bytes
.get(pos..pos + 8)
.map(|chunk| u64::from_le_bytes(chunk.try_into().expect("slice of exactly eight bytes")))
}
#[inline]
fn swar_skip_ascii_letters(bytes: &[u8], mut pos: usize) -> usize {
while let Some(word) = word_at(bytes, pos) {
if word & HIGH != 0 {
break;
}
let not_letters = !ascii_letter_lanes(word) & HIGH;
if not_letters != 0 {
return pos + (not_letters.trailing_zeros() / 8) as usize;
}
pos += 8;
}
while pos < bytes.len() && (bytes[pos] | 0x20).wrapping_sub(b'a') < 26 {
pos += 1;
}
pos
}
#[inline]
fn swar_skip_ascii(bytes: &[u8], mut pos: usize) -> usize {
while let Some(word) = word_at(bytes, pos) {
let high = word & HIGH;
if high != 0 {
return pos + (high.trailing_zeros() / 8) as usize;
}
pos += 8;
}
while pos < bytes.len() && bytes[pos] < 0x80 {
pos += 1;
}
pos
}
#[inline]
fn swar_skip_to_number(bytes: &[u8], mut pos: usize) -> usize {
while let Some(word) = word_at(bytes, pos) {
if word & HIGH != 0 {
break;
}
let digits = lanes_below(word, b'9' + 1) & !lanes_below(word, b'0') & HIGH;
if digits != 0 {
return pos + (digits.trailing_zeros() / 8) as usize;
}
pos += 8;
}
while pos < bytes.len() && bytes[pos] < 0x80 && !bytes[pos].is_ascii_digit() {
pos += 1;
}
pos
}
#[inline]
fn is_letter_char(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::UppercaseLetter
| GeneralCategory::LowercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
)
}
#[inline]
fn is_number_char(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::DecimalNumber
| GeneralCategory::LetterNumber
| GeneralCategory::OtherNumber
)
}
#[inline]
fn is_mark_char(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::NonspacingMark
| GeneralCategory::SpacingMark
| GeneralCategory::EnclosingMark
)
}
#[inline]
fn is_punct_or_symbol_char(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::ConnectorPunctuation
| GeneralCategory::DashPunctuation
| GeneralCategory::OpenPunctuation
| GeneralCategory::ClosePunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::FinalPunctuation
| GeneralCategory::OtherPunctuation
| GeneralCategory::MathSymbol
| GeneralCategory::CurrencySymbol
| GeneralCategory::ModifierSymbol
| GeneralCategory::OtherSymbol
)
}
#[inline]
fn is_upper_run_char(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::UppercaseLetter
| GeneralCategory::TitlecaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
) || is_mark_char(c)
}
#[inline]
fn is_lower_run_char(c: char) -> bool {
matches!(
get_general_category(c),
GeneralCategory::LowercaseLetter
| GeneralCategory::ModifierLetter
| GeneralCategory::OtherLetter
) || is_mark_char(c)
}
#[inline]
fn char_at(text: &str, pos: usize) -> (char, usize) {
let c = text[pos..]
.chars()
.next()
.expect("pos is a char boundary inside the string");
(c, c.len_utf8())
}
#[inline]
fn char_len_if(
text: &str,
bytes: &[u8],
pos: usize,
ascii: impl Fn(Class) -> bool,
pred: impl Fn(char) -> bool,
) -> Option<usize> {
let class = CLASS[bytes[pos] as usize];
if class == Class::Lead {
let (c, len) = char_at(text, pos);
pred(c).then_some(len)
} else {
ascii(class).then_some(1)
}
}
#[inline]
fn letter_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
char_len_if(text, bytes, pos, |c| c == Class::Letter, is_letter_char)
}
#[inline]
fn number_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
char_len_if(text, bytes, pos, |c| c == Class::Digit, is_number_char)
}
#[inline]
fn space_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
char_len_if(
text,
bytes,
pos,
|c| matches!(c, Class::Space | Class::Newline),
char::is_whitespace,
)
}
#[inline]
fn punct_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
char_len_if(
text,
bytes,
pos,
|c| c == Class::Punct,
|c| !c.is_whitespace() && !is_letter_char(c) && !is_number_char(c),
)
}
#[inline]
fn letter_or_mark_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
char_len_if(
text,
bytes,
pos,
|c| c == Class::Letter,
|c| is_letter_char(c) || is_mark_char(c),
)
}
#[inline]
fn punct_or_symbol_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
let class = CLASS[bytes[pos] as usize];
if class == Class::Lead {
let (c, len) = char_at(text, pos);
is_punct_or_symbol_char(c).then_some(len)
} else {
let b = bytes[pos];
(class == Class::Punct && b.is_ascii_graphic()).then_some(1)
}
}
#[inline]
fn scan_letters(text: &str, bytes: &[u8], mut pos: usize) -> usize {
loop {
pos = swar_skip_ascii_letters(bytes, pos);
if pos >= bytes.len() || bytes[pos] < 0x80 {
return pos;
}
match letter_at(text, bytes, pos) {
Some(n) => pos += n,
None => return pos,
}
}
}
#[inline]
fn scan_run(
text: &str,
bytes: &[u8],
mut pos: usize,
at: impl Fn(&str, &[u8], usize) -> Option<usize>,
) -> usize {
while pos < bytes.len() {
match at(text, bytes, pos) {
Some(n) => pos += n,
None => break,
}
}
pos
}
#[inline]
fn contraction_len(bytes: &[u8], pos: usize) -> Option<usize> {
if bytes.get(pos) != Some(&b'\'') {
return None;
}
let lower = |i: usize| bytes.get(i).map(|b| b | 0x20);
match lower(pos + 1)? {
b's' | b'd' | b'm' | b't' => Some(2),
b'l' if lower(pos + 2) == Some(b'l') => Some(3),
b'v' | b'r' if lower(pos + 2) == Some(b'e') => Some(3),
_ => None,
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum WhitespaceOrder {
EndOfTextFirst,
NewlineFirst,
}
#[inline]
fn whitespace_span(text: &str, bytes: &[u8], pos: usize, order: WhitespaceOrder) -> (usize, usize) {
let len = bytes.len();
let run_end = scan_run(text, bytes, pos, space_at);
let last_newline = bytes[pos..run_end]
.iter()
.rposition(|&b| b == b'\r' || b == b'\n')
.map(|offset| pos + offset + 1);
match order {
WhitespaceOrder::EndOfTextFirst if run_end == len => return (pos, run_end),
_ => {
if let Some(end) = last_newline {
return (pos, end);
}
}
}
if run_end == len {
return (pos, run_end);
}
let last_char_len = text[pos..run_end]
.chars()
.next_back()
.map(char::len_utf8)
.unwrap_or(1);
if run_end - pos > last_char_len {
return (pos, run_end - last_char_len);
}
(pos, run_end)
}
#[derive(Clone, Copy)]
struct Family {
max_digits: u32,
whitespace: WhitespaceOrder,
}
const CL100K: Family = Family {
max_digits: 3,
whitespace: WhitespaceOrder::EndOfTextFirst,
};
const LLAMA3: Family = Family {
max_digits: 3,
whitespace: WhitespaceOrder::NewlineFirst,
};
const QWEN2: Family = Family {
max_digits: 1,
whitespace: WhitespaceOrder::NewlineFirst,
};
fn family_spans(text: &str, out: &mut Vec<(usize, usize)>, scheme: Family) {
let bytes = text.as_bytes();
let len = bytes.len();
let mut pos = 0usize;
while pos < len {
let start = pos;
if let Some(n) = contraction_len(bytes, pos) {
pos += n;
out.push((start, pos));
continue;
}
if let Some(n) = letter_at(text, bytes, pos) {
pos = scan_letters(text, bytes, pos + n);
out.push((start, pos));
continue;
}
if let Some(prefix) = prefix_len(text, bytes, pos) {
if let Some(n) = pos
.checked_add(prefix)
.filter(|&p| p < len)
.and_then(|p| letter_at(text, bytes, p))
{
pos = scan_letters(text, bytes, pos + prefix + n);
out.push((start, pos));
continue;
}
}
if let Some(n) = number_at(text, bytes, pos) {
pos += n;
for _ in 1..scheme.max_digits {
match (pos < len).then(|| number_at(text, bytes, pos)).flatten() {
Some(n) => pos += n,
None => break,
}
}
out.push((start, pos));
continue;
}
let after_space = if bytes[pos] == b' ' { pos + 1 } else { pos };
if after_space < len && punct_at(text, bytes, after_space).is_some() {
pos = scan_run(text, bytes, after_space, punct_at);
while pos < len && (bytes[pos] == b'\r' || bytes[pos] == b'\n') {
pos += 1;
}
out.push((start, pos));
continue;
}
if space_at(text, bytes, pos).is_some() {
let (s, e) = whitespace_span(text, bytes, pos, scheme.whitespace);
pos = e;
out.push((s, e));
continue;
}
let (_, l) = char_at(text, pos);
pos += l;
}
}
#[inline]
fn prefix_len(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
match CLASS[bytes[pos] as usize] {
Class::Newline | Class::Digit | Class::Letter => None,
Class::Lead => {
let (c, l) = char_at(text, pos);
(!is_letter_char(c) && !is_number_char(c)).then_some(l)
}
_ => Some(1),
}
}
pub(super) fn cl100k_spans(text: &str, out: &mut Vec<(usize, usize)>) {
family_spans(text, out, CL100K)
}
pub(super) fn llama3_spans(text: &str, out: &mut Vec<(usize, usize)>) {
family_spans(text, out, LLAMA3)
}
pub(super) fn qwen2_spans(text: &str, out: &mut Vec<(usize, usize)>) {
family_spans(text, out, QWEN2)
}
pub(super) fn o200k_spans(text: &str, out: &mut Vec<(usize, usize)>) {
let bytes = text.as_bytes();
let len = bytes.len();
let mut pos = 0usize;
while pos < len {
let start = pos;
if let Some(end) = o200k_letter_branches(text, bytes, pos) {
pos = end;
out.push((start, pos));
continue;
}
if let Some(n) = number_at(text, bytes, pos) {
pos += n;
for _ in 1..3 {
match (pos < len).then(|| number_at(text, bytes, pos)).flatten() {
Some(n) => pos += n,
None => break,
}
}
out.push((start, pos));
continue;
}
let after_space = if bytes[pos] == b' ' { pos + 1 } else { pos };
if after_space < len && punct_at(text, bytes, after_space).is_some() {
pos = scan_run(text, bytes, after_space, punct_at);
while pos < len && matches!(bytes[pos], b'\r' | b'\n' | b'/') {
pos += 1;
}
out.push((start, pos));
continue;
}
if space_at(text, bytes, pos).is_some() {
let (s, e) = whitespace_span(text, bytes, pos, WhitespaceOrder::NewlineFirst);
pos = e;
out.push((s, e));
continue;
}
let (_, l) = char_at(text, pos);
pos += l;
}
}
fn o200k_letter_branches(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
let with_prefix = prefix_len(text, bytes, pos).map(|p| pos + p);
for &q in [with_prefix, Some(pos)].iter().flatten() {
if q < bytes.len() {
if let Some(end) = o200k_branch_a(text, bytes, q) {
return Some(end);
}
}
}
for &q in [with_prefix, Some(pos)].iter().flatten() {
if q < bytes.len() {
if let Some(end) = o200k_branch_b(text, bytes, q) {
return Some(end);
}
}
}
None
}
fn o200k_branch_a(text: &str, bytes: &[u8], start: usize) -> Option<usize> {
let upper_end = scan_run(text, bytes, start, upper_run_at);
let mut boundary = upper_end;
loop {
if boundary < bytes.len() {
if let Some(n) = lower_run_at(text, bytes, boundary) {
let lower_end = scan_run(text, bytes, boundary + n, lower_run_at);
return Some(lower_end + trailing_contraction(bytes, lower_end));
}
}
if boundary <= start {
return None;
}
boundary = prev_char_boundary(text, start, boundary);
}
}
fn o200k_branch_b(text: &str, bytes: &[u8], start: usize) -> Option<usize> {
let upper_end = scan_run(text, bytes, start, upper_run_at);
if upper_end == start {
return None;
}
let lower_end = scan_run(text, bytes, upper_end, lower_run_at);
Some(lower_end + trailing_contraction(bytes, lower_end))
}
#[inline]
fn upper_run_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
if bytes[pos] < 0x80 {
bytes[pos].is_ascii_uppercase().then_some(1)
} else {
let (c, l) = char_at(text, pos);
is_upper_run_char(c).then_some(l)
}
}
#[inline]
fn lower_run_at(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
if bytes[pos] < 0x80 {
bytes[pos].is_ascii_lowercase().then_some(1)
} else {
let (c, l) = char_at(text, pos);
is_lower_run_char(c).then_some(l)
}
}
#[inline]
fn trailing_contraction(bytes: &[u8], pos: usize) -> usize {
contraction_len(bytes, pos).unwrap_or(0)
}
#[inline]
fn prev_char_boundary(text: &str, floor: usize, pos: usize) -> usize {
let mut p = pos.saturating_sub(1);
while p > floor && !text.is_char_boundary(p) {
p -= 1;
}
p
}
pub(super) fn deepseek_v3_pass3_spans(text: &str, out: &mut Vec<(usize, usize)>) {
let bytes = text.as_bytes();
let len = bytes.len();
let mut pos = 0usize;
while pos < len {
let start = pos;
if bytes[pos].is_ascii_graphic()
&& CLASS[bytes[pos] as usize] == Class::Punct
&& pos + 1 < len
&& bytes[pos + 1].is_ascii_alphabetic()
{
pos = swar_skip_ascii_letters(bytes, pos + 1);
out.push((start, pos));
continue;
}
if let Some(n) = letter_or_mark_at(text, bytes, pos) {
pos = scan_run(text, bytes, pos + n, letter_or_mark_at);
out.push((start, pos));
continue;
}
if let Some(prefix) = deepseek_prefix_len(text, bytes, pos) {
if let Some(n) = (pos + prefix < len)
.then(|| letter_or_mark_at(text, bytes, pos + prefix))
.flatten()
{
pos = scan_run(text, bytes, pos + prefix + n, letter_or_mark_at);
out.push((start, pos));
continue;
}
}
let after_space = if bytes[pos] == b' ' { pos + 1 } else { pos };
if after_space < len && punct_or_symbol_at(text, bytes, after_space).is_some() {
pos = scan_run(text, bytes, after_space, punct_or_symbol_at);
while pos < len && (bytes[pos] == b'\r' || bytes[pos] == b'\n') {
pos += 1;
}
out.push((start, pos));
continue;
}
if space_at(text, bytes, pos).is_some() {
let (s, e) = whitespace_span(text, bytes, pos, WhitespaceOrder::NewlineFirst);
pos = e;
out.push((s, e));
continue;
}
let (_, l) = char_at(text, pos);
pos += l;
}
}
#[inline]
fn deepseek_prefix_len(text: &str, bytes: &[u8], pos: usize) -> Option<usize> {
if CLASS[bytes[pos] as usize] == Class::Newline {
return None;
}
let (c, l) = if bytes[pos] < 0x80 {
(bytes[pos] as char, 1)
} else {
char_at(text, pos)
};
(!is_letter_char(c) && !is_punct_or_symbol_char(c)).then_some(l)
}
pub(super) fn deepseek_v3_pass1_spans(text: &str, out: &mut Vec<(usize, usize)>) {
let bytes = text.as_bytes();
let len = bytes.len();
let mut pos = 0usize;
while pos < len {
pos = swar_skip_to_number(bytes, pos);
if pos >= len {
break;
}
let Some(n) = number_at(text, bytes, pos) else {
let (_, l) = char_at(text, pos);
pos += l;
continue;
};
let start = pos;
pos += n;
for _ in 1..3 {
match (pos < len).then(|| number_at(text, bytes, pos)).flatten() {
Some(n) => pos += n,
None => break,
}
}
out.push((start, pos));
}
}
pub(super) fn deepseek_v3_pass2_spans(text: &str, out: &mut Vec<(usize, usize)>) {
let bytes = text.as_bytes();
let len = bytes.len();
let mut pos = 0usize;
while pos < len {
pos = swar_skip_ascii(bytes, pos);
if pos >= len {
break;
}
let (c, l) = char_at(text, pos);
if !is_deepseek_cjk(c) {
pos += l;
continue;
}
let start = pos;
pos += l;
while pos < len && bytes[pos] >= 0x80 {
let (c, l) = char_at(text, pos);
if !is_deepseek_cjk(c) {
break;
}
pos += l;
}
out.push((start, pos));
}
}
#[inline]
fn is_deepseek_cjk(c: char) -> bool {
matches!(c, '\u{4E00}'..='\u{9FA5}' | '\u{3040}'..='\u{30FF}')
}
pub(crate) type SpanScanner = fn(&str, &mut Vec<(usize, usize)>);
pub(crate) fn for_pattern(pattern: &str) -> Option<SpanScanner> {
use crate::core::tokenizer::patterns as p;
if pattern == p::CL100K_BASE_PATTERN {
Some(cl100k_spans)
} else if pattern == p::LLAMA3_PATTERN {
Some(llama3_spans)
} else if pattern == p::QWEN2_PATTERN {
Some(qwen2_spans)
} else if pattern == p::O200K_BASE_PATTERN {
Some(o200k_spans)
} else if pattern == p::DEEPSEEK_V3_PATTERNS[0] {
Some(deepseek_v3_pass1_spans)
} else if pattern == p::DEEPSEEK_V3_PATTERNS[1] {
Some(deepseek_v3_pass2_spans)
} else if pattern == p::DEEPSEEK_V3_PATTERNS[2] {
Some(deepseek_v3_pass3_spans)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::tokenizer::patterns::{
CL100K_BASE_PATTERN, DEEPSEEK_V3_PATTERNS, LLAMA3_PATTERN, O200K_BASE_PATTERN,
QWEN2_PATTERN,
};
type Scanner = fn(&str, &mut Vec<(usize, usize)>);
fn all_scanners() -> Vec<(&'static str, &'static str, Scanner)> {
vec![
("cl100k", CL100K_BASE_PATTERN, cl100k_spans as Scanner),
("llama3", LLAMA3_PATTERN, llama3_spans as Scanner),
("qwen2", QWEN2_PATTERN, qwen2_spans as Scanner),
("o200k", O200K_BASE_PATTERN, o200k_spans as Scanner),
(
"deepseek-pass1",
DEEPSEEK_V3_PATTERNS[0],
deepseek_v3_pass1_spans as Scanner,
),
(
"deepseek-pass2",
DEEPSEEK_V3_PATTERNS[1],
deepseek_v3_pass2_spans as Scanner,
),
(
"deepseek-pass3",
DEEPSEEK_V3_PATTERNS[2],
deepseek_v3_pass3_spans as Scanner,
),
]
}
#[test]
fn every_bundled_vocabulary_that_should_have_a_scanner_has_one() {
use crate::core::pretrained::{patterns, PretrainedVocab::*};
let no_scanner = [MistralV3, WhisperV1, WhisperV2, WhisperV3];
for vocab in [
Cl100kBase, O200kBase, GptOss, Llama3, DeepseekV3, Qwen3, Glm4, MistralV1, MistralV2,
MistralV3, WhisperV1, WhisperV2, WhisperV3,
] {
let Some(pats) = patterns(vocab) else {
continue;
};
let expected = !no_scanner.contains(&vocab);
for pattern in pats {
assert_eq!(
for_pattern(pattern).is_some(),
expected,
"{vocab:?} scanner coverage changed for {pattern}"
);
}
}
}
fn assert_agrees(name: &str, pattern: &str, scan: Scanner, input: &str) {
let re = regexr::RegexBuilder::new(pattern)
.jit(true)
.build()
.expect("pattern compiles");
let expected: Vec<(usize, usize)> =
re.find_iter(input).map(|m| (m.start(), m.end())).collect();
let mut got = Vec::new();
scan(input, &mut got);
assert_eq!(
got,
expected,
"{name} scanner disagrees with its regex on {input:?}\n scanner: {:?}\n regex: {:?}",
got.iter().map(|&(s, e)| &input[s..e]).collect::<Vec<_>>(),
expected
.iter()
.map(|&(s, e)| &input[s..e])
.collect::<Vec<_>>()
);
}
const SHAPED: &[&str] = &[
"",
" ",
" ",
" ",
"\n",
"\n\n",
" \n",
" \n ",
"\t\n\t",
"hello world",
"hello[]!?",
"[]!?",
"The[]getUserName",
"[]get",
"hello[]",
"a{}b",
"Zürich{}",
" hello",
"!hello",
"hello!",
"don't stop",
"DON'T STOP",
"it's o'clock 'tis",
"'s 'S 'll 'LL 've 're 'd 'm 't",
"'x 'zz '",
"123",
"1234",
"12345678",
" 123",
"abc123def",
"a1b2c3",
"!!!",
" !!!",
"!!!\n\n",
" ...\r\n",
"( )",
"{\"key\": \"value\", \"n\": 42}",
"def f(x):\n return x + 1\n",
"trailing ",
"trailing\n",
"trailing \n ",
" leading",
"a b",
"a b",
"a \n b",
"中文测试",
" 中文",
"中文 abc",
"café naïve",
"emoji 🚀 here",
"🚀🚀",
" 🚀",
"Ⅷ Ⅸ",
"½ ¾",
"\u{3000}abc",
"a\u{00a0}b",
"x\u{2028}y",
"\r\n\r\n",
"a\r\nb",
" \r\n ",
"word\u{0301} mark",
"XMLHttpRequest",
"camelCase",
"PascalCase",
"ALLCAPS",
"ALLCAPSthenLower",
"aB",
"Ab",
"A",
"AB",
"iPhone",
"McDonald's",
"HTTPServer's",
"ÉCOLE école",
"path/to/file",
"a//b",
"!/",
" //\n",
"1",
"12",
"1234567",
"abc 1234567 def",
"a1234567890b",
"\u{4e00}\u{9fa5}",
"\u{4dff}\u{4e00}",
"\u{9fa5}\u{9fa6}",
"\u{303f}\u{3040}",
"\u{30ff}\u{3100}",
"\u{3040}\u{309f}\u{30a0}\u{30ff}",
"ひらがな カタカナ 漢字",
"long ascii prefix before 漢字 appears",
"漢字123ひらがな",
"abcdefg",
"abcdefgh",
"abcdefghi",
"abcdefghijklmnopqrstuvwxyz",
"abcdefghijklmnopqrstuvwxyz0",
"Supercalifragilisticexpialidocious and more words here",
"aaaaaaaa\u{4e2d}",
"aaaaaaaaaaaaaaaa\u{4e2d}bbbbbbbb",
];
#[test]
fn scanners_agree_with_their_regex_on_shaped_inputs() {
for (name, pattern, scan) in all_scanners() {
for input in SHAPED {
assert_agrees(name, pattern, scan, input);
}
}
}
#[test]
fn scanners_agree_with_their_regex_on_long_inputs() {
let filler = "The quick brown fox jumps over the lazy dog. ";
let cases: Vec<String> = vec![
filler.repeat(400),
format!("{}\n{}", filler.repeat(200), filler.repeat(200)),
format!("{} \n \n {}", filler.repeat(200), filler.repeat(200)),
format!(
"{}\n/path/to/file {}",
filler.repeat(200),
filler.repeat(200)
),
format!("{}\n中文测试 {}", filler.repeat(200), filler.repeat(200)),
format!("{}\n\n\nx{}", filler.repeat(200), filler.repeat(200)),
format!("{}\n漢字ひらがな{}", filler.repeat(200), "漢字".repeat(600)),
format!("{}\n1234567890 {}", filler.repeat(200), "42 ".repeat(600)),
format!("{}\nx{} ", filler.repeat(200), filler.repeat(200)),
];
for (name, pattern, scan) in all_scanners() {
for input in &cases {
assert_agrees(name, pattern, scan, input);
}
}
}
#[test]
fn scanners_agree_at_every_run_boundary_in_a_swar_word() {
let cl100k = regexr::RegexBuilder::new(CL100K_BASE_PATTERN)
.jit(true)
.build()
.expect("cl100k pattern compiles");
for run in 0..20usize {
for byte in 0u8..=127 {
let c = byte as char;
if c.is_control() && c != '\n' && c != '\r' && c != '\t' {
continue;
}
let input = format!("{}{}{}", "a".repeat(run), c, "b".repeat(3));
let expected: Vec<(usize, usize)> = cl100k
.find_iter(&input)
.map(|m| (m.start(), m.end()))
.collect();
let mut got = Vec::new();
cl100k_spans(&input, &mut got);
assert_eq!(
got,
expected,
"run={run} byte={byte:#04x} input={input:?}\n scanner: {:?}",
got.iter().map(|&(s, e)| &input[s..e]).collect::<Vec<_>>()
);
}
}
}
#[test]
fn scanners_agree_with_their_regex_on_random_inputs() {
const ALPHABET: &[&str] = &[
"a", "z", "A", "Z", "1", "9", " ", " ", "\t", "\n", "\r", "\r\n", "'", "!", ".", ",",
"-", "_", "/", "中", "é", "É", "🚀", "\u{00a0}", "\u{3000}", "Ⅷ", "½", "\u{0301}", "«",
"€", "\u{05d0}",
];
let mut state = 0x2545_F491_4F6C_DD1Du64;
let mut next = move || {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
state.wrapping_mul(0x2545_F491_4F6C_DD1D)
};
let scanners = all_scanners();
for _ in 0..3000 {
let pieces = 1 + (next() % 14) as usize;
let mut input = String::new();
for _ in 0..pieces {
input.push_str(ALPHABET[(next() % ALPHABET.len() as u64) as usize]);
}
for &(name, pattern, scan) in &scanners {
assert_agrees(name, pattern, scan, &input);
}
}
}
}