use super::alloc::reserve_vec;
#[derive(Debug, Clone)]
pub struct NfaPlan {
pub num_states: u32,
pub input_len: u32,
pub accept_states: Vec<(u32, u32)>,
pub accept_state_ids: Vec<u32>,
pub accept_start_anchored: Vec<bool>,
pub accept_end_anchored: Vec<bool>,
}
impl NfaPlan {
#[must_use]
pub fn for_input_len(mut self, input_len: u32) -> Self {
self.input_len = input_len;
self
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum NfaCompileError {
PatternCountOverflow {
count: usize,
},
PatternLengthOverflow {
pattern_index: usize,
len: usize,
},
StateCountOverflow,
TableWordCountOverflow {
table: &'static str,
},
StorageReserveFailed {
field: &'static str,
requested: usize,
message: String,
},
}
impl std::fmt::Display for NfaCompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PatternCountOverflow { count } => write!(
f,
"NFA pattern count {count} exceeds u32 capacity. Fix: shard the pattern set before NFA compilation."
),
Self::PatternLengthOverflow { pattern_index, len } => write!(
f,
"NFA pattern {pattern_index} length {len} exceeds u32 capacity. Fix: split or reject oversized literals before NFA compilation."
),
Self::StateCountOverflow => write!(
f,
"NFA state count overflows u32. Fix: use plan_shards to split the pattern set before compilation."
),
Self::TableWordCountOverflow { table } => write!(
f,
"NFA {table} table word count overflows host usize. Fix: shard the pattern set before table construction."
),
Self::StorageReserveFailed {
field,
requested,
message,
} => write!(
f,
"NFA compilation could not reserve {requested} {field} slot(s): {message}. Fix: shard the pattern set before compilation."
),
}
}
}
impl std::error::Error for NfaCompileError {}
#[must_use]
pub fn compile(patterns: &[&str]) -> NfaPlan {
match try_compile(patterns) {
Ok(plan) => plan,
Err(error) => {
eprintln!("vyre-libs NFA compile failed: {error}");
empty_plan()
}
}
}
pub fn try_compile(patterns: &[&str]) -> Result<NfaPlan, NfaCompileError> {
let _pattern_count =
u32::try_from(patterns.len()).map_err(|_| NfaCompileError::PatternCountOverflow {
count: patterns.len(),
})?;
let mut accept_states = Vec::new();
reserve_vec(&mut accept_states, patterns.len(), "accept state")?;
let mut accept_state_ids = Vec::new();
reserve_vec(&mut accept_state_ids, patterns.len(), "accept state id")?;
let mut accept_start_anchored = Vec::new();
reserve_vec(
&mut accept_start_anchored,
patterns.len(),
"accept start-anchor flag",
)?;
accept_start_anchored.resize(patterns.len(), false);
let mut accept_end_anchored = Vec::new();
reserve_vec(
&mut accept_end_anchored,
patterns.len(),
"accept end-anchor flag",
)?;
accept_end_anchored.resize(patterns.len(), false);
let mut next_state: u32 = 1;
for (pid, p) in patterns.iter().enumerate() {
let pid = u32::try_from(pid).map_err(|_| NfaCompileError::PatternCountOverflow {
count: patterns.len(),
})?;
let len = u32::try_from(p.len()).map_err(|_| NfaCompileError::PatternLengthOverflow {
pattern_index: pid as usize,
len: p.len(),
})?;
let accept_state_id = if len == 0 {
0
} else {
next_state
.checked_add(len)
.and_then(|value| value.checked_sub(1))
.ok_or(NfaCompileError::StateCountOverflow)?
};
accept_states.push((pid, len));
accept_state_ids.push(accept_state_id);
next_state = next_state
.checked_add(len)
.ok_or(NfaCompileError::StateCountOverflow)?;
}
Ok(NfaPlan {
num_states: next_state,
input_len: 0,
accept_states,
accept_state_ids,
accept_start_anchored,
accept_end_anchored,
})
}
fn empty_plan() -> NfaPlan {
NfaPlan {
num_states: 1,
input_len: 0,
accept_states: Vec::new(),
accept_state_ids: Vec::new(),
accept_start_anchored: Vec::new(),
accept_end_anchored: Vec::new(),
}
}
#[cfg(test)]
mod tests {
use super::{compile, empty_plan, try_compile};
#[test]
fn compile_empty_patterns_returns_real_entry_state() {
let plan = compile(&[]);
assert_eq!(plan.num_states, 1);
assert!(plan.accept_states.is_empty());
assert!(plan.accept_state_ids.is_empty());
}
#[test]
fn empty_plan_matches_try_compile_empty_contract() {
let fallible = try_compile(&[]).expect("Fix: empty NFA compile must fit ABI");
let fallback = empty_plan();
assert_eq!(fallback.num_states, fallible.num_states);
assert_eq!(fallback.accept_states, fallible.accept_states);
assert_eq!(fallback.accept_state_ids, fallible.accept_state_ids);
}
#[test]
fn production_compile_wrapper_has_no_raw_panic_path() {
let production = include_str!("plan.rs")
.split("#[cfg(test)]")
.next()
.expect("Fix: plan.rs must contain production section");
assert!(
!production.contains(".expect(") && !production.contains(".unwrap("),
"Fix: NFA compile compatibility wrapper must not panic in production."
);
}
}