mod engine;
pub mod interpreter;
pub(crate) mod shared;
pub use engine::LazyDfaEngine;
pub use interpreter::LazyDfa;
pub(crate) use shared::SCAN_BUDGET_FACTOR;
pub use shared::{CacheCeilingExceeded, CharClass, DfaStateId, PositionContext};
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::translate;
use crate::nfa::compile;
use crate::parser::parse;
fn make_dfa(pattern: &str) -> LazyDfa {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
let nfa = compile(&hir).unwrap();
LazyDfa::new(nfa)
}
fn make_engine(pattern: &str) -> LazyDfaEngine {
let ast = parse(pattern).unwrap();
let hir = translate(&ast).unwrap();
let nfa = compile(&hir).unwrap();
LazyDfaEngine::new(nfa)
}
fn reference_find(pattern: &str, input: &[u8]) -> Option<(usize, usize)> {
let hir = parse(pattern).and_then(|ast| translate(&ast)).unwrap();
crate::reference::find(&hir.expr, hir.props.capture_count as usize, input)
}
#[test]
fn test_simple_match() {
let mut dfa = make_dfa("abc");
assert_eq!(dfa.is_match_bytes(b"abc"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"ab"), Ok(false));
assert_eq!(dfa.is_match_bytes(b"abcd"), Ok(false));
}
#[test]
fn test_alternation() {
let mut dfa = make_dfa("a|b");
assert_eq!(dfa.is_match_bytes(b"a"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"b"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"c"), Ok(false));
}
#[test]
fn test_repetition() {
let mut dfa = make_dfa("a*");
assert_eq!(dfa.is_match_bytes(b""), Ok(true));
assert_eq!(dfa.is_match_bytes(b"a"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"aaa"), Ok(true));
}
#[test]
fn test_find() {
let mut dfa = make_dfa("abc");
assert_eq!(dfa.find(b"xyzabc123"), Ok(Some((3, 6))));
assert_eq!(dfa.find(b"abc"), Ok(Some((0, 3))));
assert_eq!(dfa.find(b"xyz"), Ok(None));
}
#[test]
fn test_class() {
let mut dfa = make_dfa("[a-z]+");
assert_eq!(dfa.is_match_bytes(b"hello"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"HELLO"), Ok(false));
assert_eq!(dfa.is_match_bytes(b""), Ok(false));
}
#[test]
fn test_cache_flush() {
let mut dfa = make_dfa("a|b|c|d|e|f|g|h");
dfa.set_cache_limit(3);
let initial_count = dfa.state_count();
assert!(initial_count >= 1);
for _ in 0..10 {
let _ = dfa.find(b"abcdefgh");
let _ = dfa.find(b"xyzabcxyz");
}
let flush_count = dfa.flush_count();
assert_eq!(dfa.is_match_bytes(b"a"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"h"), Ok(true));
assert_eq!(dfa.is_match_bytes(b"z"), Ok(false));
let _ = flush_count;
}
#[test]
fn flush_mid_search_does_not_truncate_the_match() {
let mut dfa = make_dfa("abcd");
dfa.set_cache_limit(3);
assert_eq!(dfa.find_from(b"abcd", 0), Ok(Some((0, 4))));
assert_eq!(reference_find("abcd", b"abcd"), Some((0, 4)));
}
#[test]
fn cache_ceiling_gives_up_rather_than_answering_wrongly() {
let mut dfa = make_dfa("abcdef");
dfa.set_cache_limit(1);
assert_eq!(dfa.find(b"abcdef"), Err(CacheCeilingExceeded));
assert_eq!(reference_find("abcdef", b"abcdef"), Some((0, 6)));
}
#[test]
fn deferred_flush_runs_once_the_search_ends() {
let mut dfa = make_dfa("abcd");
dfa.set_cache_limit(3);
for _ in 0..5 {
assert_eq!(dfa.find(b"abcd"), Ok(Some((0, 4))));
assert_eq!(
dfa.state_count(),
1,
"the deferred flush should have reset the cache to the start state"
);
}
assert!(
dfa.flush_count() >= 5,
"each search should have flushed once"
);
}
#[test]
fn test_word_boundary_basic() {
let mut dfa = make_dfa(r"\bthe\b");
assert!(dfa.has_word_boundary(), "DFA should detect word boundary");
assert_eq!(
dfa.find(b"the cat"),
Ok(Some((0, 3))),
"Should match 'the' at start"
);
assert_eq!(
dfa.find(b"see the cat"),
Ok(Some((4, 7))),
"Should match 'the' in middle"
);
assert_eq!(dfa.find(b"there"), Ok(None), "Should not match 'there'");
assert_eq!(dfa.find(b"other"), Ok(None), "Should not match 'other'");
assert_eq!(dfa.find(b"bathe"), Ok(None), "Should not match 'bathe'");
}
#[test]
fn test_word_boundary_no_partial() {
let mut dfa = make_dfa(r"\bword\b");
assert_eq!(dfa.find(b"word"), Ok(Some((0, 4))));
assert_eq!(dfa.find(b"a word here"), Ok(Some((2, 6))));
assert_eq!(dfa.find(b"keyword"), Ok(None));
assert_eq!(dfa.find(b"wording"), Ok(None));
assert_eq!(dfa.find(b"swordfish"), Ok(None));
}
#[test]
fn test_not_word_boundary() {
let mut dfa = make_dfa(r"a\Bb");
assert_eq!(dfa.find(b"ab"), Ok(Some((0, 2))));
assert_eq!(dfa.find(b"cab"), Ok(Some((1, 3))));
assert_eq!(dfa.find(b"cabin"), Ok(Some((1, 3))));
let mut dfa2 = make_dfa(r"x\By");
assert_eq!(dfa2.find(b"x y"), Ok(None));
assert_eq!(dfa2.find(b"xy"), Ok(Some((0, 2))));
}
#[test]
fn test_start_of_text_anchor() {
let mut dfa = make_dfa("^hello");
assert!(dfa.has_anchors(), "DFA should detect anchors");
assert!(dfa.has_start_anchor(), "DFA should detect start anchor");
assert_eq!(
dfa.find(b"hello world"),
Ok(Some((0, 5))),
"Should match at start"
);
assert_eq!(dfa.find(b"hello"), Ok(Some((0, 5))), "Should match exact");
assert_eq!(
dfa.find(b"say hello"),
Ok(None),
"Should not match in middle"
);
assert_eq!(
dfa.find(b" hello"),
Ok(None),
"Should not match after spaces"
);
}
#[test]
fn test_end_of_text_anchor() {
let mut dfa = make_dfa("world$");
assert!(dfa.has_anchors(), "DFA should detect anchors");
assert!(dfa.has_end_anchor(), "DFA should detect end anchor");
assert_eq!(
dfa.find(b"hello world"),
Ok(Some((6, 11))),
"Should match at end"
);
assert_eq!(dfa.find(b"world"), Ok(Some((0, 5))), "Should match exact");
assert_eq!(
dfa.find(b"world hello"),
Ok(None),
"Should not match at start"
);
assert_eq!(
dfa.find(b"world "),
Ok(None),
"Should not match before space"
);
}
#[test]
fn test_both_anchors() {
let mut dfa = make_dfa("^hello$");
assert!(dfa.has_start_anchor() && dfa.has_end_anchor());
assert_eq!(dfa.find(b"hello"), Ok(Some((0, 5))), "Should match exact");
assert_eq!(
dfa.find(b"hello world"),
Ok(None),
"Should not match with suffix"
);
assert_eq!(
dfa.find(b"say hello"),
Ok(None),
"Should not match with prefix"
);
assert_eq!(dfa.find(b" hello "), Ok(None), "Should not match with both");
}
#[test]
fn test_anchor_with_pattern() {
let mut dfa = make_dfa("^[a-z]+$");
assert_eq!(dfa.find(b"hello"), Ok(Some((0, 5))));
assert_eq!(dfa.find(b"world"), Ok(Some((0, 5))));
assert_eq!(dfa.find(b"abc"), Ok(Some((0, 3))));
assert_eq!(dfa.find(b"hello world"), Ok(None));
assert_eq!(dfa.find(b"123abc"), Ok(None));
assert_eq!(dfa.find(b"abc123"), Ok(None));
}
#[test]
fn test_start_anchor_optimization() {
let mut dfa = make_dfa("^test");
assert_eq!(dfa.find(b"test here"), Ok(Some((0, 4))));
assert_eq!(dfa.find(b"not test"), Ok(None));
}
#[test]
fn test_multiline_start_anchor() {
let mut dfa = make_dfa("(?m)^hello");
assert!(
dfa.has_multiline_anchors(),
"DFA should detect multiline anchors"
);
assert_eq!(dfa.find(b"hello world"), Ok(Some((0, 5))));
assert_eq!(dfa.find(b"first\nhello"), Ok(Some((6, 11))));
assert_eq!(dfa.find(b"line1\nline2\nhello"), Ok(Some((12, 17))));
assert_eq!(dfa.find(b"say hello"), Ok(None));
}
#[test]
fn test_multiline_end_anchor() {
let mut dfa = make_dfa("(?m)world$");
assert!(dfa.has_multiline_anchors());
assert_eq!(dfa.find(b"hello world"), Ok(Some((6, 11))));
assert_eq!(dfa.find(b"world\nnext"), Ok(Some((0, 5))));
assert_eq!(dfa.find(b"world hello"), Ok(None));
}
#[test]
fn test_anchor_empty_input() {
let mut dfa = make_dfa("^$");
assert_eq!(dfa.find(b""), Ok(Some((0, 0))));
assert_eq!(dfa.find(b"x"), Ok(None));
}
#[test]
fn test_engine_facade() {
let mut engine = make_engine("abc");
assert_eq!(engine.find(b"xyzabc123"), Ok(Some((3, 6))));
assert_eq!(engine.is_match_bytes(b"abc"), Ok(true));
assert!(!engine.is_jit());
}
#[test]
fn test_engine_state_count() {
let mut engine = make_engine("[a-z]+");
let _ = engine.find(b"hello");
assert!(engine.state_count() > 0);
}
}