use crate::unicode_data::{UNICODE_MAP_LOWERCASE, UNICODE_RANGES_FLAGS, UNICODE_SET_WHITESPACE};
use std::collections::HashMap;
use std::sync::OnceLock;
const MAX_CODEPOINTS: usize = 0x110000;
#[allow(dead_code)]
mod flag {
pub const UNDEFINED: u16 = 0x0001;
pub const NUMBER: u16 = 0x0002; pub const LETTER: u16 = 0x0004; pub const SEPARATOR: u16 = 0x0008; pub const ACCENT_MARK: u16 = 0x0010; pub const PUNCTUATION: u16 = 0x0020; pub const SYMBOL: u16 = 0x0040; pub const CONTROL: u16 = 0x0080; pub const WHITESPACE: u16 = 0x0100; }
pub use flag::{
ACCENT_MARK as FLAG_ACCENT_MARK, LETTER as FLAG_LETTER, NUMBER as FLAG_NUMBER,
UNDEFINED as FLAG_UNDEFINED, WHITESPACE as FLAG_WHITESPACE,
};
#[derive(Clone, Copy, Default)]
pub struct CptFlags(pub u16);
impl CptFlags {
#[inline]
pub fn is_number(self) -> bool {
self.0 & FLAG_NUMBER != 0
}
#[inline]
pub fn is_letter(self) -> bool {
self.0 & FLAG_LETTER != 0
}
#[inline]
pub fn is_accent_mark(self) -> bool {
self.0 & FLAG_ACCENT_MARK != 0
}
#[inline]
pub fn is_whitespace(self) -> bool {
self.0 & FLAG_WHITESPACE != 0
}
#[inline]
pub fn as_uint(self) -> u16 {
self.0
}
#[inline]
pub fn category_flag(self) -> u16 {
self.0 & 0x00FF
}
}
fn cpt_flags_table() -> &'static Vec<u16> {
static TABLE: OnceLock<Vec<u16>> = OnceLock::new();
TABLE.get_or_init(|| {
let mut flags = vec![FLAG_UNDEFINED; MAX_CODEPOINTS];
for i in 1..UNICODE_RANGES_FLAGS.len() {
let (ini, fl) = UNICODE_RANGES_FLAGS[i - 1];
let (end, _) = UNICODE_RANGES_FLAGS[i];
for cpt in ini..end {
flags[cpt as usize] = fl;
}
}
for &cpt in UNICODE_SET_WHITESPACE.iter() {
flags[cpt as usize] |= FLAG_WHITESPACE;
}
flags
})
}
#[inline]
pub fn cpt_flags_from_cpt(cpt: u32) -> CptFlags {
let table = cpt_flags_table();
if (cpt as usize) < MAX_CODEPOINTS {
CptFlags(table[cpt as usize])
} else {
CptFlags(FLAG_UNDEFINED)
}
}
#[inline]
pub fn tolower(cpt: u32) -> u32 {
match UNICODE_MAP_LOWERCASE.binary_search_by(|&(k, _)| k.cmp(&cpt)) {
Ok(idx) => UNICODE_MAP_LOWERCASE[idx].1,
Err(_) => cpt,
}
}
fn byte_unicode_maps() -> &'static (Vec<char>, HashMap<char, u8>) {
static MAPS: OnceLock<(Vec<char>, HashMap<char, u8>)> = OnceLock::new();
MAPS.get_or_init(|| {
let mut byte_to_char: Vec<Option<char>> = vec![None; 256];
let mut set = |ch: u32| {
byte_to_char[ch as usize] = Some(char::from_u32(ch).unwrap());
};
for ch in 0x21..=0x7E {
set(ch);
}
for ch in 0xA1..=0xAC {
set(ch);
}
for ch in 0xAE..=0xFF {
set(ch);
}
let mut n: u32 = 0;
for ch in 0..256u32 {
if byte_to_char[ch as usize].is_none() {
byte_to_char[ch as usize] = Some(char::from_u32(256 + n).unwrap());
n += 1;
}
}
let b2c: Vec<char> = byte_to_char.into_iter().map(|c| c.unwrap()).collect();
let mut c2b: HashMap<char, u8> = HashMap::with_capacity(256);
for (b, &c) in b2c.iter().enumerate() {
c2b.insert(c, b as u8);
}
(b2c, c2b)
})
}
#[inline]
pub fn byte_to_unicode(byte: u8) -> char {
byte_unicode_maps().0[byte as usize]
}
#[inline]
pub fn unicode_to_byte(c: char) -> Option<u8> {
byte_unicode_maps().1.get(&c).copied()
}
pub fn byte_encode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
out.push(byte_to_unicode(b));
}
out
}
pub fn split_qwen35(text: &str) -> Vec<String> {
let cpts: Vec<u32> = text.chars().map(|c| c as u32).collect();
let cpt_bytes: Vec<usize> = text.chars().map(|c| c.len_utf8()).collect();
let n = cpts.len();
const OOR: u32 = 0xFFFF_FFFF;
let get_cpt = |pos: usize| -> u32 { if pos < n { cpts[pos] } else { OOR } };
let get_flags = |pos: usize| -> CptFlags {
if pos < n {
cpt_flags_from_cpt(cpts[pos])
} else {
CptFlags::default()
}
};
let mut lens: Vec<usize> = Vec::new(); let mut prev_end = 0usize;
let add_token = |end: usize, prev_end: &mut usize, lens: &mut Vec<usize>| -> usize {
debug_assert!(*prev_end <= end && end <= n);
let len = end - *prev_end;
if len > 0 {
lens.push(len);
}
*prev_end = end;
len
};
let mut pos = 0usize;
while pos < n {
let cpt = get_cpt(pos);
let flags = get_flags(pos);
if cpt == b'\'' as u32 && pos + 1 < n {
let cpt_next = tolower(get_cpt(pos + 1));
if cpt_next == 's' as u32
|| cpt_next == 't' as u32
|| cpt_next == 'm' as u32
|| cpt_next == 'd' as u32
{
pos += add_token(pos + 2, &mut prev_end, &mut lens);
continue;
}
if pos + 2 < n {
let cpt_nn = tolower(get_cpt(pos + 2));
if (cpt_next == 'r' as u32 && cpt_nn == 'e' as u32)
|| (cpt_next == 'v' as u32 && cpt_nn == 'e' as u32)
|| (cpt_next == 'l' as u32 && cpt_nn == 'l' as u32)
{
pos += add_token(pos + 3, &mut prev_end, &mut lens);
continue;
}
}
}
if !(cpt == '\r' as u32 || cpt == '\n' as u32 || flags.is_number()) {
if flags.is_letter()
|| flags.is_accent_mark()
|| get_flags(pos + 1).is_accent_mark()
|| get_flags(pos + 1).is_letter()
{
pos += 1;
while get_flags(pos).is_letter() || get_flags(pos).is_accent_mark() {
pos += 1;
}
add_token(pos, &mut prev_end, &mut lens);
continue;
}
}
if flags.is_number() {
pos += 1;
add_token(pos, &mut prev_end, &mut lens);
continue;
}
let mut flags2 = if cpt == ' ' as u32 {
get_flags(pos + 1)
} else {
flags
};
if !(flags2.is_whitespace()
|| flags2.is_letter()
|| flags2.is_accent_mark()
|| flags2.is_number())
&& flags.as_uint() != 0
{
pos += (cpt == ' ' as u32) as usize;
while !(flags2.is_whitespace()
|| flags2.is_letter()
|| flags2.is_accent_mark()
|| flags2.is_number())
&& flags2.as_uint() != 0
{
pos += 1;
flags2 = get_flags(pos);
}
let mut cpt2 = get_cpt(pos);
while cpt2 == '\r' as u32 || cpt2 == '\n' as u32 {
pos += 1;
cpt2 = get_cpt(pos);
}
add_token(pos, &mut prev_end, &mut lens);
continue;
}
let mut num_ws = 0usize;
let mut last_end_rn = 0usize;
while get_flags(pos + num_ws).is_whitespace() {
let cpt2 = get_cpt(pos + num_ws);
if cpt2 == '\r' as u32 || cpt2 == '\n' as u32 {
last_end_rn = pos + num_ws + 1;
}
num_ws += 1;
}
if last_end_rn > 0 {
pos = last_end_rn;
add_token(pos, &mut prev_end, &mut lens);
continue;
}
if num_ws > 1 && get_cpt(pos + num_ws) != OOR {
pos += num_ws - 1;
add_token(pos, &mut prev_end, &mut lens);
continue;
}
if num_ws > 0 {
pos += num_ws;
add_token(pos, &mut prev_end, &mut lens);
continue;
}
pos += 1;
add_token(pos, &mut prev_end, &mut lens);
}
let mut words = Vec::with_capacity(lens.len());
let mut cpt_i = 0usize;
let mut byte_i = 0usize;
for &len in &lens {
let mut nbytes = 0usize;
for k in 0..len {
nbytes += cpt_bytes[cpt_i + k];
}
words.push(text[byte_i..byte_i + nbytes].to_string());
cpt_i += len;
byte_i += nbytes;
}
words
}
#[inline]
fn collapse_cpt(cpt: u32) -> u8 {
if cpt < 128 {
return cpt as u8;
}
let fl = cpt_flags_from_cpt(cpt);
if fl.is_whitespace() {
return 0x0B; }
match fl.category_flag() {
FLAG_NUMBER => 0xD1,
FLAG_LETTER => 0xD2,
flag::PUNCTUATION => 0xD3,
FLAG_ACCENT_MARK => 0xD4,
flag::SYMBOL => 0xD5,
_ => 0xD0, }
}
#[inline]
fn c_is_letter(b: u8) -> bool {
b == 0xD2 || b.is_ascii_alphabetic()
}
#[inline]
fn c_is_mark(b: u8) -> bool {
b == 0xD4 }
#[inline]
fn c_is_punct(b: u8) -> bool {
b == 0xD3
|| matches!(b,
0x21..=0x23 | 0x25..=0x2A | 0x2C..=0x2F | 0x3A..=0x3B | 0x3F..=0x40
| 0x5B..=0x5D | 0x5F | 0x7B | 0x7D)
}
#[inline]
fn c_is_symbol(b: u8) -> bool {
b == 0xD5 || matches!(b, 0x24 | 0x2B | 0x3C..=0x3E | 0x5E | 0x60 | 0x7C | 0x7E)
}
#[inline]
fn c_is_number(b: u8) -> bool {
b == 0xD1 || b.is_ascii_digit()
}
#[inline]
fn c_is_space(b: u8) -> bool {
matches!(b, 0x20 | 0x09..=0x0D)
}
#[inline]
fn c_is_ascii_punct_lit(b: u8) -> bool {
matches!(b,
0x21..=0x2F | 0x3A..=0x40 | 0x5B..=0x60 | 0x7B..=0x7E)
}
#[inline]
fn push_len(lens: &mut Vec<usize>, len: usize) {
if len > 0 {
lens.push(len);
}
}
fn split_pass<F>(offsets: &[usize], mut matcher: F) -> Vec<usize>
where
F: FnMut(usize, usize, usize) -> Option<usize>,
{
let mut out: Vec<usize> = Vec::with_capacity(offsets.len());
let mut start = 0usize;
for &off in offsets {
let end = start + off;
let mut gap = start; let mut pos = start;
while pos < end {
match matcher(start, end, pos) {
Some(m_end) if m_end > pos => {
push_len(&mut out, pos - gap);
push_len(&mut out, m_end - pos);
gap = m_end;
pos = m_end;
}
_ => pos += 1,
}
}
push_len(&mut out, end - gap);
start = end;
}
out
}
#[inline]
fn is_cjk_kana(cpt: u32) -> bool {
(0x4E00..=0x9FA5).contains(&cpt)
|| (0x3040..=0x309F).contains(&cpt)
|| (0x30A0..=0x30FF).contains(&cpt)
}
pub fn split_deepseek_v3(text: &str) -> Vec<String> {
let cpts: Vec<u32> = text.chars().map(|c| c as u32).collect();
let cpt_bytes: Vec<usize> = text.chars().map(|c| c.len_utf8()).collect();
let n = cpts.len();
let coll: Vec<u8> = cpts.iter().map(|&c| collapse_cpt(c)).collect();
let mut offsets = vec![n];
offsets = split_pass(&offsets, |_s, end, pos| {
if !c_is_number(coll[pos]) {
return None;
}
let mut e = pos + 1;
while e < end && e - pos < 3 && c_is_number(coll[e]) {
e += 1;
}
Some(e)
});
offsets = split_pass(&offsets, |_s, end, pos| {
if !is_cjk_kana(cpts[pos]) {
return None;
}
let mut e = pos + 1;
while e < end && is_cjk_kana(cpts[e]) {
e += 1;
}
Some(e)
});
offsets = split_pass(&offsets, |_s, end, pos| {
let b = coll[pos];
if c_is_ascii_punct_lit(b) && pos + 1 < end && coll[pos + 1].is_ascii_alphabetic() {
let mut e = pos + 2;
while e < end && coll[e].is_ascii_alphabetic() {
e += 1;
}
return Some(e);
}
{
let lead_ok =
b != b'\r' && b != b'\n' && !c_is_letter(b) && !c_is_punct(b) && !c_is_symbol(b);
let mut e = pos;
if lead_ok && pos + 1 < end && (c_is_letter(coll[pos + 1]) || c_is_mark(coll[pos + 1]))
{
e = pos + 1;
} else if !(c_is_letter(b) || c_is_mark(b)) {
e = usize::MAX; }
if e != usize::MAX {
let run_start = e;
while e < end && (c_is_letter(coll[e]) || c_is_mark(coll[e])) {
e += 1;
}
if e > run_start {
return Some(e);
}
}
}
{
let mut e = pos;
if b == b' ' {
e += 1;
}
let run_start = e;
while e < end && (c_is_punct(coll[e]) || c_is_symbol(coll[e])) {
e += 1;
}
if e > run_start {
while e < end && (coll[e] == b'\r' || coll[e] == b'\n') {
e += 1;
}
return Some(e);
}
}
if c_is_space(b) {
let mut e = pos;
let mut last_rn = None;
while e < end && c_is_space(coll[e]) {
if coll[e] == b'\r' || coll[e] == b'\n' {
last_rn = Some(e + 1);
}
e += 1;
}
if let Some(rn_end) = last_rn {
return Some(rn_end);
}
let run_end = e;
if run_end < end {
if run_end - pos > 1 {
return Some(run_end - 1);
}
return Some(pos + 1);
}
return Some(run_end); }
None
});
let mut words = Vec::with_capacity(offsets.len());
let mut cpt_i = 0usize;
let mut byte_i = 0usize;
for &len in &offsets {
let nbytes: usize = cpt_bytes[cpt_i..cpt_i + len].iter().sum();
words.push(text[byte_i..byte_i + nbytes].to_string());
cpt_i += len;
byte_i += nbytes;
}
words
}
#[cfg(test)]
mod tests {
use super::*;
const DS3_CASES: &[(&str, &[&str])] = &[
("Hello world", &["Hello", " world"]),
("Hello, world!", &["Hello", ",", " world", "!"]),
(
" leading and trailing ",
&[" leading", " and", " trailing", " "],
),
(
"don't can't we're I've I'm you'll he'd",
&[
"don", "'t", " can", "'t", " we", "'re", " I", "'ve", " I", "'m", " you", "'ll",
" he", "'d",
],
),
("1234567 89 0", &["123", "456", "7", " ", "89", " ", "0"]),
(
"v0.71.0 and 128K ctx",
&[
"v", "0", ".", "71", ".", "0", " and", " ", "128", "K", " ctx",
],
),
(
"Step-3.7-Flash: 196B-A11B (45 blocks)",
&[
"Step", "-", "3", ".", "7", "-Flash", ":", " ", "196", "B", "-A", "11", "B", " (",
"45", " blocks", ")",
],
),
(
"line1\nline2\r\nline3",
&["line", "1", "\n", "line", "2", "\r\n", "line", "3"],
),
(
"trailing newlines\n\n\n",
&["trailing", " newlines", "\n\n\n"],
),
(
"tabs\tand\t\tspaces x",
&["tabs", "\tand", "\t", "\tspaces", " ", " x"],
),
("\n\n \n indented", &["\n\n \n", " indented"]),
("中文测试", &["中文测试"]),
(
"混合 English 中文 123",
&["混合", " English", " ", "中文", " ", "123"],
),
(
"日本語のテスト、カタカナ",
&["日本語のテスト", "、", "カタカナ"],
),
("한국어 테스트", &["한국어", " 테스트"]),
(
"emoji 🚀 and symbols ~ ^ | $ +",
&[
"emoji", " 🚀", " and", " symbols", " ~", " ^", " |", " $", " +",
],
),
("naïve café résumé", &["naïve", " café", " résumé"]),
("Ünïcödé mÄrks", &["Ünïcödé", " mÄrks"]),
("áb̧c", &["áb̧c"]),
(
"MoE top-8 288 experts@4096",
&[
"MoE", " top", "-", "8", " ", "288", " experts", "@", "409", "6",
],
),
(" ", &[" "]),
(" ", &[" "]),
("", &[]),
("\t", &["\t"]),
("\n", &["\n"]),
("x", &["x"]),
("@#$%^&*()", &["@#$%^&*()"]),
(
"snake_case camelCase kebab-case",
&["snake", "_case", " camelCase", " kebab", "-case"],
),
("path/to/file.gguf", &["path", "/to", "/file", ".gguf"]),
(
"{\"key\": [1, 2, 3]}",
&[
"{\"", "key", "\":", " [", "1", ",", " ", "2", ",", " ", "3", "]}",
],
),
(
"5e6 vs 1e4 rope base",
&["5", "e", "6", " vs", " ", "1", "e", "4", " rope", " base"],
),
("ЖИВЁТ русский текст", &["ЖИВЁТ", " русский", " текст"]),
("Ελληνικά κείμενα", &["Ελληνικά", " κείμενα"]),
("العربية نص", &["العربية", " نص"]),
("▁escaped▁space", &["▁", "escaped", "▁", "space"]),
(
"100%% sure? yes!!!",
&["100", "%%", " sure", "?", " yes", "!!!"],
),
(" .a", &[" .", "a"]),
(" a", &[" a"]),
("..", &[".."]),
("a1", &["a", "1"]),
("1a", &["1", "a"]),
("12345678901234", &["123", "456", "789", "012", "34"]),
(" 123", &[" ", "123"]),
("123 ", &["123", " "]),
(" 123 ", &[" ", "123", " "]),
("-abc", &["-abc"]),
("-abc1", &["-abc", "1"]),
("~abc", &["~abc"]),
("~", &["~"]),
("~ ^", &["~", " ^"]),
(" nbsp", &[" nbsp"]),
("a b", &["a", " ", " b"]),
("x \n y", &["x", " \n", " y"]),
("x \n\n y", &["x", " \n\n", " ", " y"]),
("end with space ", &["end", " with", " space", " "]),
("end with spaces ", &["end", " with", " spaces", " "]),
("\r", &["\r"]),
("\r\r\n\n", &["\r\r\n\n"]),
(" \n ", &[" \n", " "]),
("́leading mark", &["́leading", " mark"]),
("中1文2", &["中", "1", "文", "2"]),
("ーヽヾ", &["ーヽヾ"]),
("龥龦", &["龥", "龦"]),
("〿", &["", "〿"]),
];
#[test]
fn deepseek_v3_split_matches_reference() {
for (text, want) in DS3_CASES {
let got = split_deepseek_v3(text);
assert_eq!(got, *want, "split_deepseek_v3({text:?})");
assert_eq!(got.concat(), *text, "reassembly of {text:?}");
}
}
#[test]
fn deepseek_v3_differs_from_qwen35_per_mechanism() {
let q = |t: &str| split_qwen35(t);
let d = |t: &str| split_deepseek_v3(t);
assert_eq!(d("12345678901234"), ["123", "456", "789", "012", "34"]);
assert_eq!(q("1234").len(), 4, "qwen35 emits one token per digit");
assert_eq!(
d("日本語のテスト、カタカナ"),
["日本語のテスト", "、", "カタカナ"]
);
assert_eq!(
q("日本語のテスト、カタカナ"),
["日本語のテスト", "、カタカナ"]
);
assert_eq!(
d(" 中文"),
[" ", "中文"],
"pass 2 runs before the letter alternative"
);
assert_eq!(q(" 中文"), [" 中文"]);
assert_eq!(d("▁escaped▁space"), ["▁", "escaped", "▁", "space"]);
assert_eq!(q("▁escaped▁space"), ["▁escaped", "▁space"]);
assert_eq!(d("a\u{200b}!"), ["a", "\u{200b}", "!"]);
assert_eq!(q("a\u{200b}!"), ["a", "\u{200b}!"]);
assert_eq!(d(" symbols ~ ^"), [" symbols", " ~", " ^"]);
assert_eq!(d("-abc1"), ["-abc", "1"]);
assert_eq!(d("don't"), ["don", "'t"]);
}
#[test]
fn collapse_map_category_bytes() {
assert_eq!(collapse_cpt(b'a' as u32), b'a', "ASCII passes through");
assert_eq!(collapse_cpt(0x4E2D), 0xD2, "CJK ideograph is a LETTER");
assert_eq!(collapse_cpt(0x00E9), 0xD2, "e-acute is a LETTER");
assert_eq!(
collapse_cpt(0x0301),
0xD4,
"combining acute is an ACCENT_MARK"
);
assert_eq!(
collapse_cpt(0x3001),
0xD3,
"ideographic comma is PUNCTUATION"
);
assert_eq!(
collapse_cpt(0x00A0),
0x0B,
"NBSP collapses to the ws stand-in"
);
assert_eq!(collapse_cpt(0x0660), 0xD1, "Arabic-Indic digit is a NUMBER");
}
}