use std::sync::Arc;
use crate::hash::FxHashMap;
use crate::hir::unicode::is_word_byte;
use crate::nfa::{Nfa, NfaInstruction, StateId as NfaStateId};
pub type DfaStateId = u32;
pub const STRIDE: u32 = 256;
pub const TAG_MATCH: u32 = 1 << 30;
pub const TAG_DEAD: u32 = 1 << 31;
pub const TAG_MASK: u32 = TAG_MATCH | TAG_DEAD;
pub const STATE_MASK: u32 = !TAG_MASK;
pub const DEAD_STATE: u32 = TAG_DEAD | STATE_MASK;
pub const UNKNOWN_STATE: u32 = TAG_DEAD;
pub const DEFAULT_CACHE_LIMIT: usize = 10_000;
pub const CACHE_GROWTH_CEILING_FACTOR: usize = 4;
pub(crate) const SCAN_BUDGET_FACTOR: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CacheCeilingExceeded;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct PositionContext {
pub at_start_of_input: bool,
pub at_start_of_line: bool,
pub at_end_of_input: bool,
pub at_end_of_line: bool,
}
impl PositionContext {
pub fn start_of_input() -> Self {
Self {
at_start_of_input: true,
at_start_of_line: true,
at_end_of_input: false,
at_end_of_line: false,
}
}
pub fn middle() -> Self {
Self {
at_start_of_input: false,
at_start_of_line: false,
at_end_of_input: false,
at_end_of_line: false,
}
}
pub fn after_newline() -> Self {
Self {
at_start_of_input: false,
at_start_of_line: true,
at_end_of_input: false,
at_end_of_line: false,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum CharClass {
#[default]
NonWord = 0,
Word = 1,
}
impl CharClass {
#[inline]
pub fn from_byte(b: u8) -> Self {
if is_word_byte(b) {
CharClass::Word
} else {
CharClass::NonWord
}
}
}
pub type NfaSubset = Arc<[NfaStateId]>;
#[derive(Debug, Clone)]
pub(crate) struct ClosureScratch {
stamp: Vec<u32>,
generation: u32,
touched: Vec<NfaStateId>,
}
impl ClosureScratch {
pub(crate) fn new(state_count: usize) -> Self {
Self {
stamp: vec![0; state_count],
generation: 0,
touched: Vec::new(),
}
}
fn begin(&mut self) {
self.touched.clear();
self.generation = self.generation.wrapping_add(1);
if self.generation == 0 {
self.stamp.iter_mut().for_each(|slot| *slot = 0);
self.generation = 1;
}
}
fn mark(&mut self, id: NfaStateId) {
let generation = self.generation;
match self.stamp.get_mut(id as usize) {
Some(slot) if *slot == generation => return,
Some(slot) => *slot = generation,
None => {}
}
self.touched.push(id);
}
fn finish(&mut self) -> NfaSubset {
self.touched.sort_unstable();
self.touched.dedup();
Arc::from(self.touched.as_slice())
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum StateKey {
Simple(NfaSubset),
WithClass(NfaSubset, CharClass),
}
#[derive(Debug, Clone)]
pub struct DfaState {
pub is_match: bool,
pub nfa_states: NfaSubset,
pub prev_class: CharClass,
pub match_without_end_assertion: bool,
}
impl DfaState {
pub fn new(
nfa_states: NfaSubset,
is_match: bool,
prev_class: CharClass,
match_without_end_assertion: bool,
) -> Self {
Self {
is_match,
nfa_states,
prev_class,
match_without_end_assertion,
}
}
}
#[derive(Debug, Clone)]
pub struct LazyDfaContext {
pub(crate) nfa: Arc<Nfa>,
pub(crate) scratch: ClosureScratch,
pub(crate) states: Vec<DfaState>,
pub(crate) transitions: Vec<u32>,
pub(crate) transitions_unanchored: Vec<u32>,
pub(crate) last_reach: usize,
pub(crate) state_map: FxHashMap<StateKey, DfaStateId>,
pub(crate) start: DfaStateId,
pub(crate) cache_limit: usize,
pub(crate) flush_count: usize,
pub(crate) search_depth: u32,
pub(crate) ceiling_exceeded: bool,
pub(crate) has_word_boundary: bool,
pub(crate) has_anchors: bool,
pub(crate) has_start_anchor: bool,
pub(crate) has_end_anchor: bool,
pub(crate) has_multiline_anchors: bool,
pub(crate) has_multiline_start_anchor: bool,
pub(crate) has_clean_accept: bool,
#[cfg(test)]
pub(crate) per_byte_computations: usize,
#[cfg(test)]
pub(crate) context_run_computations: usize,
}
impl LazyDfaContext {
pub fn new(mut nfa: Nfa) -> Self {
nfa.precompute_epsilon_closures();
let has_word_boundary = nfa_has_word_boundary(&nfa);
let (
has_anchors,
has_start_anchor,
has_end_anchor,
has_multiline_anchors,
has_multiline_start_anchor,
) = nfa_anchor_info(&nfa);
let has_clean_accept = nfa.states.iter().any(|s| {
s.is_match
&& !matches!(
s.instruction,
Some(NfaInstruction::EndOfText) | Some(NfaInstruction::EndOfLine)
)
});
let scratch = ClosureScratch::new(nfa.states.len());
let mut ctx = Self {
nfa: Arc::new(nfa),
scratch,
states: Vec::new(),
transitions: Vec::new(),
transitions_unanchored: Vec::new(),
last_reach: 0,
state_map: FxHashMap::default(),
start: 0,
cache_limit: DEFAULT_CACHE_LIMIT,
flush_count: 0,
search_depth: 0,
ceiling_exceeded: false,
has_word_boundary,
has_anchors,
has_start_anchor,
has_end_anchor,
has_multiline_anchors,
has_multiline_start_anchor,
has_clean_accept,
#[cfg(test)]
per_byte_computations: 0,
#[cfg(test)]
context_run_computations: 0,
};
let start_seed = [ctx.nfa.start];
let is_at_boundary = None;
let start_closure = if has_word_boundary || has_anchors {
epsilon_closure_with_context(
&ctx.nfa,
&mut ctx.scratch,
&start_seed,
is_at_boundary,
Some(PositionContext::start_of_input()),
)
} else {
epsilon_closure_subset(&ctx.nfa, &mut ctx.scratch, &start_seed)
};
let start_clean = match_reachable_without_end_assertion(
&ctx.nfa,
&mut ctx.scratch,
&start_seed,
is_at_boundary,
Some(PositionContext::start_of_input()),
);
ctx.start = get_or_create_state_with_class(
&mut ctx,
start_closure,
CharClass::NonWord,
start_clean,
);
ctx
}
pub fn has_word_boundary(&self) -> bool {
self.has_word_boundary
}
pub fn has_anchors(&self) -> bool {
self.has_anchors
}
pub fn has_start_anchor(&self) -> bool {
self.has_start_anchor
}
pub fn has_end_anchor(&self) -> bool {
self.has_end_anchor
}
pub fn has_multiline_anchors(&self) -> bool {
self.has_multiline_anchors
}
pub fn has_multiline_start_anchor(&self) -> bool {
self.has_multiline_start_anchor
}
pub fn has_clean_accept(&self) -> bool {
self.has_clean_accept
}
pub fn start(&self) -> DfaStateId {
self.start
}
pub fn state_count(&self) -> usize {
self.states.len()
}
pub fn flush_count(&self) -> usize {
self.flush_count
}
pub fn set_cache_limit(&mut self, limit: usize) {
self.cache_limit = limit;
}
pub fn cache_ceiling(&self) -> usize {
self.cache_limit.saturating_mul(CACHE_GROWTH_CEILING_FACTOR)
}
pub(crate) fn nfa_arc(&self) -> Arc<Nfa> {
Arc::clone(&self.nfa)
}
}
pub fn nfa_has_word_boundary(nfa: &Nfa) -> bool {
nfa.states.iter().any(|state| {
matches!(
&state.instruction,
Some(NfaInstruction::WordBoundary) | Some(NfaInstruction::NotWordBoundary)
)
})
}
pub fn nfa_anchor_info(nfa: &Nfa) -> (bool, bool, bool, bool, bool) {
let mut has_start_anchor = false;
let mut has_end_anchor = false;
let mut has_multiline_anchors = false;
let mut has_multiline_start_anchor = false;
for state in &nfa.states {
match &state.instruction {
Some(NfaInstruction::StartOfText) => has_start_anchor = true,
Some(NfaInstruction::EndOfText) => has_end_anchor = true,
Some(NfaInstruction::StartOfLine) => {
has_start_anchor = true;
has_multiline_anchors = true;
has_multiline_start_anchor = true;
}
Some(NfaInstruction::EndOfLine) => {
has_end_anchor = true;
has_multiline_anchors = true;
}
_ => {}
}
}
let has_anchors = has_start_anchor || has_end_anchor;
(
has_anchors,
has_start_anchor,
has_end_anchor,
has_multiline_anchors,
has_multiline_start_anchor,
)
}
pub fn epsilon_closure_with_context<'a, I>(
nfa: &Nfa,
scratch: &mut ClosureScratch,
seeds: I,
is_at_boundary: Option<bool>,
pos_ctx: Option<PositionContext>,
) -> NfaSubset
where
I: IntoIterator<Item = &'a NfaStateId>,
{
scratch.begin();
for &seed in seeds {
scratch.mark(seed);
}
let mut cursor = 0usize;
while let Some(state_id) = scratch.touched.get(cursor).copied() {
cursor += 1;
let state = match nfa.get(state_id) {
Some(s) => s,
None => continue,
};
let should_follow_epsilons = match &state.instruction {
Some(NfaInstruction::WordBoundary) => match is_at_boundary {
Some(true) => true, Some(false) => false, None => false, },
Some(NfaInstruction::NotWordBoundary) => match is_at_boundary {
Some(false) => true, Some(true) => false, None => false, },
Some(NfaInstruction::StartOfText) => match pos_ctx {
Some(ctx) if ctx.at_start_of_input => true,
Some(_) => false,
None => false,
},
Some(NfaInstruction::StartOfLine) => match pos_ctx {
Some(ctx) if ctx.at_start_of_line => true,
Some(_) => false,
None => false,
},
Some(NfaInstruction::EndOfText) => true,
Some(NfaInstruction::EndOfLine) => true,
_ => true,
};
if should_follow_epsilons {
for &eps_target in &state.epsilon {
scratch.mark(eps_target);
}
}
}
scratch.finish()
}
pub fn epsilon_closure_subset<'a, I>(nfa: &Nfa, scratch: &mut ClosureScratch, seeds: I) -> NfaSubset
where
I: IntoIterator<Item = &'a NfaStateId>,
{
scratch.begin();
if let Some(precomputed) = nfa.epsilon_closures.as_ref() {
for &seed in seeds {
if let Some(state_closure) = precomputed.get(seed as usize) {
for &member in state_closure {
scratch.mark(member);
}
}
}
return scratch.finish();
}
for &seed in seeds {
scratch.mark(seed);
}
let mut cursor = 0usize;
while let Some(state_id) = scratch.touched.get(cursor).copied() {
cursor += 1;
if let Some(state) = nfa.get(state_id) {
for &next in &state.epsilon {
scratch.mark(next);
}
}
}
scratch.finish()
}
pub fn match_reachable_without_end_assertion<'a, I>(
nfa: &Nfa,
scratch: &mut ClosureScratch,
seeds: I,
is_at_boundary: Option<bool>,
pos_ctx: Option<PositionContext>,
) -> bool
where
I: IntoIterator<Item = &'a NfaStateId>,
{
scratch.begin();
for &seed in seeds {
scratch.mark(seed);
}
let mut cursor = 0usize;
while let Some(state_id) = scratch.touched.get(cursor).copied() {
cursor += 1;
let Some(state) = nfa.get(state_id) else {
continue;
};
if matches!(
state.instruction,
Some(NfaInstruction::EndOfText) | Some(NfaInstruction::EndOfLine)
) {
continue;
}
if state.is_match {
return true;
}
let follow = match &state.instruction {
Some(NfaInstruction::WordBoundary) => is_at_boundary == Some(true),
Some(NfaInstruction::NotWordBoundary) => is_at_boundary == Some(false),
Some(NfaInstruction::StartOfText) => pos_ctx.is_some_and(|ctx| ctx.at_start_of_input),
Some(NfaInstruction::StartOfLine) => pos_ctx.is_some_and(|ctx| ctx.at_start_of_line),
_ => true,
};
if follow {
for &target in &state.epsilon {
scratch.mark(target);
}
}
}
false
}
pub fn get_or_create_state_with_class(
ctx: &mut LazyDfaContext,
nfa_states: NfaSubset,
prev_class: CharClass,
match_without_end_assertion: bool,
) -> DfaStateId {
let key = if ctx.has_word_boundary {
StateKey::WithClass(nfa_states, prev_class)
} else {
StateKey::Simple(nfa_states)
};
if let Some(&id) = ctx.state_map.get(&key) {
return id;
}
let nfa_states = match key {
StateKey::WithClass(states, _) | StateKey::Simple(states) => states,
};
if ctx.states.len() >= ctx.cache_limit {
if ctx.search_depth == 0 {
flush_cache(ctx);
let reprobe_key = if ctx.has_word_boundary {
StateKey::WithClass(Arc::clone(&nfa_states), prev_class)
} else {
StateKey::Simple(Arc::clone(&nfa_states))
};
if let Some(&id) = ctx.state_map.get(&reprobe_key) {
return id;
}
} else if ctx.states.len() >= ctx.cache_ceiling() {
ctx.ceiling_exceeded = true;
return ctx.start;
}
}
let is_match = nfa_states
.iter()
.any(|&s| ctx.nfa.get(s).map(|state| state.is_match).unwrap_or(false));
let state_index = ctx.states.len();
let premul_id = (state_index as u32) * STRIDE;
let key = if ctx.has_word_boundary {
StateKey::WithClass(Arc::clone(&nfa_states), prev_class)
} else {
StateKey::Simple(Arc::clone(&nfa_states))
};
ctx.states.push(DfaState::new(
nfa_states,
is_match,
prev_class,
match_without_end_assertion,
));
ctx.transitions
.resize(ctx.transitions.len() + STRIDE as usize, UNKNOWN_STATE);
if !ctx.transitions_unanchored.is_empty() {
ctx.transitions_unanchored
.resize(ctx.transitions.len(), UNKNOWN_STATE);
}
ctx.state_map.insert(key, premul_id);
premul_id
}
pub fn flush_cache(ctx: &mut LazyDfaContext) {
let start_index = state_index(ctx.start);
let Some(start_state) = ctx.states.get(start_index).cloned() else {
return;
};
ctx.flush_count += 1;
ctx.states.clear();
ctx.transitions.clear();
ctx.transitions_unanchored.clear();
ctx.state_map.clear();
let key = if ctx.has_word_boundary {
StateKey::WithClass(Arc::clone(&start_state.nfa_states), start_state.prev_class)
} else {
StateKey::Simple(Arc::clone(&start_state.nfa_states))
};
ctx.states.push(start_state);
ctx.transitions.resize(STRIDE as usize, UNKNOWN_STATE);
ctx.state_map.insert(key, 0);
ctx.start = 0;
}
#[inline(always)]
pub fn state_index(premul_id: DfaStateId) -> usize {
((premul_id & STATE_MASK) / STRIDE) as usize
}
#[inline(always)]
pub fn tag_state(premul_id: DfaStateId, is_match: bool) -> u32 {
if is_match {
premul_id | TAG_MATCH
} else {
premul_id
}
}
#[inline(always)]
pub fn is_dead_state(tagged: u32) -> bool {
tagged == DEAD_STATE
}
#[inline(always)]
pub fn is_unknown_state(tagged: u32) -> bool {
tagged == UNKNOWN_STATE
}
#[inline(always)]
pub fn is_tagged_match(tagged: u32) -> bool {
(tagged & TAG_MATCH) != 0
}
#[inline(always)]
pub fn untag_state(tagged: u32) -> DfaStateId {
tagged & STATE_MASK
}