#![cfg(not(target_arch = "wasm32"))]
use bitcannon::Span;
use bitcannon::classify::classify;
use bitcannon::regexes::{
CL100K, DEEPSEEK_BIG as DS_BIG, DEEPSEEK_CJK as DS_CJK, DEEPSEEK_NUM as DS_NUM, GPT2, KIMI_K2,
O200K, TEKKEN,
};
use onig::Regex;
const CORPUS: &str = "The quick brown fox. Don't 12345 numbers, \u{00BD}\u{00B2}\u{00BC} \u{2168}! \
café × naïve — Привет, наука! Ελλάδα 中文分词。ひらがな カタカナ 한글 مرحبا العربية \
नरेंद्र मोदी x_y a1b2c3 e-mail@host.com 😀👍 hello world\ttabs\nnewlines end ";
const EDGE: &str = "IT'S O'Brien can't 'quoted' l'été rock'n'roll\r\n\
0 42 999 1000 1234567 v1.2.3 3.14159 1,000,000 \
https://host/a/b?c=1&d=2 path/to//file /\r\n/ ///x \
CamelCase XMLHttpRequest IJSSELMEER DžAMBO ŀl a\u{0301}b \
日本語1234テスト ½3¼ \u{2168}42\u{2169} #tag @user $9.99 100% \
end\n\n\nlines\r\n\r\n \t trailing ";
type Split = fn(&[u8], &[u8], &mut [u64], &mut [u64], &mut [u64], &mut [Span]) -> usize;
fn bs_deepseek(
t: &[u8],
g: &[u8],
s: &mut [u64],
_f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_deepseek(t, g, s, o)
}
fn bs_byte_level(
t: &[u8],
g: &[u8],
s: &mut [u64],
f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_byte_level(t, g, s, f, o)
}
fn bs_cl100k(
t: &[u8],
g: &[u8],
s: &mut [u64],
f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_cl100k(t, g, s, f, o)
}
fn bs_qwen(
t: &[u8],
g: &[u8],
s: &mut [u64],
f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_qwen(t, g, s, f, o)
}
fn bs_o200k(
t: &[u8],
g: &[u8],
s: &mut [u64],
f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_o200k(t, g, s, f, _l, o)
}
fn bs_tekken(
t: &[u8],
g: &[u8],
s: &mut [u64],
f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_tekken(t, g, s, f, _l, o)
}
fn bs_kimi(
t: &[u8],
g: &[u8],
s: &mut [u64],
f: &mut [u64],
_l: &mut [u64],
o: &mut [Span],
) -> usize {
bitcannon::bitcannon_kimi(t, g, s, f, _l, o)
}
fn spans(f: Split, s: &str) -> Vec<Span> {
if s.is_empty() {
return Vec::new();
}
let b = s.as_bytes();
let mut tags = vec![0u8; b.len()];
classify(b, &mut tags);
let nblk = b.len().div_ceil(64);
let (mut starts, mut flag) = (vec![0u64; nblk], vec![0u64; nblk]);
let mut later = vec![0u64; 2 * nblk];
let mut out = vec![Span::default(); b.len() + 1];
let k = f(b, &tags, &mut starts, &mut flag, &mut later, &mut out);
out.truncate(k);
out
}
fn onig_spans(re: &Regex, s: &str) -> Vec<Span> {
re.find_iter(s)
.map(|(a, b)| Span::new(a as u32, b as u32))
.collect()
}
fn split_iso(text: &str, s: usize, e: usize, re: &Regex, out: &mut Vec<(usize, usize)>) {
let sub = &text[s..e];
let mut prev = 0usize;
for (ms, me) in re.find_iter(sub) {
if ms > prev {
out.push((s + prev, s + ms));
}
out.push((s + ms, s + me));
prev = me;
}
if prev < sub.len() {
out.push((s + prev, e));
}
}
fn deepseek_ref(text: &str) -> Vec<Span> {
let (rn, rc, rb) = (
Regex::new(DS_NUM).unwrap(),
Regex::new(DS_CJK).unwrap(),
Regex::new(DS_BIG).unwrap(),
);
let mut a = Vec::new();
split_iso(text, 0, text.len(), &rn, &mut a);
let mut b = Vec::new();
for (s, e) in a {
split_iso(text, s, e, &rc, &mut b);
}
let mut c = Vec::new();
for (s, e) in b {
split_iso(text, s, e, &rb, &mut c);
}
c.into_iter()
.map(|(s, e)| Span::new(s as u32, e as u32))
.collect()
}
enum Oracle {
Whole(Regex),
DeepSeek,
}
impl Oracle {
fn spans(&self, s: &str) -> Vec<Span> {
match self {
Oracle::Whole(re) => onig_spans(re, s),
Oracle::DeepSeek => deepseek_ref(s),
}
}
}
fn long_corpus() -> String {
let mut s = String::new();
while s.len() < 4096 {
s.push_str(CORPUS);
s.push_str(EDGE);
}
s
}
fn check(name: &str, f: Split, oracle: &Oracle) {
for text in [CORPUS, EDGE] {
assert_eq!(spans(f, text), oracle.spans(text), "{name}: whole {text:?}");
}
let long = long_corpus();
let mut checked = 0usize;
for off in 0..512.min(long.len()) {
if !long.is_char_boundary(off) {
continue;
}
let sub = &long[off..];
assert_eq!(spans(f, sub), oracle.spans(sub), "{name}: suffix off={off}");
checked += 1;
}
for end in (long.len().saturating_sub(512))..=long.len() {
if !long.is_char_boundary(end) {
continue;
}
let sub = &long[..end];
assert_eq!(spans(f, sub), oracle.spans(sub), "{name}: prefix end={end}");
checked += 1;
}
assert!(checked > 500, "{name}: sweep too small ({checked})");
}
fn fuzz_texts(n: usize) -> Vec<String> {
const ALPHA: &[&str] = &[
"a", "b", "z", "A", "Q", "é", "ß", "IJ", "0", "1", "9", " ", " ", "\n", "\r\n", "\t", "'",
"'s", "'ll", ".", "!", "/", "#", "_", "中", "文", "ひ", "カ", "한", "م", "😀", "\u{0301}",
"\u{200D}", "½", "\u{2168}",
];
let mut st = 0x243F_6A88_85A3_08D3u64;
let mut next = move || {
st ^= st << 13;
st ^= st >> 7;
st ^= st << 17;
st
};
(0..n)
.map(|_| {
let len = 1 + (next() % 300) as usize;
(0..len)
.map(|_| ALPHA[(next() % ALPHA.len() as u64) as usize])
.collect()
})
.collect()
}
fn check_fuzz(name: &str, f: Split, oracle: &Oracle) {
for (i, t) in fuzz_texts(4000).iter().enumerate() {
assert_eq!(spans(f, t), oracle.spans(t), "{name}: fuzz #{i} {t:?}");
}
}
#[test]
fn byte_level_parity() {
let o = Oracle::Whole(Regex::new(GPT2).unwrap());
check("byte_level", bs_byte_level, &o);
check_fuzz("byte_level", bs_byte_level, &o);
}
#[test]
fn cl100k_parity() {
let o = Oracle::Whole(Regex::new(CL100K).unwrap());
check("cl100k", bs_cl100k, &o);
check_fuzz("cl100k", bs_cl100k, &o);
}
#[test]
fn qwen_parity() {
let qwen = CL100K.replace(r"\p{N}{1,3}", r"\p{N}");
let o = Oracle::Whole(Regex::new(&qwen).unwrap());
check("qwen", bs_qwen, &o);
check_fuzz("qwen", bs_qwen, &o);
}
#[test]
fn deepseek_parity() {
check("deepseek", bs_deepseek, &Oracle::DeepSeek);
check_fuzz("deepseek", bs_deepseek, &Oracle::DeepSeek);
}
#[test]
fn o200k_parity() {
let o = Oracle::Whole(Regex::new(O200K).unwrap());
check("o200k", bs_o200k, &o);
check_fuzz("o200k", bs_o200k, &o);
}
#[test]
fn tekken_parity() {
let o = Oracle::Whole(Regex::new(TEKKEN).unwrap());
check("tekken", bs_tekken, &o);
check_fuzz("tekken", bs_tekken, &o);
}
#[test]
fn kimi_parity() {
let o = Oracle::Whole(Regex::new(KIMI_K2).unwrap());
check("kimi", bs_kimi, &o);
check_fuzz("kimi", bs_kimi, &o);
}
#[test]
#[should_panic(expected = "negative-control")]
fn harness_discriminates() {
check(
"negative-control",
bs_byte_level,
&Oracle::Whole(Regex::new(CL100K).unwrap()),
);
}