#![warn(missing_docs)]
use std::{
borrow::{Borrow, Cow},
collections::{HashMap, HashSet, VecDeque},
fmt::Display,
hash::BuildHasherDefault,
ops::{Bound, RangeBounds},
str::FromStr,
};
use cardinality::Cardinality;
use error::EngineError;
use fast_automaton::{FastAutomaton, GenerationOptions};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
use regex::RegularExpression;
use regex_charclass::{char::Char, irange::RangeSet};
use crate::execution_profile::ExecutionProfile;
pub mod cardinality;
pub mod error;
pub mod execution_profile;
pub mod fast_automaton;
pub mod regex;
pub use regex_charclass;
#[derive(Clone, Copy, Debug, Default)]
pub struct NoHashHasher<Key>(u64, std::marker::PhantomData<Key>);
macro_rules! impl_no_hash_hasher {
($($int:ty => $write:ident),* $(,)?) => {
$(
impl std::hash::Hasher for NoHashHasher<$int> {
#[inline]
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, _: &[u8]) {
unreachable!("NoHashHasher hashes integer keys through their value");
}
#[inline]
fn $write(&mut self, n: $int) {
self.0 = n as u64;
}
}
)*
};
}
impl_no_hash_hasher!(u32 => write_u32, u64 => write_u64, usize => write_usize);
pub(crate) type IntMap<Key, Value> = HashMap<Key, Value, BuildHasherDefault<NoHashHasher<Key>>>;
pub type IntSet<Key> = HashSet<Key, BuildHasherDefault<NoHashHasher<Key>>>;
pub type CharRange = RangeSet<Char>;
#[derive(Clone, PartialEq, Eq, Debug)]
#[must_use = "terms are immutable; operations return a new term"]
pub enum Term {
RegularExpression(RegularExpression),
Automaton(FastAutomaton),
}
impl Default for Term {
fn default() -> Self {
Term::new_empty()
}
}
impl Display for Term {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Term::RegularExpression(regular_expression) => write!(f, "{regular_expression}"),
Term::Automaton(fast_automaton) => write!(f, "{fast_automaton}"),
}
}
}
impl FromStr for Term {
type Err = EngineError;
fn from_str(pattern: &str) -> Result<Self, Self::Err> {
Term::from_pattern(pattern)
}
}
impl From<RegularExpression> for Term {
fn from(regex: RegularExpression) -> Self {
Term::RegularExpression(regex)
}
}
impl From<FastAutomaton> for Term {
fn from(automaton: FastAutomaton) -> Self {
Term::Automaton(automaton)
}
}
impl Term {
fn run_with_implicit_determinization<R>(f: impl FnOnce() -> R) -> R {
ExecutionProfile::get()
.with_implicit_determinization(true)
.apply(f)
}
pub fn new_empty() -> Self {
Term::RegularExpression(RegularExpression::new_empty())
}
pub fn new_total() -> Self {
Term::RegularExpression(RegularExpression::new_total())
}
pub fn new_empty_string() -> Self {
Term::RegularExpression(RegularExpression::new_empty_string())
}
pub fn from_pattern(pattern: &str) -> Result<Self, EngineError> {
Ok(Term::RegularExpression(RegularExpression::new(pattern)?))
}
pub fn from_regex(regex: RegularExpression) -> Self {
Term::RegularExpression(regex)
}
pub fn from_automaton(automaton: FastAutomaton) -> Self {
Term::Automaton(automaton)
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn concat(
&self,
terms: impl IntoIterator<Item = impl Borrow<Term>>,
) -> Result<Term, EngineError> {
let mut return_regex = RegularExpression::new_empty();
let mut return_automaton = FastAutomaton::new_empty();
let mut has_automaton = false;
match self {
Term::RegularExpression(regular_expression) => {
return_regex = regular_expression.clone()
}
Term::Automaton(fast_automaton) => {
has_automaton = true;
return_automaton = fast_automaton.clone();
}
}
for term in terms {
let term = term.borrow();
if has_automaton {
return_automaton = return_automaton.concat(term.to_automaton()?.as_ref())?;
} else {
match term {
Term::RegularExpression(regular_expression) => {
return_regex = return_regex.concat(regular_expression, true);
}
Term::Automaton(fast_automaton) => {
has_automaton = true;
return_automaton = return_regex.to_automaton()?.concat(fast_automaton)?;
}
}
}
}
if !has_automaton {
Ok(Term::RegularExpression(return_regex))
} else {
Ok(Term::Automaton(return_automaton))
}
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn union(
&self,
terms: impl IntoIterator<Item = impl Borrow<Term>>,
) -> Result<Term, EngineError> {
let terms: Vec<_> = terms.into_iter().collect();
let terms: Vec<&Term> = terms.iter().map(Borrow::borrow).collect();
let mut has_automaton = matches!(self, Term::Automaton(_));
if !has_automaton {
for term in &terms {
if matches!(term, Term::Automaton(_)) {
has_automaton = true;
break;
}
}
}
if has_automaton {
let parallel = cfg!(feature = "parallel") && terms.len() > 3;
let automaton_list = self.get_automata(&terms, parallel)?;
let automaton_list = automaton_list.iter().map(AsRef::as_ref).collect::<Vec<_>>();
#[cfg(feature = "parallel")]
let return_automaton = if parallel {
FastAutomaton::union_all_par(automaton_list)
} else {
FastAutomaton::union_all(automaton_list)
}?;
#[cfg(not(feature = "parallel"))]
let return_automaton = FastAutomaton::union_all(automaton_list)?;
Ok(Term::Automaton(return_automaton))
} else {
let regexes_list = self.get_regexes(&terms)?;
let regexes_list = regexes_list.iter().map(AsRef::as_ref).collect::<Vec<_>>();
Ok(Term::RegularExpression(RegularExpression::union_all(
regexes_list,
)))
}
}
#[tracing::instrument(level = "debug", skip_all)]
pub fn intersection(
&self,
terms: impl IntoIterator<Item = impl Borrow<Term>>,
) -> Result<Term, EngineError> {
let terms: Vec<_> = terms.into_iter().collect();
let terms: Vec<&Term> = terms.iter().map(Borrow::borrow).collect();
let parallel = cfg!(feature = "parallel") && terms.len() > 3;
let automaton_list = self.get_automata(&terms, parallel)?;
let automaton_list = automaton_list.iter().map(AsRef::as_ref).collect::<Vec<_>>();
#[cfg(feature = "parallel")]
let return_automaton = if terms.len() > 3 {
FastAutomaton::intersection_all_par(automaton_list)
} else {
FastAutomaton::intersection_all(automaton_list)
}?;
#[cfg(not(feature = "parallel"))]
let return_automaton = FastAutomaton::intersection_all(automaton_list)?;
Ok(Term::Automaton(return_automaton))
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic(), other_deterministic = other.is_deterministic()))]
pub fn difference(&self, other: &Term) -> Result<Term, EngineError> {
Self::run_with_implicit_determinization(|| {
let minuend_automaton = self.to_automaton()?;
let subtrahend_automaton = other.to_automaton()?;
let return_automaton = minuend_automaton.difference(&subtrahend_automaton)?;
Ok(Term::Automaton(return_automaton))
})
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic()))]
pub fn complement(&self) -> Result<Term, EngineError> {
Self::run_with_implicit_determinization(|| {
let mut automaton = self.to_automaton()?.into_owned();
automaton.complement()?;
Ok(Term::Automaton(automaton))
})
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic(), min = tracing::field::Empty, max = tracing::field::Empty))]
pub fn repeat(&self, range: impl RangeBounds<u32>) -> Result<Term, EngineError> {
let mut min = match range.start_bound() {
Bound::Included(&n) => n,
Bound::Excluded(&n) => n.saturating_add(1),
Bound::Unbounded => 0,
};
let max_opt = match range.end_bound() {
Bound::Included(&n) => Some(n),
Bound::Excluded(&n) => Some(n.saturating_sub(1)),
Bound::Unbounded => None,
};
if matches!(range.end_bound(), Bound::Excluded(&0)) {
min = min.max(1);
}
let span = tracing::Span::current();
span.record("min", min);
span.record("max", tracing::field::debug(max_opt));
match self {
Term::RegularExpression(regular_expression) => Ok(Term::RegularExpression(
regular_expression.repeat(min, max_opt),
)),
Term::Automaton(fast_automaton) => {
let repeat_automaton = fast_automaton.repeat(min, max_opt)?;
Ok(Term::Automaton(repeat_automaton))
}
}
}
#[tracing::instrument(level = "debug", skip(self, options), fields(self_deterministic = self.is_deterministic(), limit = limit, offset = offset))]
pub fn generate_strings(
&self,
limit: usize,
offset: usize,
options: impl Into<GenerationOptions>,
) -> Result<Vec<String>, EngineError> {
self.to_automaton()?
.generate_strings(limit, offset, options)
}
pub fn iter_strings(&self, options: impl Into<GenerationOptions>) -> StringGenerator<'_> {
let options = options.into();
match self.to_deterministic_automaton() {
Ok(automaton) => StringGenerator {
automaton: Some(automaton),
pending_error: None,
offset: 0,
options,
buffer: VecDeque::new(),
},
Err(e) => StringGenerator {
automaton: None,
pending_error: Some(e),
offset: 0,
options,
buffer: VecDeque::new(),
},
}
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic()))]
pub fn determinize(&self) -> Result<Term, EngineError> {
let automaton = self.to_automaton()?;
let determinized = automaton.determinize()?.into_owned();
Ok(Term::Automaton(determinized))
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic(), self_minimal = self.is_minimal()))]
pub fn minimize(&self) -> Result<Term, EngineError> {
Self::run_with_implicit_determinization(|| {
let mut automaton = self.to_automaton()?.into_owned();
automaton.minimize()?;
Ok(Term::Automaton(automaton))
})
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic(), other_deterministic = other.is_deterministic()))]
pub fn equivalent(&self, other: &Term) -> Result<bool, EngineError> {
if self == other {
return Ok(true);
}
Self::run_with_implicit_determinization(|| {
let automaton_1 = self.to_automaton()?;
let automaton_2 = other.to_automaton()?;
automaton_1.equivalent(&automaton_2)
})
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic(), other_deterministic = other.is_deterministic()))]
pub fn subset(&self, other: &Term) -> Result<bool, EngineError> {
if self == other {
return Ok(true);
}
Self::run_with_implicit_determinization(|| {
let automaton_1 = self.to_automaton()?;
let automaton_2 = other.to_automaton()?;
automaton_1.subset(&automaton_2)
})
}
#[tracing::instrument(level = "debug", skip(self, input), fields(self_deterministic = self.is_deterministic(), input_len = input.len()))]
pub fn matches(&self, input: &str) -> Result<bool, EngineError> {
Ok(self.to_automaton()?.is_match(input))
}
pub fn is_empty(&self) -> Result<bool, EngineError> {
Ok(match self {
Term::RegularExpression(regex) => regex.is_empty(),
Term::Automaton(automaton) => automaton.is_empty(),
})
}
pub fn is_total(&self) -> Result<bool, EngineError> {
if let Term::RegularExpression(regex) = self
&& regex.is_total()
{
return Ok(true);
}
let automaton = self.to_automaton()?;
if automaton.is_total() {
Ok(true)
} else if automaton.is_deterministic() {
Ok(false)
} else {
Ok(automaton.determinize()?.is_total())
}
}
pub fn is_empty_string(&self) -> Result<bool, EngineError> {
Ok(match self {
Term::RegularExpression(regex) => regex.is_empty_string(),
Term::Automaton(automaton) => automaton.is_empty_string(),
})
}
#[must_use]
pub fn is_deterministic(&self) -> bool {
match self {
Term::RegularExpression(_) => false,
Term::Automaton(automaton) => automaton.is_deterministic(),
}
}
#[must_use]
pub fn is_minimal(&self) -> bool {
match self {
Term::RegularExpression(_) => false,
Term::Automaton(automaton) => automaton.is_minimal(),
}
}
#[must_use]
pub fn length(&self) -> (Option<u32>, Option<u32>) {
match self {
Term::RegularExpression(regex) => regex.length(),
Term::Automaton(automaton) => automaton.length(),
}
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic()))]
pub fn cardinality(&self) -> Result<Cardinality<u32>, EngineError> {
Self::run_with_implicit_determinization(|| self.to_automaton()?.cardinality())
}
pub fn is_finite(&self) -> Result<bool, EngineError> {
Ok(!matches!(self.cardinality()?, Cardinality::Infinite))
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic()))]
pub fn to_automaton(&self) -> Result<Cow<'_, FastAutomaton>, EngineError> {
Ok(match self {
Term::RegularExpression(regex) => Cow::Owned(regex.to_automaton()?),
Term::Automaton(automaton) => Cow::Borrowed(automaton),
})
}
fn to_deterministic_automaton(&self) -> Result<Cow<'_, FastAutomaton>, EngineError> {
let automaton = self.to_automaton()?;
if automaton.is_deterministic() {
return Ok(automaton);
}
Ok(Cow::Owned(automaton.determinize()?.into_owned()))
}
#[tracing::instrument(level = "debug", skip_all, fields(self_deterministic = self.is_deterministic()))]
pub fn to_regex(&self) -> Result<Cow<'_, RegularExpression>, EngineError> {
Ok(match self {
Term::RegularExpression(regex) => Cow::Borrowed(regex),
Term::Automaton(automaton) => Cow::Owned(automaton.to_regex()?),
})
}
pub fn to_pattern(&self) -> Result<String, EngineError> {
Ok(self.to_regex()?.to_string())
}
fn get_automata<'a>(
&'a self,
terms: &[&'a Term],
parallel: bool,
) -> Result<Vec<Cow<'a, FastAutomaton>>, EngineError> {
let mut automaton_list = Vec::with_capacity(terms.len() + 1);
automaton_list.push(self.to_automaton()?);
#[cfg(feature = "parallel")]
let mut terms_automata = if parallel {
let execution_profile = ExecutionProfile::get();
terms
.par_iter()
.map(|a| execution_profile.apply(|| a.to_automaton()))
.collect::<Result<Vec<_>, _>>()
} else {
terms
.iter()
.map(|a| a.to_automaton())
.collect::<Result<Vec<_>, _>>()
}?;
#[cfg(not(feature = "parallel"))]
let mut terms_automata = {
let _ = parallel;
terms
.iter()
.map(|a| a.to_automaton())
.collect::<Result<Vec<_>, EngineError>>()?
};
automaton_list.append(&mut terms_automata);
Ok(automaton_list)
}
fn get_regexes<'a>(
&'a self,
terms: &[&'a Term],
) -> Result<Vec<Cow<'a, RegularExpression>>, EngineError> {
let mut regex_list = Vec::with_capacity(terms.len() + 1);
regex_list.push(self.to_regex()?);
for term in terms {
regex_list.push(term.to_regex()?);
}
Ok(regex_list)
}
}
#[derive(Debug)]
pub struct StringGenerator<'a> {
automaton: Option<Cow<'a, FastAutomaton>>,
pending_error: Option<EngineError>,
offset: usize,
options: GenerationOptions,
buffer: VecDeque<String>,
}
impl std::iter::FusedIterator for StringGenerator<'_> {}
impl Iterator for StringGenerator<'_> {
type Item = Result<String, EngineError>;
fn next(&mut self) -> Option<Self::Item> {
const BATCH: usize = 32;
if let Some(s) = self.buffer.pop_front() {
return Some(Ok(s));
}
if let Some(e) = self.pending_error.take() {
return Some(Err(e));
}
let automaton = self.automaton.as_ref()?;
match automaton.generate(BATCH, self.offset, &self.options) {
Ok(batch) => {
if batch.len() < BATCH {
self.automaton = None;
}
self.offset += batch.len();
self.buffer.extend(batch);
self.buffer.pop_front().map(Ok)
}
Err(e) => {
self.automaton = None;
Some(Err(e))
}
}
}
}
#[cfg(test)]
mod tests {
use crate::fast_automaton::GenerationOptions;
use crate::regex::RegularExpression;
use super::*;
#[test]
#[allow(clippy::reversed_empty_ranges)] fn repeat_empty_ranges_yield_the_empty_language() {
let regex_term = Term::from_pattern("abc").unwrap();
let automaton_term = regex_term.determinize().unwrap();
assert!(matches!(automaton_term, Term::Automaton(..)));
for term in [regex_term, automaton_term] {
assert!(term.repeat(0..0).unwrap().is_empty().unwrap());
assert!(term.repeat(3..3).unwrap().is_empty().unwrap());
assert!(term.repeat(5..2).unwrap().is_empty().unwrap());
assert!(term.repeat(0..=0).unwrap().is_empty_string().unwrap());
assert!(term.repeat(0..1).unwrap().is_empty_string().unwrap());
}
}
#[test]
fn display_is_pattern_for_regexes_and_dot_for_automata() {
let regex_term = Term::from_pattern("(abc){2}").unwrap();
assert_eq!("(abc){2}", regex_term.to_string());
let automaton_term = regex_term.determinize().unwrap();
assert!(matches!(automaton_term, Term::Automaton(..)));
assert!(automaton_term.to_string().starts_with("digraph"));
let reparsed: Term = automaton_term.to_pattern().unwrap().parse().unwrap();
assert!(reparsed.equivalent(&automaton_term).unwrap());
}
#[test]
fn to_pattern_honors_the_execution_deadline() {
let term = Term::from_pattern(".*abc.*def.*")
.unwrap()
.determinize()
.unwrap();
crate::execution_profile::ExecutionProfileBuilder::new()
.execution_timeout(0)
.build()
.run(|| {
assert_eq!(
EngineError::OperationTimeOutError,
term.to_pattern().unwrap_err()
);
});
assert!(term.to_pattern().is_ok());
}
#[test]
fn test_complement() -> Result<(), String> {
let term = Term::from_pattern("(abc|de)").unwrap();
let complement = term.complement().unwrap();
assert!(
term.intersection([&complement])
.unwrap()
.is_empty()
.unwrap()
);
println!("term: {}", term.to_automaton().unwrap().to_dot());
if let Term::Automaton(complement) = &complement {
println!("complement: {}", complement.to_dot());
}
let union = term.union(&[complement]).unwrap();
if let Term::Automaton(union) = &union {
println!("{}", union.to_dot());
let union = union.determinize().unwrap();
println!("{}", union.to_dot());
}
assert!(union.is_total().unwrap());
Ok(())
}
#[test]
fn union_of_regex_with_complement_pattern_is_total() {
for pattern in ["(abc|de)", "a", "x*", "[0-9]{2,4}"] {
let term = Term::from_pattern(pattern).unwrap();
let complement_pattern = term.complement().unwrap().to_pattern().unwrap();
let complement = Term::from_pattern(&complement_pattern).unwrap();
assert!(matches!(complement, Term::RegularExpression(..)));
let union = term.union([&complement]).unwrap();
assert!(matches!(union, Term::RegularExpression(..)));
assert!(union.is_total().unwrap(), "not total for {pattern}");
assert_eq!(
".*",
union.minimize().unwrap().to_pattern().unwrap(),
"wrong minimized pattern for {pattern}"
);
}
}
#[test]
fn test_intersection() -> Result<(), String> {
let regex1 = Term::from_pattern("a").unwrap();
let regex2 = Term::from_pattern("b").unwrap();
let intersection = regex1.intersection(&[regex2]).unwrap();
assert!(intersection.is_empty().unwrap());
assert_eq!("[]", intersection.to_pattern().unwrap());
Ok(())
}
#[test]
fn test_difference_1() -> Result<(), String> {
let regex1 = Term::from_pattern("a*").unwrap();
let regex2 = Term::from_pattern("").unwrap();
let result = regex1.difference(®ex2);
assert!(result.is_ok());
let result = result.unwrap().to_pattern().unwrap();
assert_eq!("a+", result);
Ok(())
}
#[test]
fn test_difference_2() -> Result<(), String> {
let regex1 = Term::from_pattern("x*").unwrap();
let regex2 = Term::from_pattern("(xxx)*").unwrap();
let result = regex1.difference(®ex2);
assert!(result.is_ok());
let result = result.unwrap().to_regex().unwrap().into_owned();
assert_eq!(
Term::RegularExpression(RegularExpression::new("x(x{3})*x?").unwrap()),
Term::RegularExpression(result)
);
Ok(())
}
#[test]
fn test_intersection_1() -> Result<(), String> {
let regex1 = Term::from_pattern("a*").unwrap();
let regex2 = Term::from_pattern("b*").unwrap();
let result = regex1.intersection(&[regex2]);
assert!(result.is_ok());
let result = result.unwrap().to_pattern().unwrap();
assert_eq!("", result);
Ok(())
}
#[test]
fn test_intersection_2() -> Result<(), String> {
let regex1 = Term::from_pattern("x*").unwrap();
let regex2 = Term::from_pattern("(xxx)*").unwrap();
let result = regex1.intersection(&[regex2]);
assert!(result.is_ok());
let result = result.unwrap().to_pattern().unwrap();
assert_eq!("(x{3})*", result);
Ok(())
}
#[test]
fn test_default_is_empty_language() {
assert!(Term::default().is_empty().unwrap());
assert_eq!(Term::default(), Term::new_empty());
}
#[test]
fn test_iter_strings_exhaustive_matches_generate_strings() {
let term = Term::from_pattern("[A-Za-z0-9]")
.unwrap()
.minimize()
.unwrap();
let eager = term
.generate_strings(1000, 0, GenerationOptions::new())
.unwrap();
let lazy = term
.iter_strings(GenerationOptions::new())
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(eager.len(), lazy.len());
assert_eq!(eager, lazy);
assert_eq!(62, lazy.len());
}
#[test]
fn test_is_finite() {
assert!(
Term::from_pattern("(ab|c){2}")
.unwrap()
.is_finite()
.unwrap()
);
assert!(!Term::from_pattern("a+").unwrap().is_finite().unwrap());
}
#[test]
fn test_matches_is_anchored() {
let term = Term::from_pattern("abc.*").unwrap();
assert!(term.matches("abc").unwrap());
assert!(term.matches("abcdef").unwrap());
assert!(!term.matches("xyzabc").unwrap());
let exact = Term::from_pattern("abc").unwrap();
assert!(exact.matches("abc").unwrap());
assert!(!exact.matches("abcd").unwrap());
let automaton_backed = exact.intersection([&term]).unwrap();
assert!(matches!(automaton_backed, Term::Automaton(_)));
assert!(automaton_backed.matches("abc").unwrap());
assert!(!automaton_backed.matches("abcd").unwrap());
assert!(!Term::new_empty().matches("").unwrap());
assert!(Term::new_empty_string().matches("").unwrap());
assert!(!Term::new_empty_string().matches("a").unwrap());
}
#[test]
fn test_from_str_and_from_conversions() {
let parsed: Term = "abc".parse().unwrap();
assert_eq!(parsed, Term::from_pattern("abc").unwrap());
assert!(r"(a)\1".parse::<Term>().is_err());
let regex = RegularExpression::new("abc").unwrap();
let from_into: Term = regex.clone().into();
assert_eq!(from_into, Term::from_regex(regex));
let automaton = Term::from_pattern("abc")
.unwrap()
.to_automaton()
.unwrap()
.into_owned();
let from_into: Term = automaton.clone().into();
assert_eq!(from_into, Term::from_automaton(automaton));
}
#[test]
fn test_is_deterministic_and_determinize() {
let regex_term = Term::from_pattern("(abc|de){2}").unwrap();
assert!(!regex_term.is_deterministic());
let dfa = regex_term.determinize().unwrap();
assert!(dfa.is_deterministic());
assert!(regex_term.equivalent(&dfa).unwrap());
let dfa2 = dfa.determinize().unwrap();
assert!(dfa2.is_deterministic());
assert!(dfa.equivalent(&dfa2).unwrap());
}
#[test]
fn test_is_minimal_and_minimize() {
let regex_term = Term::from_pattern("(abc|de){2}").unwrap();
assert!(!regex_term.is_minimal());
let minimal = regex_term.minimize().unwrap();
assert!(minimal.is_minimal());
assert!(minimal.is_deterministic()); assert!(regex_term.equivalent(&minimal).unwrap());
}
#[test]
fn test_eq_is_structural_not_language() {
let regex_term = Term::from_pattern("(a|b)*").unwrap();
let automaton_term = Term::from_automaton(regex_term.to_automaton().unwrap().into_owned());
assert_ne!(regex_term, automaton_term);
assert!(regex_term.equivalent(&automaton_term).unwrap());
}
#[test]
fn test_repeat_range_edges() {
let term = Term::from_pattern("abc").unwrap();
assert_eq!("(abc)*", term.repeat(..).unwrap().to_pattern().unwrap());
assert_eq!("(abc){2,}", term.repeat(2..).unwrap().to_pattern().unwrap());
assert_eq!(
"(abc){0,2}",
term.repeat(..3).unwrap().to_pattern().unwrap()
);
assert!(term.repeat(0..=0).unwrap().is_empty_string().unwrap());
let (min, max) = (5u32, 3u32);
assert!(term.repeat(min..max).unwrap().is_empty().unwrap());
}
#[test]
fn test_iter_strings_is_lazy_on_infinite_language() {
let term = Term::from_pattern("a+").unwrap();
let first = term
.iter_strings(GenerationOptions::new())
.take(5)
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(5, first.len());
}
#[test]
fn test_iter_strings_propagates_error_then_ends() {
use crate::execution_profile::ExecutionProfileBuilder;
let term = Term::from_pattern("abcdef").unwrap();
let profile = ExecutionProfileBuilder::new()
.max_number_of_states(1)
.build();
profile.run(|| {
let mut it = term.iter_strings(GenerationOptions::new());
assert!(matches!(
it.next(),
Some(Err(EngineError::AutomatonHasTooManyStates))
));
assert!(it.next().is_none());
});
}
#[test]
fn test_variadic_ops_with_no_operands_equal_self() {
let term = Term::from_pattern("abc").unwrap();
assert!(
term.concat(std::iter::empty::<&Term>())
.unwrap()
.equivalent(&term)
.unwrap()
);
assert!(
term.union(std::iter::empty::<&Term>())
.unwrap()
.equivalent(&term)
.unwrap()
);
assert!(
term.intersection(std::iter::empty::<&Term>())
.unwrap()
.equivalent(&term)
.unwrap()
);
}
}