use std::{error::Error, fmt};
#[derive(Debug, Clone)]
pub struct CompiledDfa {
pub transitions: Vec<u32>,
pub accept: Vec<u32>,
pub state_count: u32,
pub max_pattern_len: u32,
pub output_offsets: Vec<u32>,
pub output_records: Vec<u32>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DfaCompileError {
TooLarge {
requested_bytes: usize,
budget_bytes: usize,
state_count: u32,
},
TrieStateCapExceeded {
state_cap: usize,
},
}
impl fmt::Display for DfaCompileError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooLarge {
requested_bytes,
budget_bytes,
..
} => write!(
formatter,
"DFA transition table is too large: {requested_bytes} bytes (cap = {budget_bytes}). Fix: reduce the pattern set, raise the budget, or shard patterns into multiple DFAs."
),
Self::TrieStateCapExceeded { state_cap } => write!(
formatter,
"DFA trie exceeded state cap during construction: requested > {state_cap} states. Fix: reduce the pattern set or raise the budget (cap derived from budget_bytes / 1024)."
),
}
}
}
impl Error for DfaCompileError {}
const DFA_WIRE_MAGIC: [u8; 4] = *b"VDFA";
const DFA_WIRE_VERSION: u32 = 2;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DfaWireError {
Truncated {
needed: usize,
got: usize,
},
BadMagic,
VersionMismatch {
expected: u32,
found: u32,
},
ShapeMismatch {
reason: &'static str,
},
SectionTooLarge {
len: usize,
max: usize,
},
Envelope(String),
}
impl fmt::Display for DfaWireError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Truncated { needed, got } => write!(
f,
"DFA wire blob truncated: needed {needed} bytes, got {got}. \
Fix: regenerate the cache."
),
Self::BadMagic => write!(
f,
"DFA wire blob does not start with `VDFA` magic. Fix: this \
is not a CompiledDfa::to_bytes payload."
),
Self::VersionMismatch { expected, found } => write!(
f,
"DFA wire blob version {found} does not match the runtime \
version {expected}. Fix: discard the cache and recompile \
the DFA."
),
Self::ShapeMismatch { reason } => write!(
f,
"DFA wire blob shape mismatch: {reason}. Fix: this blob is \
corrupt - discard and recompile."
),
Self::SectionTooLarge { len, max } => write!(
f,
"DFA wire section length {len} exceeds maximum {max}. \
Fix: shard the DFA into smaller pattern groups."
),
Self::Envelope(message) => write!(f, "DFA wire envelope error: {message}"),
}
}
}
impl Error for DfaWireError {}
impl CompiledDfa {
#[must_use]
pub fn empty() -> Self {
Self {
transitions: vec![0; 256],
accept: vec![0],
state_count: 1,
max_pattern_len: 0,
output_offsets: vec![0, 0],
output_records: Vec::new(),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, DfaWireError> {
let mut out = vyre_foundation::serial::WireWriter::new(&DFA_WIRE_MAGIC, DFA_WIRE_VERSION);
out.write_u32(self.state_count);
out.write_u32(self.max_pattern_len);
out.write_words(&self.transitions)
.map_err(map_envelope_error)?;
out.write_words(&self.accept).map_err(map_envelope_error)?;
out.write_words(&self.output_offsets)
.map_err(map_envelope_error)?;
out.write_words(&self.output_records)
.map_err(map_envelope_error)?;
Ok(out.into_bytes())
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, DfaWireError> {
let mut reader =
vyre_foundation::serial::WireReader::new(bytes, &DFA_WIRE_MAGIC, DFA_WIRE_VERSION)
.map_err(map_envelope_error)?;
let state_count = reader.read_u32().map_err(map_envelope_error)?;
let max_pattern_len = reader.read_u32().map_err(map_envelope_error)?;
let transitions = reader.read_words().map_err(map_envelope_error)?;
let accept = reader.read_words().map_err(map_envelope_error)?;
let output_offsets = reader.read_words().map_err(map_envelope_error)?;
let output_records = reader.read_words().map_err(map_envelope_error)?;
if transitions.len() != (state_count as usize) * 256 {
return Err(DfaWireError::ShapeMismatch {
reason: "transitions length != state_count * 256",
});
}
if transitions
.iter()
.any(|&target| target as usize >= state_count as usize)
{
return Err(DfaWireError::ShapeMismatch {
reason: "transition target out of range for state_count",
});
}
if accept.len() != state_count as usize {
return Err(DfaWireError::ShapeMismatch {
reason: "accept length != state_count",
});
}
if output_offsets.len() != (state_count as usize) + 1 {
return Err(DfaWireError::ShapeMismatch {
reason: "output_offsets length != state_count + 1",
});
}
if output_offsets.first().copied() != Some(0) {
return Err(DfaWireError::ShapeMismatch {
reason: "output_offsets must start at zero",
});
}
if output_offsets.last().copied() != Some(output_records.len() as u32) {
return Err(DfaWireError::ShapeMismatch {
reason: "output_offsets last entry must equal output_records length",
});
}
if output_offsets
.windows(2)
.any(|window| window[0] > window[1])
{
return Err(DfaWireError::ShapeMismatch {
reason: "output_offsets must be monotonic",
});
}
if output_offsets
.iter()
.any(|&offset| offset as usize > output_records.len())
{
return Err(DfaWireError::ShapeMismatch {
reason: "output_offsets entries must be within output_records",
});
}
if max_pattern_len == 0 && accept.iter().skip(1).any(|&state| state != 0) {
return Err(DfaWireError::ShapeMismatch {
reason: "max_pattern_len == 0 but a non-root state accepts",
});
}
Ok(Self {
transitions,
accept,
state_count,
max_pattern_len,
output_offsets,
output_records,
})
}
}
fn map_envelope_error(error: vyre_foundation::serial::EnvelopeError) -> DfaWireError {
match error {
vyre_foundation::serial::EnvelopeError::Truncated { needed, got } => {
DfaWireError::Truncated { needed, got }
}
vyre_foundation::serial::EnvelopeError::BadMagic { .. } => DfaWireError::BadMagic,
vyre_foundation::serial::EnvelopeError::VersionMismatch { expected, found } => {
DfaWireError::VersionMismatch { expected, found }
}
vyre_foundation::serial::EnvelopeError::SectionTooLarge { len, max } => {
DfaWireError::SectionTooLarge { len, max }
}
error => DfaWireError::Envelope(error.to_string()),
}
}
pub const DEFAULT_DFA_BUDGET_BYTES: usize = 16 * 1024 * 1024;
#[must_use]
pub fn dfa_compile(patterns: &[&[u8]]) -> CompiledDfa {
match dfa_compile_with_budget(patterns, DEFAULT_DFA_BUDGET_BYTES) {
Ok(dfa) => dfa,
Err(error) => panic!(
"dfa_compile: compiling {} pattern(s) exceeded the default {DEFAULT_DFA_BUDGET_BYTES}-byte DFA budget ({error}). \
Returning the empty rejecting automaton would silently drop every match; \
use dfa_compile_with_budget and shard oversized pattern sets to handle this as a structured error.",
patterns.len()
),
}
}
pub fn dfa_compile_with_budget(
patterns: &[&[u8]],
budget_bytes: usize,
) -> Result<CompiledDfa, DfaCompileError> {
dfa_compile_with_budget_ci(patterns, budget_bytes, false)
}
#[must_use]
pub fn dfa_compile_case_insensitive(patterns: &[&[u8]]) -> CompiledDfa {
match dfa_compile_case_insensitive_with_budget(patterns, DEFAULT_DFA_BUDGET_BYTES) {
Ok(dfa) => dfa,
Err(error) => panic!(
"dfa_compile_case_insensitive: compiling {} pattern(s) exceeded the default {DEFAULT_DFA_BUDGET_BYTES}-byte DFA budget ({error}). \
Returning the empty rejecting automaton would silently drop every match; \
use dfa_compile_case_insensitive_with_budget and shard oversized pattern sets to handle this as a structured error.",
patterns.len()
),
}
}
pub fn dfa_compile_case_insensitive_with_budget(
patterns: &[&[u8]],
budget_bytes: usize,
) -> Result<CompiledDfa, DfaCompileError> {
dfa_compile_with_budget_ci(patterns, budget_bytes, true)
}
fn dfa_compile_with_budget_ci(
patterns: &[&[u8]],
budget_bytes: usize,
case_insensitive: bool,
) -> Result<CompiledDfa, DfaCompileError> {
let state_cap = budget_bytes / (256 * core::mem::size_of::<u32>());
let inner = dfa_compile_inner_capped(patterns, state_cap, case_insensitive)?;
let requested_bytes = (inner.state_count as usize)
.saturating_mul(256)
.saturating_mul(core::mem::size_of::<u32>());
if requested_bytes > budget_bytes {
return Err(DfaCompileError::TooLarge {
requested_bytes,
budget_bytes,
state_count: inner.state_count,
});
}
Ok(inner)
}
#[inline]
fn fold_ascii_byte(b: usize, case_insensitive: bool) -> usize {
if case_insensitive && (0x41..=0x5A).contains(&b) {
b | 0x20
} else {
b
}
}
fn dfa_compile_inner_capped(
patterns: &[&[u8]],
state_cap: usize,
case_insensitive: bool,
) -> Result<CompiledDfa, DfaCompileError> {
const NO_TRANSITION: u32 = u32::MAX;
let upper_bound = patterns
.iter()
.fold(0usize, |acc, p| acc.saturating_add(p.len()))
.saturating_add(1);
let max_pattern_len = patterns
.iter()
.map(|pattern| pattern.len())
.max()
.unwrap_or(0)
.min(u32::MAX as usize) as u32;
let trie_capacity = state_cap.min(upper_bound).max(1);
let mut trie: Vec<[u32; 256]> = Vec::with_capacity(trie_capacity);
let mut accept: Vec<u32> = Vec::with_capacity(trie_capacity);
let mut local_accepts: Vec<Vec<u32>> = Vec::with_capacity(trie_capacity);
trie.push([NO_TRANSITION; 256]);
accept.push(0);
local_accepts.push(Vec::new());
for (pattern_idx, pat) in patterns.iter().enumerate() {
let mut cur = 0usize;
for &b in *pat {
let b = fold_ascii_byte(b as usize, case_insensitive);
let next = trie[cur][b];
if next != NO_TRANSITION {
cur = next as usize;
} else {
if trie.len() >= state_cap {
return Err(DfaCompileError::TrieStateCapExceeded { state_cap });
}
let new_id = trie.len() as u32;
trie.push([NO_TRANSITION; 256]);
accept.push(0);
local_accepts.push(Vec::new());
trie[cur][b] = new_id;
cur = new_id as usize;
}
}
local_accepts[cur].push(pattern_idx as u32);
if accept[cur] == 0 {
accept[cur] = (pattern_idx as u32)
.checked_add(1)
.expect("pattern_idx must be <= u32::MAX - 1 to fit the pid+1 encoding");
}
}
let state_count = trie.len();
let mut fail = vec![0u32; state_count];
let mut queue = Vec::new();
for b in 0..256usize {
let child = trie[0][b];
if child != NO_TRANSITION {
fail[child as usize] = 0;
queue.push(child as usize);
}
}
let mut head = 0usize;
while head < queue.len() {
let state = queue[head];
head += 1;
for b in 0..256usize {
let child = trie[state][b];
if child != NO_TRANSITION {
let mut f = fail[state] as usize;
while f != 0 && trie[f][b] == NO_TRANSITION {
f = fail[f] as usize;
}
let f_child = trie[f][b];
if f_child != NO_TRANSITION && f_child != child {
fail[child as usize] = f_child;
}
if accept[child as usize] == 0 {
let f_accept = accept[fail[child as usize] as usize];
if f_accept != 0 {
accept[child as usize] = f_accept;
}
}
queue.push(child as usize);
}
}
}
let mut bfs_order = Vec::with_capacity(state_count);
let mut bfs_queue = Vec::with_capacity(state_count);
bfs_queue.push(0usize);
let mut bfs_head = 0usize;
while bfs_head < bfs_queue.len() {
let state = bfs_queue[bfs_head];
bfs_head += 1;
bfs_order.push(state);
for b in 0..256usize {
let child = trie[state][b];
if child != NO_TRANSITION {
bfs_queue.push(child as usize);
}
}
}
let mut output_counts = vec![0usize; state_count];
for &state in &bfs_order {
let f = fail[state] as usize;
let inherited = if f != 0 && f != state {
output_counts[f]
} else {
0
};
let adds_local = local_accepts[state]
.iter()
.filter(|&&pattern| !fail_chain_accepts_pattern(state, pattern, &fail, &local_accepts))
.count();
output_counts[state] = inherited + adds_local;
}
let mut output_offsets = vec![0u32; state_count + 1];
for state in 0..state_count {
output_offsets[state + 1] =
output_offsets[state].saturating_add(output_counts[state] as u32);
}
let mut output_records = vec![0u32; output_offsets[state_count] as usize];
for &state in &bfs_order {
let mut write = output_offsets[state] as usize;
let f = fail[state] as usize;
if f != 0 && f != state {
let start = output_offsets[f] as usize;
let end = output_offsets[f + 1] as usize;
let len = end - start;
output_records.copy_within(start..end, write);
write += len;
}
for &pattern in &local_accepts[state] {
let start = output_offsets[state] as usize;
if !output_records[start..write].contains(&pattern) {
output_records[write] = pattern;
write += 1;
}
}
debug_assert_eq!(write, output_offsets[state + 1] as usize);
}
let mut transitions = vec![0u32; state_count * 256];
let mut accept_out = vec![0u32; state_count];
for state in 0..state_count {
accept_out[state] = accept[state];
for b in 0..256usize {
let fb = fold_ascii_byte(b, case_insensitive);
let mut s = state;
loop {
let child = trie[s][fb];
if child != NO_TRANSITION {
transitions[state * 256 + b] = child;
break;
}
if s == 0 {
transitions[state * 256 + b] = 0;
break;
}
s = fail[s] as usize;
}
}
}
Ok(CompiledDfa {
transitions,
accept: accept_out,
state_count: state_count as u32,
max_pattern_len,
output_offsets,
output_records,
})
}
fn fail_chain_accepts_pattern(
state: usize,
pattern: u32,
fail: &[u32],
local_accepts: &[Vec<u32>],
) -> bool {
let mut f = fail[state] as usize;
while f != 0 && f != state {
if local_accepts[f].contains(&pattern) {
return true;
}
let next = fail[f] as usize;
if next == f {
return false;
}
f = next;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_string_matches_only_its_suffix() {
let dfa = dfa_compile(&[b"abc"]);
let input = b"xxabcxx";
let mut s = 0usize;
for &b in b"xxabc" {
s = dfa.transitions[s * 256 + b as usize] as usize;
}
assert_eq!(
dfa.accept[s], 1,
"after 'xxabc' the DFA must be in a state that accepts pattern 0 (encoded as 1); \
got accept[{s}] = {}",
dfa.accept[s]
);
let rec_start = dfa.output_offsets[s] as usize;
let rec_end = dfa.output_offsets[s + 1] as usize;
assert_eq!(
&dfa.output_records[rec_start..rec_end],
&[0u32],
"output_records for the accept state must contain exactly [0] (pid=0)"
);
let s_after_x = dfa.transitions[s * 256 + b'x' as usize] as usize;
assert_eq!(
dfa.accept[s_after_x], 0,
"after trailing 'x' the DFA must not accept; pattern 'abc' ends before it"
);
}
fn scan_ends(dfa: &CompiledDfa, haystack: &[u8]) -> std::collections::BTreeSet<(u32, u32)> {
let mut state = 0usize;
let mut out = std::collections::BTreeSet::new();
for (pos, &b) in haystack.iter().enumerate() {
state = dfa.transitions[state * 256 + b as usize] as usize;
let begin = dfa.output_offsets[state] as usize;
let end = dfa.output_offsets[state + 1] as usize;
for &pid in &dfa.output_records[begin..end] {
out.insert((pid, pos as u32));
}
}
out
}
#[test]
fn case_insensitive_matches_every_case_variant() {
let dfa = dfa_compile_case_insensitive(&[b"key"]);
for variant in [b"KEY", b"Key", b"kEy", b"keY", b"kEY", b"key"] {
let hits = scan_ends(&dfa, variant);
assert!(
hits.contains(&(0, 2)),
"case-insensitive DFA must match {:?} as pattern 0 ending at 2, got {hits:?}",
std::str::from_utf8(variant).unwrap()
);
}
assert!(
scan_ends(&dfa, b"kez").is_empty(),
"case-insensitive folding must not match a non-variant string"
);
}
#[test]
fn case_insensitive_is_identical_to_host_folded_case_sensitive() {
let alphabet = b"aAbBkK_9/";
let mut seed = 0x9E37_79B1u64;
let mut next = || {
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
(seed >> 33) as u32
};
for _ in 0..500 {
let pat_count = 1 + (next() % 4) as usize;
let patterns_owned: Vec<Vec<u8>> = (0..pat_count)
.map(|_| {
let len = 1 + (next() % 5) as usize;
(0..len)
.map(|_| alphabet[(next() as usize) % alphabet.len()])
.collect()
})
.collect();
let patterns: Vec<&[u8]> = patterns_owned.iter().map(Vec::as_slice).collect();
let hay_len = 4 + (next() % 40) as usize;
let haystack: Vec<u8> = (0..hay_len)
.map(|_| alphabet[(next() as usize) % alphabet.len()])
.collect();
let ci = dfa_compile_case_insensitive(&patterns);
let ci_hits = scan_ends(&ci, &haystack);
let lowered_pat: Vec<Vec<u8>> = patterns_owned
.iter()
.map(|p| p.iter().map(|b| b.to_ascii_lowercase()).collect())
.collect();
let lowered_refs: Vec<&[u8]> = lowered_pat.iter().map(Vec::as_slice).collect();
let lowered_hay: Vec<u8> = haystack.iter().map(|b| b.to_ascii_lowercase()).collect();
let reference = dfa_compile(&lowered_refs);
let ref_hits = scan_ends(&reference, &lowered_hay);
assert_eq!(
ci_hits, ref_hits,
"case-insensitive DFA over raw haystack must equal host-folded case-sensitive scan\n\
patterns={patterns_owned:?}\n\
haystack={:?}",
String::from_utf8_lossy(&haystack)
);
}
}
#[test]
fn overlapping_patterns_both_accept() {
let patterns: [&[u8]; 4] = [b"he", b"she", b"his", b"hers"];
let dfa = dfa_compile(&patterns);
let mut state = 0u32;
let mut matches = Vec::new();
for &b in b"ushers" {
state = dfa.transitions[(state as usize) * 256 + (b as usize)];
let accept = dfa.accept[state as usize];
if accept != 0 {
matches.push(accept - 1);
}
}
assert!(matches.contains(&1), "must accept `she`");
assert!(
matches.contains(&0) || matches.contains(&3),
"must accept `he` or `hers`"
);
}
#[test]
fn duplicate_literals_preserve_distinct_output_records() {
let dfa = dfa_compile(&[b"B".as_slice(), b"B".as_slice(), b"AB".as_slice()]);
let state_b = dfa.transitions[b'B' as usize] as usize;
let state_ab = {
let state_a = dfa.transitions[b'A' as usize] as usize;
dfa.transitions[state_a * 256 + b'B' as usize] as usize
};
let b_start = dfa.output_offsets[state_b] as usize;
let b_end = dfa.output_offsets[state_b + 1] as usize;
assert_eq!(
&dfa.output_records[b_start..b_end],
&[0, 1],
"Fix: exact duplicate literals must keep both consumer pattern ids in output_records."
);
let ab_start = dfa.output_offsets[state_ab] as usize;
let ab_end = dfa.output_offsets[state_ab + 1] as usize;
assert_eq!(
&dfa.output_records[ab_start..ab_end],
&[0, 1, 2],
"Fix: suffix inheritance must preserve duplicate suffix pattern ids plus the local longer pattern."
);
}
#[test]
fn empty_pattern_list_yields_trivial_dfa() {
let dfa = dfa_compile(&[]);
assert_eq!(dfa.state_count, 1);
assert_eq!(dfa.transitions.len(), 256);
assert!(dfa.transitions.iter().all(|&t| t == 0));
assert_eq!(dfa.accept, vec![0]);
}
#[test]
fn budget_exhaustion_returns_structured_error() {
let err = dfa_compile_with_budget(&[b"ab", b"cd"], 1024).unwrap_err();
match err {
DfaCompileError::TooLarge {
requested_bytes,
budget_bytes,
state_count,
} => {
assert!(
requested_bytes > budget_bytes,
"TooLarge must carry requested > budget"
);
assert_eq!(budget_bytes, 1024);
assert!(state_count >= 1);
}
DfaCompileError::TrieStateCapExceeded { state_cap } => {
assert!(state_cap <= 1024);
}
}
}
#[test]
fn generous_budget_succeeds() {
let dfa = dfa_compile_with_budget(&[b"abc"], DEFAULT_DFA_BUDGET_BYTES)
.expect("Fix: generous budget must succeed; restore this invariant before continuing.");
assert!(dfa.state_count >= 1);
}
#[test]
fn zero_budget_rejects_every_nonempty_dfa() {
let err = dfa_compile_with_budget(&[b"a"], 0).unwrap_err();
assert!(matches!(
err,
DfaCompileError::TooLarge { .. } | DfaCompileError::TrieStateCapExceeded { .. }
));
}
#[test]
fn empty_pattern_dfa_round_trips() {
let dfa = dfa_compile(&[b"".as_slice()]);
assert_eq!(
dfa.accept[0], 1,
"dfa_compile(&[b\"\"]) root state must accept pattern 0 (accept=1)"
);
assert_eq!(
dfa.max_pattern_len, 0,
"empty pattern must produce max_pattern_len=0"
);
let bytes = dfa
.to_bytes()
.expect("Fix: serialization must succeed for empty-pattern DFA");
let dfa2 = CompiledDfa::from_bytes(&bytes)
.expect("Fix: round-trip must succeed for empty-pattern DFA");
assert_eq!(
dfa2.accept[0], 1,
"deserialized DFA must preserve accept[0]=1 for empty-pattern compile"
);
assert_eq!(
dfa2.max_pattern_len, 0,
"deserialized DFA must preserve max_pattern_len=0"
);
}
#[test]
fn from_bytes_rejects_zero_max_pattern_len_with_non_root_accept() {
let mut dfa = dfa_compile(&[b"AKIA".as_slice()]);
assert!(
dfa.max_pattern_len >= 1,
"precondition: AKIA must compile to max_pattern_len >= 1, got {}",
dfa.max_pattern_len
);
assert!(
dfa.accept.iter().skip(1).any(|&state| state != 0),
"precondition: AKIA must have a non-root accept state"
);
dfa.max_pattern_len = 0;
let bytes = dfa.to_bytes().expect("encode forged DFA wire blob");
let err = CompiledDfa::from_bytes(&bytes).unwrap_err();
assert!(
matches!(
err,
DfaWireError::ShapeMismatch {
reason: "max_pattern_len == 0 but a non-root state accepts"
}
),
"expected ShapeMismatch with the non-root-accept reason, got {err:?}"
);
}
#[test]
fn from_bytes_rejects_out_of_range_transition_target() {
let mut dfa = dfa_compile(&[b"abc".as_slice()]);
assert!(
dfa.state_count >= 2,
"precondition: fixture must have real states"
);
assert!(
dfa.transitions
.iter()
.all(|&t| (t as usize) < dfa.state_count as usize),
"precondition: an honest compile keeps every transition target in range"
);
dfa.transitions[0] = dfa.state_count;
let bytes = dfa.to_bytes().expect("encode forged DFA wire blob");
let err = CompiledDfa::from_bytes(&bytes).unwrap_err();
assert!(
matches!(
err,
DfaWireError::ShapeMismatch {
reason: "transition target out of range for state_count"
}
),
"expected the transition-target range violation, got {err:?}"
);
}
#[test]
fn duplicate_literal_accept_field_contains_first_pattern() {
let dfa = dfa_compile(&[b"B".as_slice(), b"B".as_slice()]);
let state_b = dfa.transitions[b'B' as usize] as usize;
assert_eq!(
dfa.accept[state_b],
1,
"first duplicate literal (pid=0) must win the accept fast-path field (encoded as pid+1=1); \
last-writer-wins would give 2 (pid=1)"
);
let start = dfa.output_offsets[state_b] as usize;
let end = dfa.output_offsets[state_b + 1] as usize;
assert_eq!(
&dfa.output_records[start..end],
&[0u32, 1u32],
"duplicate literals must both appear in output_records"
);
}
#[test]
fn infallible_compile_does_not_silently_return_empty_on_error() {
let src = std::fs::read_to_string(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/matching/dfa_compile.rs"
))
.expect("Fix: DFA compiler source must be readable");
let production = src
.split("#[cfg(test)]")
.next()
.expect("Fix: meta-test scans production sources; update fixture path if module moved - production section must exist");
assert!(
!production.contains("unwrap_or_else(|_| CompiledDfa::empty())"),
"dfa_compile must never hide a failed compile by returning the empty rejecting automaton"
);
assert!(
production.contains("use dfa_compile_with_budget and shard oversized pattern sets"),
"dfa_compile panic must explain the structured recovery path"
);
}
}