use std::{
cell::RefCell,
time::{Duration, Instant},
};
use crate::error::EngineError;
#[derive(Clone, Debug)]
pub struct ExecutionProfile {
max_number_of_states: Option<usize>,
execution_timeout: Option<u64>,
execution_deadline: Option<Instant>,
implicit_determinization: bool,
}
impl PartialEq for ExecutionProfile {
fn eq(&self, other: &ExecutionProfile) -> bool {
self.max_number_of_states == other.max_number_of_states
&& self.execution_timeout == other.execution_timeout
&& self.implicit_determinization == other.implicit_determinization
}
}
impl ExecutionProfile {
pub fn get() -> ExecutionProfile {
ThreadLocalParams::get_execution_profile()
}
#[inline]
pub fn limits_execution_time(&self) -> bool {
self.execution_deadline.is_some()
}
pub fn assert_not_timed_out(&self) -> Result<(), EngineError> {
if let Some(execution_deadline) = self.execution_deadline {
if Instant::now() > execution_deadline {
Err(EngineError::OperationTimeOutError)
} else {
Ok(())
}
} else {
Ok(())
}
}
#[inline]
pub fn limits_number_of_states(&self) -> bool {
self.max_number_of_states.is_some()
}
pub fn assert_max_number_of_states(&self, number_of_states: usize) -> Result<(), EngineError> {
if let Some(max_number_of_states) = self.max_number_of_states
&& number_of_states > max_number_of_states
{
return Err(EngineError::AutomatonHasTooManyStates);
}
Ok(())
}
pub fn assert_implicit_determinization_allowed(&self) -> Result<(), EngineError> {
if self.implicit_determinization {
Ok(())
} else {
Err(EngineError::DeterministicAutomatonRequired)
}
}
pub fn with_execution_timeout(mut self, execution_timeout_in_ms: u64) -> Self {
self.execution_timeout = Some(execution_timeout_in_ms);
self
}
pub fn with_max_number_of_states(mut self, max_number_of_states: usize) -> Self {
self.max_number_of_states = Some(max_number_of_states);
self
}
pub fn with_implicit_determinization(mut self, allowed: bool) -> Self {
self.implicit_determinization = allowed;
self
}
pub fn run<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = ProfileRestoreGuard::install();
let mut execution_profile = self.clone();
if let Some(execution_timeout) = execution_profile.execution_timeout {
execution_profile.execution_deadline =
Instant::now().checked_add(Duration::from_millis(execution_timeout));
}
ThreadLocalParams::set_execution_profile(&execution_profile);
f()
}
pub fn apply<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
let _guard = ProfileRestoreGuard::install();
ThreadLocalParams::set_execution_profile(self);
f()
}
}
struct ProfileRestoreGuard {
previous: ExecutionProfile,
}
impl ProfileRestoreGuard {
fn install() -> Self {
ProfileRestoreGuard {
previous: ThreadLocalParams::get_execution_profile(),
}
}
}
impl Drop for ProfileRestoreGuard {
fn drop(&mut self) {
ThreadLocalParams::set_execution_profile(&self.previous);
}
}
#[derive(Clone, Debug)]
pub struct ExecutionProfileBuilder {
max_number_of_states: Option<usize>,
execution_timeout: Option<u64>,
implicit_determinization: bool,
}
impl Default for ExecutionProfileBuilder {
fn default() -> Self {
Self::new()
}
}
impl ExecutionProfileBuilder {
pub fn new() -> Self {
Self {
max_number_of_states: None,
execution_timeout: None,
implicit_determinization: true,
}
}
pub fn execution_timeout(mut self, execution_timeout_in_ms: u64) -> Self {
self.execution_timeout = Some(execution_timeout_in_ms);
self
}
pub fn max_number_of_states(mut self, max_number_of_states: usize) -> Self {
self.max_number_of_states = Some(max_number_of_states);
self
}
pub fn implicit_determinization(mut self, allowed: bool) -> Self {
self.implicit_determinization = allowed;
self
}
pub fn build(self) -> ExecutionProfile {
ExecutionProfile {
max_number_of_states: self.max_number_of_states,
execution_timeout: self.execution_timeout,
execution_deadline: None,
implicit_determinization: self.implicit_determinization,
}
}
}
struct ThreadLocalParams;
impl ThreadLocalParams {
thread_local! {
static MAX_NUMBER_OF_STATES: RefCell<Option<usize>> = const { RefCell::new(None) };
static EXECUTION_DEADLINE: RefCell<Option<Instant>> = const { RefCell::new(None) };
static EXECUTION_TIMEOUT: RefCell<Option<u64>> = const { RefCell::new(None) };
static IMPLICIT_DETERMINIZATION: RefCell<bool> = const { RefCell::new(true) };
}
fn set_execution_profile(profile: &ExecutionProfile) {
ThreadLocalParams::MAX_NUMBER_OF_STATES.with(|cell| {
*cell.borrow_mut() = profile.max_number_of_states;
});
ThreadLocalParams::EXECUTION_DEADLINE.with(|cell| {
*cell.borrow_mut() = profile.execution_deadline;
});
ThreadLocalParams::EXECUTION_TIMEOUT.with(|cell| {
*cell.borrow_mut() = profile.execution_timeout;
});
ThreadLocalParams::IMPLICIT_DETERMINIZATION.with(|cell| {
*cell.borrow_mut() = profile.implicit_determinization;
});
}
fn get_max_number_of_states() -> Option<usize> {
ThreadLocalParams::MAX_NUMBER_OF_STATES.with(|cell| *cell.borrow())
}
fn get_execution_deadline() -> Option<Instant> {
ThreadLocalParams::EXECUTION_DEADLINE.with(|cell| *cell.borrow())
}
fn get_execution_timeout() -> Option<u64> {
ThreadLocalParams::EXECUTION_TIMEOUT.with(|cell| *cell.borrow())
}
fn get_implicit_determinization() -> bool {
ThreadLocalParams::IMPLICIT_DETERMINIZATION.with(|cell| *cell.borrow())
}
fn get_execution_profile() -> ExecutionProfile {
ExecutionProfile {
max_number_of_states: Self::get_max_number_of_states(),
execution_deadline: Self::get_execution_deadline(),
execution_timeout: Self::get_execution_timeout(),
implicit_determinization: Self::get_implicit_determinization(),
}
}
}
#[cfg(test)]
mod tests {
use crate::{Term, fast_automaton::GenerationOptions, regex::RegularExpression};
use super::*;
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
#[test]
fn max_number_of_states_allows_exactly_the_limit() {
let profile = ExecutionProfileBuilder::new()
.max_number_of_states(3)
.build();
assert!(profile.assert_max_number_of_states(2).is_ok());
assert!(profile.assert_max_number_of_states(3).is_ok());
assert_eq!(
profile.assert_max_number_of_states(4).unwrap_err(),
EngineError::AutomatonHasTooManyStates
);
}
#[test]
fn test_traits() -> Result<(), String> {
assert_send::<ExecutionProfile>();
assert_sync::<ExecutionProfile>();
Ok(())
}
#[test]
fn run_restores_previous_profile_on_panic() {
let outer = ExecutionProfileBuilder::new()
.max_number_of_states(123)
.build();
outer.run(|| {
let inner = ExecutionProfileBuilder::new()
.max_number_of_states(1)
.build();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
inner.run(|| panic!("intentional test panic"));
}));
assert!(result.is_err());
assert_eq!(outer, ExecutionProfile::get());
});
}
#[test]
fn test_execution_get() -> Result<(), String> {
let execution_profile = ExecutionProfileBuilder::new()
.execution_timeout(1000)
.max_number_of_states(8192)
.build();
execution_profile.run(|| {
assert_eq!(execution_profile, ExecutionProfile::get());
});
Ok(())
}
#[test]
fn test_execution() -> Result<(), String> {
ExecutionProfileBuilder::new()
.max_number_of_states(1)
.build()
.run(|| {
let regex = RegularExpression::new("test").unwrap();
assert!(regex.to_automaton().is_err());
assert_eq!(
EngineError::AutomatonHasTooManyStates,
regex.to_automaton().unwrap_err()
);
});
Ok(())
}
fn nondeterministic_automaton() -> crate::fast_automaton::FastAutomaton {
use crate::fast_automaton::FastAutomaton;
use crate::fast_automaton::condition::Condition;
let mut a = FastAutomaton::new_empty();
let s1 = a.new_state();
let s2 = a.new_state();
let cond = Condition::total(a.spanning_set());
a.add_transition(0, s1, &cond);
a.add_transition(0, s2, &cond);
a.accept(s1);
a.accept(s2);
assert!(!a.is_deterministic());
a
}
#[test]
fn test_implicit_determinization_disabled() {
let nfa = nondeterministic_automaton();
let dfa = nfa.determinize().unwrap().into_owned();
ExecutionProfileBuilder::new()
.implicit_determinization(false)
.build()
.run(|| {
let err = EngineError::DeterministicAutomatonRequired;
assert_eq!(nfa.clone().minimize().unwrap_err(), err);
assert_eq!(nfa.clone().complement().unwrap_err(), err);
assert_eq!(dfa.difference(&nfa).unwrap_err(), err);
assert_eq!(nfa.equivalent(&dfa).unwrap_err(), err);
assert_eq!(dfa.subset(&nfa).unwrap_err(), err);
assert_eq!(nfa.cardinality().unwrap_err(), err);
assert!(nfa.difference(&dfa).is_ok());
assert!(dfa.clone().minimize().is_ok());
assert!(dfa.clone().complement().is_ok());
assert!(dfa.cardinality().is_ok());
assert!(dfa.equivalent(&dfa).is_ok());
assert!(nfa.determinize().is_ok());
});
}
#[test]
fn test_term_api_works_without_implicit_determinization() {
let term = Term::from_automaton(nondeterministic_automaton());
let other = Term::from_pattern("a*").unwrap();
ExecutionProfileBuilder::new()
.implicit_determinization(false)
.build()
.run(|| {
assert!(term.difference(&other).is_ok());
assert!(other.difference(&term).is_ok());
assert!(term.complement().is_ok());
assert!(term.equivalent(&other).is_ok());
assert!(term.subset(&other).is_ok());
assert!(other.subset(&term).is_ok());
assert!(term.is_total().is_ok());
assert!(term.cardinality().is_ok());
assert!(term.minimize().is_ok());
assert!(
term.generate_strings(5, 0, GenerationOptions::new())
.is_ok()
);
assert!(term.concat(std::slice::from_ref(&other)).is_ok());
assert!(term.union(std::slice::from_ref(&other)).is_ok());
assert!(term.intersection(std::slice::from_ref(&other)).is_ok());
assert!(term.repeat(0..=2).is_ok());
assert!(term.is_empty().is_ok());
assert!(term.is_empty_string().is_ok());
let _ = term.length();
let _ = term.to_regex();
let _ = term.to_pattern();
assert!(term.to_automaton().is_ok());
assert_eq!(
nondeterministic_automaton().minimize().unwrap_err(),
EngineError::DeterministicAutomatonRequired
);
});
}
#[test]
fn test_implicit_determinization_default() {
let nfa = nondeterministic_automaton();
assert!(nfa.clone().minimize().is_ok());
assert!(nfa.clone().complement().is_ok());
assert!(nfa.cardinality().is_ok());
assert!(nfa.equivalent(&nfa.clone()).is_ok());
}
#[test]
fn test_execution_timeout_generate_strings() -> Result<(), String> {
let term = Term::from_pattern(".*abc.*def.*qdsqd.*sqdsqd.*qsdsqdsqdz").unwrap();
let execution_timeout_in_ms = 10;
let start_time = Instant::now();
ExecutionProfileBuilder::new()
.execution_timeout(execution_timeout_in_ms)
.build()
.run(|| {
assert_eq!(
EngineError::OperationTimeOutError,
term.generate_strings(100_000_000, 1_000_000, GenerationOptions::new())
.unwrap_err()
);
let run_duration = Instant::now().duration_since(start_time).as_millis();
println!("{run_duration}");
assert!(run_duration <= (execution_timeout_in_ms + 50) as u128);
});
Ok(())
}
#[test]
fn test_execution_timeout_difference() -> Result<(), String> {
let term1 = Term::from_pattern(".*abc.*def.*qdqd.*qsdsqdsqdz").unwrap();
let term2 = Term::from_pattern(".*abc.*def.*qdsqd.*sqdsqd.*qsdsqdsqdz.*abc.*def.*qdsqd.*sqdsqd.*qsdsqdsqdz.*abc.*def.*qdsqd.*sqdsqd.*qsdsqdsqdz").unwrap();
let execution_timeout_in_ms = 0;
let start_time = Instant::now();
ExecutionProfileBuilder::new()
.execution_timeout(execution_timeout_in_ms)
.build()
.run(|| {
assert_eq!(
EngineError::OperationTimeOutError,
term1.difference(&term2).unwrap_err()
);
let run_duration = Instant::now().duration_since(start_time).as_millis();
println!("{run_duration}");
assert!(run_duration <= (execution_timeout_in_ms + 1000) as u128);
});
Ok(())
}
}