#[test]
fn leftmost_match_wins_regardless_of_step_size() {
let re = regexr::Regex::new(r"\p{L}\s\S").unwrap();
let haystack = "x\r\u{4e2d}\te";
let found = re.find(haystack).map(|m| (m.start(), m.end()));
assert_eq!(found, Some((0, 5)), "leftmost match must win");
let re = regexr::Regex::new(r"\p{L}\s\S").unwrap();
assert_eq!(
re.find("x\r\u{4e2d}\t").map(|m| (m.start(), m.end())),
Some((0, 5))
);
}
#[test]
fn quantified_alternation_keeps_its_loop() {
for pattern in [r"\s+", r"\s+\s+", r"(?:a|b)+"] {
let interpreted = regexr::Regex::new(pattern).unwrap();
let jitted = regexr::RegexBuilder::new(pattern)
.jit(true)
.build()
.unwrap();
for haystack in [" \n", "aab", " ", "ab"] {
assert_eq!(
interpreted.find(haystack).map(|m| (m.start(), m.end())),
jitted.find(haystack).map(|m| (m.start(), m.end())),
"pattern {pattern:?} on {haystack:?}: JIT and interpreter disagree"
);
}
}
}
#[test]
fn small_unicode_classes_match_whole_characters() {
let re = regexr::Regex::new(r"^[^\s<>]+$").unwrap();
assert!(re.is_match("https://example.com/\u{4e2d}"));
assert!(!re.is_match("has space"));
assert!(!re.is_match("a\u{a0}b"), "U+00A0 is Unicode whitespace");
let re = regexr::Regex::new(r"\s+").unwrap();
let haystack = "\u{4e2d}\u{2003}\u{3000}\u{4e2d}";
let m = re.find(haystack).expect("should match the separators");
assert_eq!(m.as_str(), "\u{2003}\u{3000}");
}
#[test]
fn requesting_jit_never_downgrades_the_engine() {
const BACKREF_PATTERNS: &[&str] = &[r#"(['"])[^'"]*\1"#, r"(\w+)\s+\1", r"(a)\1"];
for pattern in BACKREF_PATTERNS {
let jitted = regexr::RegexBuilder::new(pattern)
.jit(true)
.build()
.unwrap();
assert_ne!(
jitted.engine_name(),
"PikeVm",
"{pattern}: backreferences must reach a backtracking engine"
);
}
const RUN_PATTERNS: &[&str] = &[r"\w+", r"\d+", r"[a-z]+", r"[0-9a-f]{2,}", r"\w{2,8}"];
const ALTERNATION_PATTERNS: &[&str] = &[
r#"[a-zA-Z_][a-zA-Z0-9_]*|[0-9]+(?:\.[0-9]+)?|[+\-*/=<>!&|^%]+|[(){}\[\];,.]|"[^"]*"|'[^']*'"#,
r"[^>]+",
r"<[^>]+>",
r"https?://[^\s<>]+",
r"error|warning|critical|fatal",
];
const BOUNDARY_PATTERNS: &[&str] = &[r"\bthe\b", r"\bword\b", r"\bfoo\b\s"];
let selection: &[(&str, &[&str])] = &[
("a repeated byte class", RUN_PATTERNS),
("an alternation", ALTERNATION_PATTERNS),
("a literal guarded by a word boundary", BOUNDARY_PATTERNS),
];
for (shape, patterns) in selection {
for pattern in *patterns {
let plain = regexr::Regex::new(pattern).unwrap();
let jitted = regexr::RegexBuilder::new(pattern)
.jit(true)
.build()
.unwrap();
assert_eq!(
plain.engine_name(),
jitted.engine_name(),
"{pattern}: {shape} must reach the same engine either way"
);
}
}
const PATTERNS: &[&str] = &[
r#"(['"])[^'"]*\1"#,
r"(\w+)\s+\1",
r"\s+",
r"\S+",
r"[^\s<>]+",
r"\w+(?=ing\b)",
r"(cat|dog)+",
r"\bword\b",
];
const HAYSTACKS: &[&str] = &[
r#"let x = "hello"; y = 'z';"#,
"the the word word",
"running and singing",
"cat dog catdog",
" \t\u{a0} ",
"https://example.com/a<b>",
"",
];
for pattern in PATTERNS {
let plain = regexr::Regex::new(pattern).unwrap();
let jitted = regexr::RegexBuilder::new(pattern)
.jit(true)
.build()
.unwrap();
for haystack in HAYSTACKS {
let a: Vec<_> = plain
.find_iter(haystack)
.map(|m| (m.start(), m.end()))
.collect();
let b: Vec<_> = jitted
.find_iter(haystack)
.map(|m| (m.start(), m.end()))
.collect();
assert_eq!(
a,
b,
"{pattern:?} on {haystack:?}: plain ({}) and jit(true) ({}) disagree",
plain.engine_name(),
jitted.engine_name()
);
}
}
}
#[test]
fn codepoint_class_patterns_select_the_tagged_nfa() {
const CODEPOINT_CLASS_PATTERNS: &[&str] = &[
r"\p{L}+",
r"\p{N}{1,3}",
r"\P{L}+",
r"[^\r\n\p{L}\p{N}]?\p{L}+",
];
for pattern in CODEPOINT_CLASS_PATTERNS {
let re = regexr::Regex::new(pattern).unwrap();
assert_eq!(
re.engine_name(),
"TaggedNfa",
"{pattern}: a codepoint class belongs on the tagged NFA, not the PikeVM"
);
}
let re = regexr::Regex::new(r"\p{L}+").unwrap();
let spans: Vec<_> = re
.find_iter("ab \u{4e2d}\u{6587}1 \u{3b1}")
.map(|m| (m.start(), m.end()))
.collect();
assert_eq!(spans, vec![(0, 2), (3, 9), (11, 13)]);
}
#[test]
fn codepoint_class_pattern_declining_extraction_falls_back_to_the_pikevm() {
use regexr::hir::translate;
use regexr::nfa::tagged::StepExtractor;
use regexr::parser::parse;
let groups = ["(?:ab|cd)", "(?:ef|gh)", "(?:ij|kl)", "(?:mn|op)"];
let mut pattern = String::from(r"\p{L}");
let mut haystack = String::from("x");
for i in 0..24 {
let group = groups[i % groups.len()];
pattern.push_str(group);
haystack.push_str(&group[3..5]);
}
let hir = parse(&pattern)
.and_then(|ast| translate(&ast))
.expect("pattern should compile");
let nfa = regexr::nfa::compile(&hir).expect("NFA should build");
assert!(
StepExtractor::new(&nfa).extract().is_none(),
"the fallback under test is only exercised while this shape declines extraction"
);
let re = regexr::Regex::new(&pattern).expect("pattern should compile");
assert_eq!(
re.engine_name(),
"TaggedNfa",
"declining extraction is the tagged engine's internal fallback, not a different selection"
);
assert_eq!(
re.find(&haystack).map(|m| (m.start(), m.end())),
Some((0, haystack.len())),
"the PikeVM fallback must still find the match"
);
assert!(!re.is_match("x ab cd ef gh"));
}
#[test]
fn byte_class_star_outranks_a_codepoint_class_it_could_consume() {
let re = regexr::Regex::new(r"[^\s]*\p{N}").unwrap();
assert_eq!(re.find("\u{BD}\u{BD}").map(|m| m.end()), Some(4));
assert_eq!(re.find("\u{BD}\u{BD}\u{BD}").map(|m| m.end()), Some(6));
assert_eq!(re.find("\u{BD}1").map(|m| m.end()), Some(3));
assert_eq!(re.find("12").map(|m| m.end()), Some(2));
}
#[test]
fn byte_class_star_crossing_three_byte_codepoints() {
let re = regexr::Regex::new(r"[^\s]*\p{L}").unwrap();
assert_eq!(re.find("\u{E9}\u{E9}").map(|m| m.end()), Some(4));
assert_eq!(re.find("\u{4E2D}\u{4E2D}").map(|m| m.end()), Some(6));
}
#[test]
fn plus_quantifier_before_a_codepoint_class_is_unaffected() {
let re = regexr::Regex::new(r"[^\s]+\p{N}").unwrap();
assert_eq!(re.find("\u{BD}\u{BD}").map(|m| m.end()), Some(4));
}
#[test]
fn nullable_repetition_matches_agree_with_the_reference() {
use regexr::hir::translate;
use regexr::parser::parse;
let pattern = "(?:a?){20}";
let hir = parse(pattern)
.and_then(|ast| translate(&ast))
.expect("pattern should compile");
let ncaps = hir.props.capture_count as usize;
let re = regexr::Regex::new(pattern).expect("pattern should compile");
let haystacks = [
String::new(),
"a".to_string(),
"aaaa".to_string(),
"a".repeat(20),
"b".to_string(),
"aaaabaaaa".to_string(),
];
for haystack in &haystacks {
let bytes = haystack.as_bytes();
let expected_find = regexr::reference::find(&hir.expr, ncaps, bytes);
let expected_captures = regexr::reference::captures(&hir.expr, ncaps, bytes);
let found = re.find(haystack).map(|m| (m.start(), m.end()));
assert_eq!(
found, expected_find,
"{pattern:?} on {haystack:?}: find() disagrees with reference"
);
let captured = re
.captures(haystack)
.and_then(|caps| caps.get(0))
.map(|m| (m.start(), m.end()));
let expected_group0 = expected_captures.and_then(|caps| caps[0]);
assert_eq!(
captured, expected_group0,
"{pattern:?} on {haystack:?}: captures() disagrees with reference"
);
}
}
#[test]
fn nullable_repetition_matches_agree_with_the_reference_past_the_closure_budget() {
std::thread::Builder::new()
.stack_size(16 * 1024 * 1024)
.spawn(nullable_repetition_agreement_past_the_closure_budget)
.expect("spawn deep-recursion thread")
.join()
.expect("reference comparison panicked");
}
fn nullable_repetition_agreement_past_the_closure_budget() {
use regexr::hir::translate;
use regexr::parser::parse;
let pattern = "(?:a?){500}";
let hir = parse(pattern)
.and_then(|ast| translate(&ast))
.expect("pattern should compile");
let ncaps = hir.props.capture_count as usize;
let re = regexr::Regex::new(pattern).expect("pattern should compile");
let haystacks = [
String::new(),
"a".to_string(),
"aaaa".to_string(),
"a".repeat(500),
"b".to_string(),
"aaaabaaaa".to_string(),
];
for haystack in &haystacks {
let bytes = haystack.as_bytes();
let expected_find = regexr::reference::find(&hir.expr, ncaps, bytes);
let expected_captures = regexr::reference::captures(&hir.expr, ncaps, bytes);
let found = re.find(haystack).map(|m| (m.start(), m.end()));
assert_eq!(
found, expected_find,
"{pattern:?} on {haystack:?}: find() disagrees with reference"
);
let captured = re
.captures(haystack)
.and_then(|caps| caps.get(0))
.map(|m| (m.start(), m.end()));
let expected_group0 = expected_captures.and_then(|caps| caps[0]);
assert_eq!(
captured, expected_group0,
"{pattern:?} on {haystack:?}: captures() disagrees with reference"
);
}
}