use std::{cell::RefCell, time::Duration};
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
use std::{sync::OnceLock, time::Instant};
use crate::error::EngineError;
pub type Clock = fn() -> Duration;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
fn std_clock() -> Duration {
static ORIGIN: OnceLock<Instant> = OnceLock::new();
ORIGIN.get_or_init(Instant::now).elapsed()
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
const DEFAULT_CLOCK: Option<Clock> = Some(std_clock);
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
const DEFAULT_CLOCK: Option<Clock> = None;
#[derive(Clone, Debug)]
pub struct ExecutionProfile {
max_number_of_states: Option<usize>,
execution_timeout: Option<u64>,
execution_deadline: Option<Duration>,
clock: Option<Clock>,
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), Some(clock)) = (self.execution_deadline, self.clock)
&& clock() > execution_deadline
{
return Err(EngineError::OperationTimeOutError);
}
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 with_clock(mut self, clock: Clock) -> Self {
self.clock = Some(clock);
self.execution_deadline = None;
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 {
let clock = execution_profile.clock.expect(
"an execution timeout is set but the profile has no clock: this target cannot \
read the time through the standard library, so supply one with \
`ExecutionProfileBuilder::clock` (for instance a binding to `performance.now()`)",
);
execution_profile.execution_deadline =
clock().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,
clock: Option<Clock>,
}
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,
clock: DEFAULT_CLOCK,
}
}
pub fn execution_timeout(mut self, execution_timeout_in_ms: u64) -> Self {
self.execution_timeout = Some(execution_timeout_in_ms);
self
}
pub fn clock(mut self, clock: Clock) -> Self {
self.clock = Some(clock);
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,
clock: self.clock,
}
}
}
struct ThreadLocalParams;
impl ThreadLocalParams {
thread_local! {
static MAX_NUMBER_OF_STATES: RefCell<Option<usize>> = const { RefCell::new(None) };
static EXECUTION_DEADLINE: RefCell<Option<Duration>> = const { RefCell::new(None) };
static EXECUTION_TIMEOUT: RefCell<Option<u64>> = const { RefCell::new(None) };
static IMPLICIT_DETERMINIZATION: RefCell<bool> = const { RefCell::new(true) };
static CLOCK: RefCell<Option<Clock>> = const { RefCell::new(DEFAULT_CLOCK) };
}
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;
});
ThreadLocalParams::CLOCK.with(|cell| {
*cell.borrow_mut() = profile.clock;
});
}
fn get_max_number_of_states() -> Option<usize> {
ThreadLocalParams::MAX_NUMBER_OF_STATES.with(|cell| *cell.borrow())
}
fn get_execution_deadline() -> Option<Duration> {
ThreadLocalParams::EXECUTION_DEADLINE.with(|cell| *cell.borrow())
}
fn get_clock() -> Option<Clock> {
ThreadLocalParams::CLOCK.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(),
clock: Self::get_clock(),
}
}
}
#[cfg(test)]
mod tests {
use std::{
cell::Cell,
sync::atomic::{AtomicU64, Ordering},
time::Instant,
};
use crate::{Term, fast_automaton::GenerationOptions, regex::RegularExpression};
use super::*;
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
thread_local! {
static FAKE_NOW: Cell<Duration> = const { Cell::new(Duration::ZERO) };
}
fn fake_clock() -> Duration {
FAKE_NOW.get()
}
fn ticking_clock() -> Duration {
static READINGS: AtomicU64 = AtomicU64::new(0);
Duration::from_millis(READINGS.fetch_add(1, Ordering::Relaxed))
}
#[test]
fn timeout_is_measured_against_the_profile_clock() {
FAKE_NOW.set(Duration::from_millis(1_000));
ExecutionProfileBuilder::new()
.execution_timeout(10)
.clock(fake_clock)
.build()
.run(|| {
let profile = ExecutionProfile::get();
assert!(profile.limits_execution_time());
assert!(profile.assert_not_timed_out().is_ok());
FAKE_NOW.set(Duration::from_millis(1_010));
assert!(
profile.assert_not_timed_out().is_ok(),
"the deadline itself is allowed"
);
FAKE_NOW.set(Duration::from_millis(1_011));
assert_eq!(
profile.assert_not_timed_out().unwrap_err(),
EngineError::OperationTimeOutError
);
});
}
#[test]
fn operation_times_out_on_an_injected_clock() {
let term = Term::from_pattern(".*abc.*def.*qdsqd.*sqdsqd.*qsdsqdsqdz").unwrap();
ExecutionProfileBuilder::new()
.execution_timeout(5)
.clock(ticking_clock)
.build()
.run(|| {
assert_eq!(
EngineError::OperationTimeOutError,
term.generate_strings(100_000_000, 0, GenerationOptions::new())
.unwrap_err()
);
});
}
#[test]
fn apply_keeps_the_running_deadline() {
FAKE_NOW.set(Duration::ZERO);
ExecutionProfileBuilder::new()
.execution_timeout(10)
.clock(fake_clock)
.build()
.run(|| {
let running = ExecutionProfile::get();
FAKE_NOW.set(Duration::from_millis(20));
running.apply(|| {
assert_eq!(
ExecutionProfile::get().assert_not_timed_out().unwrap_err(),
EngineError::OperationTimeOutError
);
});
});
}
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
#[test]
fn ambient_profile_has_the_default_clock() {
ExecutionProfile::get()
.with_execution_timeout(60_000)
.run(|| {
let profile = ExecutionProfile::get();
assert!(profile.limits_execution_time());
assert!(profile.assert_not_timed_out().is_ok());
});
}
#[test]
#[cfg_attr(
target_family = "wasm",
ignore = "wasm is panic = abort: a panicking test aborts the whole binary"
)]
#[should_panic(expected = "no clock")]
fn run_without_a_clock_panics_clearly() {
let mut profile = ExecutionProfileBuilder::new().execution_timeout(10).build();
profile.clock = None;
profile.run(|| {});
}
#[test]
fn run_without_a_timeout_needs_no_clock() {
let mut profile = ExecutionProfileBuilder::new()
.max_number_of_states(3)
.build();
profile.clock = None;
profile.run(|| {
let profile = ExecutionProfile::get();
assert!(!profile.limits_execution_time());
assert!(profile.assert_not_timed_out().is_ok());
});
}
#[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]
#[cfg_attr(
target_family = "wasm",
ignore = "wasm is panic = abort: a panicking test aborts the whole binary"
)]
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(())
}
}