use regexr::hir::translate;
use regexr::parser::parse;
use regexr::{Regex, RegexBuilder};
fn builds(pattern: &str) -> Vec<(&'static str, Regex)> {
vec![
(
"jit",
RegexBuilder::new(pattern)
.jit(true)
.build()
.expect("pattern should compile"),
),
(
"interp",
RegexBuilder::new(pattern)
.jit(false)
.build()
.expect("pattern should compile"),
),
]
}
fn ranges(re: &Regex, text: &str) -> Vec<(usize, usize)> {
re.find_iter(text).map(|m| (m.start(), m.end())).collect()
}
fn capture_ranges(re: &Regex, text: &str) -> Vec<(usize, usize)> {
re.captures_iter(text)
.filter_map(|c| c.get(0).map(|m| (m.start(), m.end())))
.collect()
}
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) -> Vec<(usize, usize)> {
let hir = parse(pattern)
.and_then(|ast| translate(&ast))
.expect("pattern should compile");
let ncaps = hir.props.capture_count as usize;
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut last_end = 0usize;
while last_end <= bytes.len() {
let (start, end) = match regexr::reference::find_from(&hir.expr, ncaps, bytes, last_end) {
Some(m) => m,
None => break,
};
out.push((start, end));
last_end = if start == end {
ceil_char_boundary(text, end + 1)
} else {
ceil_char_boundary(text, end)
};
}
out
}
fn replace_spans(text: &str, spans: &[(usize, usize)], rep: &str) -> String {
let bytes = text.as_bytes();
let mut out: Vec<u8> = Vec::new();
let mut last_end = 0;
for &(start, end) in spans {
out.extend_from_slice(&bytes[last_end..start]);
out.extend_from_slice(rep.as_bytes());
last_end = end;
}
out.extend_from_slice(&bytes[last_end..]);
String::from_utf8_lossy(&out).into_owned()
}
#[test]
fn start_anchor_matches_only_at_text_start() {
for (label, re) in builds(r"^a") {
assert_eq!(ranges(&re, "aa"), vec![(0, 1)], "{label}");
assert_eq!(capture_ranges(&re, "aa"), vec![(0, 1)], "{label}");
assert_eq!(re.replace_all("aa", "X"), "Xa", "{label}");
}
}
#[test]
fn start_anchor_word_run_matches_once() {
for (label, re) in builds(r"^\w+") {
assert_eq!(ranges(&re, "abc abc"), vec![(0, 3)], "{label}");
}
}
#[test]
fn start_anchor_with_complete_literal_prefilter() {
for (label, re) in builds(r"^[ab]") {
assert_eq!(ranges(&re, "ab"), vec![(0, 1)], "{label}");
assert_eq!(ranges(&re, "ba"), vec![(0, 1)], "{label}");
assert!(!re.is_match("cab"), "{label}");
}
for (label, re) in builds(r"[ab]") {
assert_eq!(ranges(&re, "ab"), vec![(0, 1), (1, 2)], "{label}");
}
}
#[test]
fn text_start_anchor_matches_only_once() {
for (label, re) in builds(r"\Aab") {
assert_eq!(ranges(&re, "abab"), vec![(0, 2)], "{label}");
}
}
#[test]
fn multiline_start_anchor_matches_every_line() {
for (label, re) in builds(r"(?m)^a") {
assert_eq!(
ranges(&re, "a\na\na"),
vec![(0, 1), (2, 3), (4, 5)],
"{label}"
);
}
}
#[test]
fn end_anchor_matches_only_at_text_end() {
for (label, re) in builds(r"a$") {
assert_eq!(ranges(&re, "aaa"), vec![(2, 3)], "{label}");
}
for (label, re) in builds(r"\w+$") {
assert_eq!(ranges(&re, "ab cd"), vec![(3, 5)], "{label}");
}
}
#[test]
fn strict_end_anchor_matches_only_at_text_end() {
for (label, re) in builds(r"a\z") {
assert_eq!(ranges(&re, "aaa"), vec![(2, 3)], "{label}");
}
}
#[test]
fn word_boundary_across_resume_boundary() {
for (label, re) in builds(r"\bfoo") {
assert_eq!(ranges(&re, "foofoo"), vec![(0, 3)], "{label}");
assert_eq!(re.replace_all("foofoo", "X"), "Xfoo", "{label}");
}
}
#[test]
fn not_word_boundary_across_resume_boundary() {
for (label, re) in builds(r"\Bo") {
assert_eq!(ranges(&re, "foo"), vec![(1, 2), (2, 3)], "{label}");
}
}
#[test]
fn word_boundary_pairs_still_found() {
for (label, re) in builds(r"\bab\b") {
assert_eq!(
ranges(&re, "ab ab ab"),
vec![(0, 2), (3, 5), (6, 8)],
"{label}"
);
}
}
#[test]
fn positive_lookbehind_across_resume_boundary() {
for (label, re) in builds(r"(?<=a)b") {
assert_eq!(ranges(&re, "abb"), vec![(1, 2)], "{label}");
assert_eq!(ranges(&re, "abab"), vec![(1, 2), (3, 4)], "{label}");
}
}
#[test]
fn negative_lookbehind_across_resume_boundary() {
for (label, re) in builds(r"(?<!a)b") {
assert_eq!(ranges(&re, "abb"), vec![(2, 3)], "{label}");
}
}
#[test]
fn plain_ascii_iteration_unchanged() {
for (label, re) in builds(r"\w+") {
assert_eq!(
ranges(&re, "ab cd ef"),
vec![(0, 2), (3, 5), (7, 9)],
"{label}"
);
assert_eq!(re.replace_all("ab cd ef", "X"), "X X X", "{label}");
}
}
#[test]
fn empty_matches_still_make_progress() {
for (label, re) in builds(r"a*") {
assert_eq!(ranges(&re, "aab"), vec![(0, 2), (2, 2), (3, 3)], "{label}");
}
}
#[test]
fn multibyte_resume_stays_on_codepoint_boundaries() {
let text = "aé世🎉";
for (label, re) in builds(r".") {
for (start, _) in ranges(&re, text) {
assert!(
text.is_char_boundary(start),
"match started inside a codepoint ({label}): {start}"
);
}
}
}
#[test]
fn lookbehind_over_multibyte_left_context() {
for (label, re) in builds(r"(?<=é)x") {
assert_eq!(ranges(&re, "éxéx"), vec![(2, 3), (5, 6)], "{label}");
}
}
#[test]
fn iteration_agrees_with_reference() {
const CASES: &[(&str, &str)] = &[
(r"^a", "aa"),
(r"^[ab]", "ab"),
(r"[ab]", "abcab"),
(r"^\w+", "abc abc"),
(r"\Aab", "abab"),
(r"(?m)^a", "a\na\na"),
(r"a$", "aaa"),
(r"\w+$", "ab cd"),
(r"\bfoo", "foofoo"),
(r"\bab\b", "ab ab ab"),
(r"\Bo", "foo"),
(r"(?<=a)b", "abab"),
(r"(?<!a)b", "abb"),
(r"(?<=a)(b)", "abab"),
(r"(\w)\1", "aabb"),
(r"\w+", "ab cd ef"),
(r"\w+", "héllo wörld"),
(r"[a-z]+", "one two three"),
(r" ?[^\s]+", "hello world"),
(r"\s+", "a b\n\nc"),
(r"\p{L}+", "中文 test"),
];
let mut failures = Vec::new();
for &(pattern, text) in CASES {
let expected = reference_ranges(pattern, text);
let expected_replacement = replace_spans(text, &expected, "X");
for (label, re) in builds(pattern) {
let case = format!("[{label}] {pattern:?} on {text:?}");
let found = ranges(&re, text);
if found != expected {
failures.push(format!("find_iter {case}: ref={expected:?} got={found:?}"));
}
let captured = capture_ranges(&re, text);
if captured != expected {
failures.push(format!(
"captures_iter {case}: ref={expected:?} got={captured:?}"
));
}
let replaced = re.replace_all(text, "X").into_owned();
if replaced != expected_replacement {
failures.push(format!(
"replace_all {case}: ref={expected_replacement:?} got={replaced:?}"
));
}
}
}
assert!(
failures.is_empty(),
"iteration/reference divergences ({}):\n{}",
failures.len(),
failures.join("\n")
);
}