use regexr::hir::translate;
use regexr::nfa::tagged::{PatternStep, StepExtractor, TaggedNfa};
use regexr::parser::parse;
const PATTERNS: &[&str] = &[
r"\w*",
r"\w*a",
r"a\w*",
r"[a-z]*x",
r"\w*$",
r"\w*\b",
r"\w*(?=ing)",
r"\w*(?!ing)",
r"[a-z]*(?=ing)",
r"\w*\d*a",
r"\w+\w*a",
r"(?:ab)*x",
r"\w*?a",
r"\w*?(?=ing)",
r"(?:a+|b)",
r"(?:a+|b)c",
r"(?:\w+|x)(?=ing)",
r"\w?(?=ing)",
r"\p{L}*(?=ing)",
r"\p{L}*a",
];
const HAYSTACKS: &[&str] = &[
"",
"a",
"aa",
"aaa",
"b",
"bc",
"aac",
"abc",
"ab cd",
" ",
"a1a",
"bca",
"ing",
"inging",
"singing",
"sing ing",
"singing ringing",
"xyz",
"héllo x",
"naïveing",
"中文 abc",
"abcded",
];
fn ceil_boundary(text: &str, at: usize) -> usize {
let mut at = at;
while at < text.len() && !text.is_char_boundary(at) {
at += 1;
}
at.min(text.len() + 1)
}
fn reference_spans(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 spans = 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_boundary(text, end + 1)
} else {
ceil_boundary(text, end)
};
if empty && skip_empty_at == Some(start) {
skip_empty_at = None;
continue;
}
skip_empty_at = (!empty).then_some(end);
spans.push((start, end));
}
spans
}
fn engine_spans(regex: ®exr::Regex, text: &str) -> Vec<(usize, usize)> {
regex
.find_iter(text)
.map(|m| (m.start(), m.end()))
.collect()
}
fn extract(pattern: &str) -> Option<Vec<PatternStep>> {
let hir = parse(pattern)
.and_then(|ast| translate(&ast))
.expect("pattern should compile");
let nfa = regexr::nfa::compile(&hir).expect("NFA should compile");
StepExtractor::new(&nfa).extract()
}
#[test]
fn star_patterns_iterate_like_the_reference() {
let mut failures = Vec::new();
for &pattern in PATTERNS {
let jit = regexr::RegexBuilder::new(pattern)
.jit(true)
.build()
.unwrap();
let interp = regexr::RegexBuilder::new(pattern)
.jit(false)
.build()
.unwrap();
for &text in HAYSTACKS {
let expected = reference_spans(pattern, text);
for (label, regex) in [("jit", &jit), ("interp", &interp)] {
let got = engine_spans(regex, text);
if got != expected {
failures.push(format!(
"{pattern:?} on {text:?} ({label}): got {got:?}, reference {expected:?}"
));
}
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn greedy_and_non_greedy_stars_report_different_spans() {
type Case = (&'static str, &'static str, &'static [(usize, usize)]);
let cases: &[Case] = &[
(r"\w*a", "aa", &[(0, 2)]),
(r"\w*?a", "aa", &[(0, 1), (1, 2)]),
(r"\w*", "ab cd", &[(0, 2), (3, 5)]),
(r"\w*", "", &[(0, 0)]),
(r"\w*a", "", &[]),
(r"\w*(?=ing)", "singing", &[(0, 4)]),
(r"\w*(?=ing)", "ing", &[(0, 0)]),
(r"\w*(?=ing)", "xyz", &[]),
(r"\w*(?!ing)", "singing", &[(0, 7)]),
(r"[a-z]*(?=ing)", "naïveing", &[(4, 6)]),
];
for &(pattern, text, expected) in cases {
for jit in [true, false] {
let regex = regexr::RegexBuilder::new(pattern).jit(jit).build().unwrap();
assert_eq!(
engine_spans(®ex, text),
expected,
"{pattern:?} on {text:?} (jit={jit})"
);
}
}
}
#[test]
fn a_nullable_run_beside_a_lookahead_reaches_the_combined_step() {
for pattern in [r"\w*(?=ing)", r"[a-z]*(?=xy)", r"a\w*(?!ing)"] {
let steps =
extract(pattern).unwrap_or_else(|| panic!("{pattern:?} extracted no step program"));
assert!(
steps
.iter()
.any(|s| matches!(s, PatternStep::GreedyStarLookahead(_, _, _))),
"{pattern:?} extracted no combined greedy-star+lookahead step: {steps:?}"
);
}
}
#[test]
fn a_genuine_alternation_is_not_compiled_as_a_star() {
for pattern in [r"(?:a+|b)", r"(?:a+|b)c"] {
let steps = extract(pattern).unwrap_or_else(|| panic!("{pattern:?} no longer extracts"));
assert!(
!steps
.iter()
.any(|s| matches!(s, PatternStep::GreedyStar(_))),
"{pattern:?} was compiled as a star: {steps:?}"
);
let hir = parse(pattern)
.and_then(|ast| translate(&ast))
.expect("pattern should compile");
let ncaps = hir.props.capture_count as usize;
for &text in HAYSTACKS {
let bytes = text.as_bytes();
assert_eq!(
TaggedNfa::find(&steps, bytes),
regexr::reference::find(&hir.expr, ncaps, bytes),
"{pattern:?} on {text:?}: step program disagrees with the reference"
);
}
}
assert!(
extract(r"(?:\w+|x)(?=ing)").is_none(),
"an alternation was recognised as a star: the exit-convergence check is \
too loose"
);
}