use regexr::RegexBuilder;
use serde_json::Value;
use super::byte_level::byte_level_encode;
use super::tokenizer::GPT2_PATTERN;
#[derive(Debug, Clone, Copy, PartialEq)]
enum Behavior {
Isolated,
Removed,
MergedWithPrevious,
MergedWithNext,
Contiguous,
}
impl Behavior {
fn parse(s: Option<&str>) -> Self {
match s {
Some("Removed") => Behavior::Removed,
Some("MergedWithPrevious") => Behavior::MergedWithPrevious,
Some("MergedWithNext") => Behavior::MergedWithNext,
Some("Contiguous") => Behavior::Contiguous,
_ => Behavior::Isolated,
}
}
}
enum Stage {
Split {
re: Box<regexr::Regex>,
behavior: Behavior,
invert: bool,
},
ByteLevel { re: Option<Box<regexr::Regex>> },
Digits { individual: bool },
Punctuation { behavior: Behavior },
WhitespaceSplit,
Whitespace { re: Box<regexr::Regex> },
}
pub struct PreTokenizer {
stages: Vec<Stage>,
add_prefix_space: bool,
pub byte_level: bool,
}
impl PreTokenizer {
pub fn split(&self, text: &str) -> Vec<String> {
let mut pieces: Vec<String> =
if self.add_prefix_space && !text.starts_with(|c: char| c.is_whitespace()) {
vec![format!(" {text}")]
} else {
vec![text.to_string()]
};
for stage in &self.stages {
let mut next = Vec::with_capacity(pieces.len());
for p in &pieces {
stage.apply(p, &mut next);
}
pieces = next;
}
pieces.retain(|p| !p.is_empty());
pieces
}
}
impl Stage {
fn apply(&self, piece: &str, out: &mut Vec<String>) {
match self {
Stage::Split {
re,
behavior,
invert,
} => split_regex(piece, re, *behavior, *invert, out),
Stage::ByteLevel { re } => match re {
Some(re) => {
let mut raw = Vec::new();
split_regex(piece, re, Behavior::Isolated, false, &mut raw);
for r in raw {
out.push(byte_level_encode(r.as_bytes()));
}
}
None => out.push(byte_level_encode(piece.as_bytes())),
},
Stage::Digits { individual } => split_digits(piece, *individual, out),
Stage::Punctuation { behavior } => split_punctuation(piece, *behavior, out),
Stage::WhitespaceSplit => {
for w in piece.split_whitespace() {
out.push(w.to_string());
}
}
Stage::Whitespace { re } => split_regex(piece, re, Behavior::Isolated, false, out),
}
}
}
fn split_regex(
piece: &str,
re: ®exr::Regex,
behavior: Behavior,
invert: bool,
out: &mut Vec<String>,
) {
let matches: Vec<(usize, usize)> = re.find_iter(piece).map(|m| (m.start(), m.end())).collect();
let delims: Vec<(usize, usize)> = if invert {
let mut d = Vec::new();
let mut last = 0;
for &(s, e) in &matches {
if s > last {
d.push((last, s));
}
last = e;
}
if last < piece.len() {
d.push((last, piece.len()));
}
d
} else {
matches
};
let mut segs: Vec<(&str, bool)> = Vec::with_capacity(delims.len() * 2 + 1);
let mut last = 0;
for (s, e) in delims {
if s > last {
segs.push((&piece[last..s], false));
}
if e > s {
segs.push((&piece[s..e], true));
}
last = e;
}
if last < piece.len() {
segs.push((&piece[last..], false));
}
emit_segments(&segs, behavior, out);
}
fn emit_segments(segs: &[(&str, bool)], behavior: Behavior, out: &mut Vec<String>) {
match behavior {
Behavior::Isolated => {
for &(t, _) in segs {
out.push(t.to_string());
}
}
Behavior::Removed => {
for &(t, d) in segs {
if !d {
out.push(t.to_string());
}
}
}
Behavior::MergedWithPrevious => {
let mut local: Vec<String> = Vec::new();
for &(t, d) in segs {
if d {
if let Some(prev) = local.last_mut() {
prev.push_str(t);
} else {
local.push(t.to_string());
}
} else {
local.push(t.to_string());
}
}
out.extend(local);
}
Behavior::MergedWithNext => {
let mut pending = String::new();
for &(t, d) in segs {
if d {
pending.push_str(t);
} else {
let mut s = std::mem::take(&mut pending);
s.push_str(t);
out.push(s);
}
}
if !pending.is_empty() {
out.push(pending);
}
}
Behavior::Contiguous => {
let mut run = String::new();
for &(t, d) in segs {
if d {
run.push_str(t);
} else {
if !run.is_empty() {
out.push(std::mem::take(&mut run));
}
out.push(t.to_string());
}
}
if !run.is_empty() {
out.push(run);
}
}
}
}
fn split_digits(piece: &str, individual: bool, out: &mut Vec<String>) {
let mut cur = String::new();
let mut cur_is_digit = false;
for c in piece.chars() {
let d = c.is_numeric();
if !cur.is_empty() && (d != cur_is_digit || (d && individual)) {
out.push(std::mem::take(&mut cur));
}
cur.push(c);
cur_is_digit = d;
if d && individual {
out.push(std::mem::take(&mut cur));
}
}
if !cur.is_empty() {
out.push(cur);
}
}
fn split_punctuation(piece: &str, behavior: Behavior, out: &mut Vec<String>) {
let mut segs: Vec<(&str, bool)> = Vec::new();
let mut content_start = 0;
let mut i = 0;
for c in piece.chars() {
let len = c.len_utf8();
if is_punctuation(c) {
if i > content_start {
segs.push((&piece[content_start..i], false));
}
segs.push((&piece[i..i + len], true));
content_start = i + len;
}
i += len;
}
if i > content_start {
segs.push((&piece[content_start..i], false));
}
emit_segments(&segs, behavior, out);
}
fn is_punctuation(c: char) -> bool {
if c.is_ascii() {
return c.is_ascii_punctuation();
}
use unicode_general_category::{get_general_category, GeneralCategory::*};
matches!(
get_general_category(c),
ConnectorPunctuation
| DashPunctuation
| ClosePunctuation
| FinalPunctuation
| InitialPunctuation
| OtherPunctuation
| OpenPunctuation
)
}
fn gpt2_regex() -> regexr::Regex {
RegexBuilder::new(GPT2_PATTERN)
.jit(true)
.build()
.expect("GPT2_PATTERN compiles")
}
fn whitespace_regex() -> regexr::Regex {
RegexBuilder::new(r"\w+|[^\w\s]+")
.jit(true)
.build()
.expect("whitespace regex compiles")
}
pub fn parse(pre: Option<&Value>) -> Option<PreTokenizer> {
let pre = pre?;
let mut stages = Vec::new();
let mut byte_level = false;
let mut add_prefix_space = false;
fn walk(
v: &Value,
stages: &mut Vec<Stage>,
byte_level: &mut bool,
add_prefix_space: &mut bool,
) {
match v.get("type").and_then(Value::as_str) {
Some("Sequence") => {
if let Some(list) = v.get("pretokenizers").and_then(Value::as_array) {
for item in list {
walk(item, stages, byte_level, add_prefix_space);
}
}
}
Some("ByteLevel") => {
*byte_level = true;
if v.get("add_prefix_space").and_then(Value::as_bool) == Some(true) {
*add_prefix_space = true;
}
let use_regex = v.get("use_regex").and_then(Value::as_bool).unwrap_or(true);
let re = use_regex.then(|| Box::new(gpt2_regex()));
stages.push(Stage::ByteLevel { re });
}
Some("Split") => {
let pat = v.get("pattern").and_then(|p| {
p.get("Regex")
.and_then(Value::as_str)
.or_else(|| p.get("String").and_then(Value::as_str))
});
if let Some(pat) = pat {
if let Ok(re) = RegexBuilder::new(pat).jit(true).build() {
stages.push(Stage::Split {
re: Box::new(re),
behavior: Behavior::parse(v.get("behavior").and_then(Value::as_str)),
invert: v.get("invert").and_then(Value::as_bool).unwrap_or(false),
});
}
}
}
Some("Digits") => stages.push(Stage::Digits {
individual: v
.get("individual_digits")
.and_then(Value::as_bool)
.unwrap_or(false),
}),
Some("Punctuation") => stages.push(Stage::Punctuation {
behavior: Behavior::parse(v.get("behavior").and_then(Value::as_str)),
}),
Some("WhitespaceSplit") => stages.push(Stage::WhitespaceSplit),
Some("Whitespace") => stages.push(Stage::Whitespace {
re: Box::new(whitespace_regex()),
}),
_ => {}
}
}
walk(pre, &mut stages, &mut byte_level, &mut add_prefix_space);
if stages.is_empty() {
return None;
}
Some(PreTokenizer {
stages,
add_prefix_space,
byte_level,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn split(piece: &str, f: impl Fn(&str, &mut Vec<String>)) -> Vec<String> {
let mut out = Vec::new();
f(piece, &mut out);
out
}
#[test]
fn digits_grouped_and_individual() {
assert_eq!(
split("abc123def", |p, o| split_digits(p, false, o)),
vec!["abc", "123", "def"]
);
assert_eq!(
split("abc123", |p, o| split_digits(p, true, o)),
vec!["abc", "1", "2", "3"]
);
assert_eq!(
split("a²½b", |p, o| split_digits(p, true, o)),
vec!["a", "²", "½", "b"]
);
}
#[test]
fn punctuation_isolated_and_contiguous() {
assert_eq!(
split("a,b!", |p, o| split_punctuation(p, Behavior::Isolated, o)),
vec!["a", ",", "b", "!"]
);
assert_eq!(
split("a)=b", |p, o| split_punctuation(p, Behavior::Contiguous, o)),
vec!["a", ")=", "b"]
);
assert_eq!(
split("a,b", |p, o| split_punctuation(p, Behavior::Removed, o)),
vec!["a", "b"]
);
}
#[test]
fn split_merge_behaviors() {
let re = RegexBuilder::new(r"\s+").jit(true).build().unwrap();
let go = |b| split("a b c", |p, o| split_regex(p, &re, b, false, o));
assert_eq!(go(Behavior::Isolated), vec!["a", " ", "b", " ", "c"]);
assert_eq!(go(Behavior::Removed), vec!["a", "b", "c"]);
assert_eq!(go(Behavior::MergedWithPrevious), vec!["a ", "b ", "c"]);
assert_eq!(go(Behavior::MergedWithNext), vec!["a", " b", " c"]);
let re2 = RegexBuilder::new(r"\s").jit(true).build().unwrap();
assert_eq!(
split("a b", |p, o| split_regex(
p,
&re2,
Behavior::Contiguous,
false,
o
)),
vec!["a", " ", "b"]
);
}
#[test]
fn pipeline_digits_then_byte_level() {
let json = serde_json::json!({
"type": "Sequence",
"pretokenizers": [
{"type": "Digits", "individual_digits": true},
{"type": "ByteLevel", "add_prefix_space": false, "use_regex": true}
]
});
let pt = parse(Some(&json)).expect("pipeline");
assert!(pt.byte_level);
let pieces = pt.split("a12");
assert_eq!(pieces, vec!["a", "1", "2"]);
}
#[test]
fn split_invert_keeps_content_between_matches() {
let re = Box::new(gpt2_regex());
let mut out = Vec::new();
super::split_regex("ab", &re, Behavior::Isolated, true, &mut out);
assert_eq!(out.concat(), "ab");
}
}