use crate::phonetic::nfa::{NFAChar, NFACompilerChar};
use crate::phonetic::regex::ast::RegexFlags;
use super::ast::{LLreFile, SymbolTable};
use super::error::{LLreError, LLreErrorKind, LLreResult};
use super::symbol_expander::expand_pattern_symbols;
#[derive(Debug, Clone)]
pub struct CompiledNFA {
pub nfa: NFAChar,
pub multiline: bool,
pub dotall: bool,
pub case_insensitive: bool,
pub name: Option<String>,
pub version: Option<String>,
}
impl CompiledNFA {
pub fn matches(&self, input: &str) -> bool {
self.nfa
.search_with_flags(input, self.multiline, self.dotall)
}
pub fn matches_full(&self, input: &str) -> bool {
self.nfa
.accepts_with_flags(input, self.multiline, self.dotall)
}
pub fn is_match(&self, input: &str) -> bool {
self.matches(input)
}
pub fn state_count(&self) -> usize {
self.nfa.state_count()
}
pub fn transition_count(&self) -> usize {
self.nfa.num_transitions()
}
}
#[derive(Debug, Clone, Default)]
pub struct CompileOptions {
pub max_states: Option<usize>,
pub use_trampolining: bool,
pub optimize: bool,
}
impl CompileOptions {
pub fn with_max_states(max_states: usize) -> Self {
Self {
max_states: Some(max_states),
..Default::default()
}
}
pub fn optimized() -> Self {
Self {
optimize: true,
..Default::default()
}
}
}
pub fn compile(file: &LLreFile) -> LLreResult<CompiledNFA> {
compile_with_options(file, &CompileOptions::default())
}
pub fn compile_with_options(file: &LLreFile, options: &CompileOptions) -> LLreResult<CompiledNFA> {
let mut compiler = NFACompilerChar::new();
add_symbols_to_compiler(&mut compiler, &file.symbol_table)?;
let expanded_pattern = if !file.symbol_table.patterns.is_empty() {
expand_pattern_symbols(&file.pattern, &file.symbol_table)?
} else {
file.pattern.clone()
};
let flags = file.effective_flags();
compiler.set_flags(flags.clone());
if options.use_trampolining {
compiler.set_trampolining(true);
}
let nfa = compiler.compile(&expanded_pattern).map_err(|e| {
LLreError::with_position(
LLreErrorKind::NfaCompilationFailed(e.to_string()),
file.pattern_position,
)
})?;
if let Some(max_states) = options.max_states {
let state_count = nfa.state_count();
if state_count > max_states {
return Err(LLreError::new(LLreErrorKind::PatternTooComplex {
size: state_count,
max: max_states,
}));
}
}
Ok(CompiledNFA {
nfa,
multiline: flags.multiline.unwrap_or(false),
dotall: flags.dotall.unwrap_or(false),
case_insensitive: flags.case_insensitive.unwrap_or(false),
name: file.metadata.name.clone(),
version: file.metadata.version.clone(),
})
}
fn add_symbols_to_compiler(compiler: &mut NFACompilerChar, table: &SymbolTable) -> LLreResult<()> {
for (name, chars) in &table.char_classes {
compiler.add_symbol(name, chars.clone());
}
Ok(())
}
pub fn compile_pattern(pattern: &str) -> LLreResult<CompiledNFA> {
compile_pattern_with_flags(pattern, &RegexFlags::default())
}
pub fn compile_pattern_with_flags(pattern: &str, flags: &RegexFlags) -> LLreResult<CompiledNFA> {
let regex = crate::phonetic::regex::parse(pattern)?;
let mut compiler = NFACompilerChar::new();
compiler.set_flags(flags.clone());
let nfa = compiler
.compile(®ex)
.map_err(|e| LLreError::new(LLreErrorKind::NfaCompilationFailed(e.to_string())))?;
Ok(CompiledNFA {
nfa,
multiline: flags.multiline.unwrap_or(false),
dotall: flags.dotall.unwrap_or(false),
case_insensitive: flags.case_insensitive.unwrap_or(false),
name: None,
version: None,
})
}
pub fn is_match(pattern: &str, input: &str) -> LLreResult<bool> {
let compiled = compile_pattern(pattern)?;
Ok(compiled.matches(input))
}
pub fn is_match_multiline(pattern: &str, input: &str) -> LLreResult<bool> {
let flags = RegexFlags {
multiline: Some(true),
..Default::default()
};
let compiled = compile_pattern_with_flags(pattern, &flags)?;
Ok(compiled.matches(input))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::phonetic::llre::parser::parse_str;
#[test]
fn test_compile_simple_pattern() {
let file = parse_str("^hello$").expect("Failed to parse");
let compiled = compile(&file).expect("Failed to compile");
assert!(compiled.matches("hello"));
assert!(!compiled.matches("hello world"));
assert!(!compiled.matches("say hello"));
}
#[test]
fn test_compile_with_multiline() {
let file = parse_str(
r#"
@flags multiline
^hello$
"#,
)
.expect("Failed to parse");
let compiled = compile(&file).expect("Failed to compile");
assert!(compiled.multiline);
assert!(compiled.matches("hello"));
}
#[test]
fn test_compile_pattern_directly() {
let compiled = compile_pattern("[a-z]+").expect("Failed to compile");
assert!(compiled.matches("hello"));
assert!(!compiled.matches("123"));
}
#[test]
fn test_compile_with_flags() {
let flags = RegexFlags {
multiline: Some(true),
..Default::default()
};
let compiled = compile_pattern_with_flags("^test$", &flags).expect("Failed to compile");
assert!(compiled.multiline);
}
#[test]
fn test_is_match_helper() {
assert!(is_match("hello", "hello").expect("Failed"));
assert!(!is_match("hello", "world").expect("Failed"));
}
#[test]
fn test_compile_with_symbols() {
let mut file = parse_str("^[a-z]+$").expect("Failed to parse");
file.symbol_table
.add_char_class("VOWEL", vec!['a', 'e', 'i', 'o', 'u'], None);
let compiled = compile(&file).expect("Failed to compile");
assert!(compiled.matches("hello"));
}
#[test]
fn test_compile_options_max_states() {
let file = parse_str("(a|b|c|d|e)+").expect("Failed to parse");
let options = CompileOptions::with_max_states(5);
let result = compile_with_options(&file, &options);
assert!(
result.is_ok()
|| matches!(
result.unwrap_err().kind,
LLreErrorKind::PatternTooComplex { .. }
)
);
}
#[test]
fn test_compiled_nfa_stats() {
let compiled = compile_pattern("[a-z]+").expect("Failed to compile");
assert!(compiled.state_count() > 0);
assert!(compiled.transition_count() > 0);
}
#[test]
fn test_compile_anchors() {
let compiled = compile_pattern("^hello").expect("Failed to compile");
assert!(compiled.matches("hello"));
assert!(compiled.matches("hello world"));
assert!(!compiled.matches("say hello"));
let compiled = compile_pattern("hello$").expect("Failed to compile");
assert!(compiled.matches("hello"));
assert!(compiled.matches("say hello"));
assert!(!compiled.matches("hello world"));
let compiled = compile_pattern("^hello$").expect("Failed to compile");
assert!(compiled.matches("hello"));
assert!(!compiled.matches("hello world"));
assert!(!compiled.matches("say hello"));
}
#[test]
fn test_compile_alternation() {
let compiled = compile_pattern("cat|dog|bird").expect("Failed to compile");
assert!(compiled.matches("cat"));
assert!(compiled.matches("dog"));
assert!(compiled.matches("bird"));
assert!(!compiled.matches("fish"));
}
#[test]
fn test_compile_quantifiers() {
let compiled = compile_pattern("ab*c").expect("Failed to compile");
assert!(compiled.matches("ac"));
assert!(compiled.matches("abc"));
assert!(compiled.matches("abbc"));
assert!(!compiled.matches("adc"));
let compiled = compile_pattern("ab+c").expect("Failed to compile");
assert!(!compiled.matches("ac"));
assert!(compiled.matches("abc"));
assert!(compiled.matches("abbc"));
let compiled = compile_pattern("ab?c").expect("Failed to compile");
assert!(compiled.matches("ac"));
assert!(compiled.matches("abc"));
assert!(!compiled.matches("abbc"));
}
#[test]
fn test_compile_character_classes() {
let compiled = compile_pattern("[aeiou]+").expect("Failed to compile");
assert!(compiled.matches("aeiou"));
assert!(compiled.matches("a"));
assert!(!compiled.matches("xyz"));
let compiled = compile_pattern("[^aeiou]+").expect("Failed to compile");
assert!(compiled.matches("xyz"));
assert!(!compiled.matches("aeiou"));
}
#[test]
fn test_compile_groups() {
let compiled = compile_pattern("(ab)+").expect("Failed to compile");
assert!(compiled.matches("ab"));
assert!(compiled.matches("abab"));
assert!(!compiled.matches("a"));
assert!(!compiled.matches("ba"));
}
}