use regexr::hir::translate;
use regexr::parser::parse;
use regexr::RegexBuilder;
fn ceil_char_boundary(text: &str, i: usize) -> usize {
let mut j = i;
while j < text.len() && !text.is_char_boundary(j) {
j += 1;
}
j
}
fn reference_ranges(pattern: &str, text: &str) -> Option<Vec<(usize, usize)>> {
let hir = parse(pattern).and_then(|ast| translate(&ast)).ok()?;
let ncaps = hir.props.capture_count as usize;
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut last_end = 0usize;
let mut skip_empty_at: Option<usize> = None;
while last_end <= bytes.len() {
let Some((start, end)) = regexr::reference::find_from(&hir.expr, ncaps, bytes, last_end)
else {
break;
};
let empty = start == end;
last_end = if empty {
ceil_char_boundary(text, end + 1)
} else {
ceil_char_boundary(text, end)
};
if empty && skip_empty_at == Some(start) {
skip_empty_at = None;
continue;
}
skip_empty_at = (!empty).then_some(end);
out.push((start, end));
if out.len() > 64 {
break;
}
}
Some(out)
}
const FRAGMENTS: &[&str] = &[
"a",
"ab",
"[ab]",
"a+",
"a*",
"a?",
"a{2}",
"a{1,2}",
".",
"\\w",
"\\w+",
"\\W",
"\\D",
"\\s*",
"(a)",
"(a)(b)",
"(a|b)",
"a|b",
"(a)\\1",
"(?:ab)+",
"a+?",
"[^a]",
"(?=a)a",
"(?!b)a",
"(?<=a)b",
"(?<!a)b",
"(a*)*",
"(a?)*",
"()+",
"(|a)*",
"(a|)*",
"(a*)+",
"((a)*)*",
"(a{0,2})*",
"(a*)*b",
"(a*?)*",
"a(?i)b",
"(?i)a(?-i)b",
"(?i:a)b",
"(?i)a|b",
"(?x) a b",
"(?x)a#c\nb",
"(?x)[a b]",
"\\ a",
"\\@|a",
"[\\e\\a]",
"\\X",
"\\X+",
"a\\X",
"\\Xb",
"(\\X)",
"\\Qa.b\\E",
"\\Qa\\Eb",
"x\\Q.\\Ey",
];
const PREFIXES: &[&str] = &["", "^", "\\A", "(?m)^", "\\b", "\\B", "(?i)"];
const SUFFIXES: &[&str] = &["", "$", "\\z", "\\b", "\\B", "(?m)$"];
const TEXTS: &[&str] = &[
"", "a", "aa", "ab", "ba", "abab", "aabb", "a b", " ab ", "a\na", "a\nb\na", "cab", "xaby",
"aaa\n", "b", "bb", "abc abc", "héllo", "中a中", "AaBb", "\n", " ", "abababab", "a@b",
"a\u{1b}b", "a\u{07}b", "a#c\nb",
];
#[test]
fn resumed_iteration_matches_reference() {
let mut patterns: Vec<String> = Vec::new();
for p in PREFIXES {
for f in FRAGMENTS {
for s in SUFFIXES {
patterns.push(format!("{p}{f}{s}"));
}
}
}
for p in [
"^a|b", "a|^b", "(?m)^a|b", "\\ba|b", "^a$|b", "^a|b$", "\\Aa|b",
"a|b$", "a$|b", "a|b\\b", "a\\b|b",
] {
patterns.push(p.to_string());
}
let mut failures = Vec::new();
for pattern in &patterns {
for text in TEXTS {
let Some(expected) = reference_ranges(pattern, text) else {
continue;
};
for (label, jit) in [("jit", true), ("interp", false)] {
let Ok(re) = RegexBuilder::new(pattern).jit(jit).build() else {
continue;
};
let got: Vec<(usize, usize)> =
re.find_iter(text).map(|m| (m.start(), m.end())).collect();
if got != expected {
let kind = if got.first() == expected.first() {
"RESUME"
} else {
"FIND "
};
failures.push(format!(
"{kind} [{label}] {pattern:?} on {text:?}: ref={expected:?} got={got:?}"
));
}
let caps: Vec<(usize, usize)> = re
.captures_iter(text)
.filter_map(|c| c.get(0).map(|m| (m.start(), m.end())))
.collect();
if caps != expected {
failures.push(format!(
"[{label}] captures {pattern:?} on {text:?}: ref={expected:?} got={caps:?}"
));
}
}
}
}
assert!(
failures.is_empty(),
"{} divergences (of {} patterns):\n{}",
failures.len(),
patterns.len(),
failures.join("\n")
);
}
#[test]
fn capture_groups_match_reference() {
let mut patterns: Vec<String> = Vec::new();
for p in PREFIXES {
for f in FRAGMENTS {
for s in SUFFIXES {
patterns.push(format!("{p}{f}{s}"));
}
}
}
let mut failures = Vec::new();
for pattern in &patterns {
let Ok(hir) = parse(pattern).and_then(|ast| translate(&ast)) else {
continue;
};
let ncaps = hir.props.capture_count as usize;
if ncaps == 0 {
continue;
}
for text in TEXTS {
let expected =
regexr::reference::captures(&hir.expr, ncaps, text.as_bytes()).map(|caps| {
caps.into_iter().collect::<Vec<_>>()
});
for (label, jit) in [("jit", true), ("interp", false)] {
let Ok(re) = RegexBuilder::new(pattern).jit(jit).build() else {
continue;
};
let got = re.captures(text).map(|caps| {
(0..caps.len())
.map(|i| caps.get(i).map(|m| (m.start(), m.end())))
.collect::<Vec<_>>()
});
if got != expected {
failures.push(format!(
"[{label}] {pattern:?} on {text:?}: ref={expected:?} got={got:?}"
));
}
}
}
}
assert!(
failures.is_empty(),
"{} divergences (of {} patterns):\n{}",
failures.len(),
patterns.len(),
failures.join("\n")
);
}
#[test]
fn first_iteration_match_equals_find() {
let mut failures = Vec::new();
for p in PREFIXES {
for f in FRAGMENTS {
for s in SUFFIXES {
let pattern = format!("{p}{f}{s}");
for text in TEXTS {
for (label, jit) in [("jit", true), ("interp", false)] {
let Ok(re) = RegexBuilder::new(&pattern).jit(jit).build() else {
continue;
};
let single = re.find(text).map(|m| (m.start(), m.end()));
let first = re.find_iter(text).next().map(|m| (m.start(), m.end()));
if single != first {
failures.push(format!(
"[{label}] {pattern:?} on {text:?}: find={single:?} iter.next={first:?}"
));
}
}
}
}
}
}
assert!(
failures.is_empty(),
"{} divergences:\n{}",
failures.len(),
failures.join("\n")
);
}