#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Segment {
pub file_idx: u32,
pub scan_start: u32,
pub emit_start: u32,
pub emit_end: u32,
}
pub const SEGMENT_WORDS: usize = 4;
impl Segment {
#[must_use]
pub const fn scan_len(&self) -> u32 {
self.emit_end - self.scan_start
}
#[must_use]
pub const fn emit_len(&self) -> u32 {
self.emit_end - self.emit_start
}
#[must_use]
pub const fn abi_words(&self) -> [u32; SEGMENT_WORDS] {
[
self.file_idx,
self.scan_start,
self.emit_start,
self.emit_end,
]
}
}
#[must_use]
pub fn segment_table(file_lens: &[u32], seg_len: u32, overlap: u32) -> Vec<u32> {
let segments = plan_segments(file_lens, seg_len, overlap);
let mut words = Vec::with_capacity(segments.len() * SEGMENT_WORDS);
for seg in &segments {
words.extend_from_slice(&seg.abi_words());
}
words
}
#[must_use]
pub fn plan_segments(file_lens: &[u32], seg_len: u32, overlap: u32) -> Vec<Segment> {
assert!(
seg_len > 0,
"segment owned-width (seg_len) must be positive"
);
let mut segments = Vec::new();
for (file_idx, &len) in file_lens.iter().enumerate() {
let file_idx = file_idx as u32;
let mut emit_start = 0u32;
while emit_start < len {
let emit_end = emit_start.saturating_add(seg_len).min(len);
let scan_start = emit_start.saturating_sub(overlap);
segments.push(Segment {
file_idx,
scan_start,
emit_start,
emit_end,
});
emit_start = emit_end;
}
}
segments
}
#[must_use]
pub fn segment_count(file_lens: &[u32], seg_len: u32) -> u64 {
assert!(
seg_len > 0,
"segment owned-width (seg_len) must be positive"
);
let seg_len = u64::from(seg_len);
file_lens
.iter()
.map(|&len| u64::from(len).div_ceil(seg_len))
.sum()
}
const DFA_BYTE_COLUMNS: usize = 256;
const PRODUCT_PAIR_BUDGET: usize = 1_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncClass {
Bounded(u32),
UnboundedCycle,
BudgetExceeded,
}
impl SyncClass {
#[must_use]
pub const fn bounded(self) -> Option<u32> {
match self {
Self::Bounded(d) => Some(d),
Self::UnboundedCycle | Self::BudgetExceeded => None,
}
}
}
#[must_use]
pub fn dfa_sync_distance(transitions: &[u32], state_count: u32) -> Option<u32> {
dfa_sync_class(transitions, state_count).bounded()
}
#[must_use]
pub fn dfa_sync_class(transitions: &[u32], state_count: u32) -> SyncClass {
let n = state_count as usize;
if n <= 1 {
return SyncClass::Bounded(0);
}
assert!(
transitions.len() >= n * DFA_BYTE_COLUMNS,
"transition table shorter than state_count * 256"
);
let delta = |s: usize, b: usize| -> usize {
let t = transitions[s * DFA_BYTE_COLUMNS + b] as usize;
if t < n { t } else { 0 }
};
let mut reachable = vec![false; n];
reachable[0] = true;
let mut stack = vec![0usize];
while let Some(s) = stack.pop() {
for b in 0..DFA_BYTE_COLUMNS {
let t = delta(s, b);
if !reachable[t] {
reachable[t] = true;
stack.push(t);
}
}
}
let key = |a: usize, b: usize| -> (usize, usize) { if a < b { (a, b) } else { (b, a) } };
use std::collections::HashMap;
let mut index: HashMap<(usize, usize), usize> = HashMap::new();
let mut off_succ: Vec<Vec<usize>> = Vec::new();
let mut frontier: Vec<(usize, usize)> = Vec::new();
let intern = |pair: (usize, usize),
index: &mut HashMap<(usize, usize), usize>,
off_succ: &mut Vec<Vec<usize>>,
frontier: &mut Vec<(usize, usize)>|
-> usize {
if let Some(&id) = index.get(&pair) {
return id;
}
let id = off_succ.len();
index.insert(pair, id);
off_succ.push(Vec::new());
frontier.push(pair);
id
};
for q in 0..n {
if reachable[q] && q != 0 {
intern(key(0, q), &mut index, &mut off_succ, &mut frontier);
}
}
let mut head = 0;
while head < frontier.len() {
if off_succ.len() > PRODUCT_PAIR_BUDGET {
return SyncClass::BudgetExceeded;
}
let (a, b) = frontier[head];
let id = head;
head += 1;
for byte in 0..DFA_BYTE_COLUMNS {
let na = delta(a, byte);
let nb = delta(b, byte);
if na == nb {
continue; }
let succ = intern(key(na, nb), &mut index, &mut off_succ, &mut frontier);
off_succ[id].push(succ);
}
}
for succs in &mut off_succ {
succs.sort_unstable();
succs.dedup();
}
let pair_count = off_succ.len();
if pair_count == 0 {
return SyncClass::Bounded(0);
}
let mut indegree = vec![0u32; pair_count];
for succs in &off_succ {
for &t in succs {
indegree[t] += 1;
}
}
let mut topo: Vec<usize> = Vec::new();
let mut queue: Vec<usize> = (0..pair_count).filter(|&p| indegree[p] == 0).collect();
while let Some(p) = queue.pop() {
topo.push(p);
for &t in &off_succ[p] {
indegree[t] -= 1;
if indegree[t] == 0 {
queue.push(t);
}
}
}
if topo.len() != pair_count {
return SyncClass::UnboundedCycle; }
let mut dist = vec![0u32; pair_count];
for &p in topo.iter().rev() {
let mut best = 0u32;
for &t in &off_succ[p] {
best = best.max(dist[t]);
}
dist[p] = 1 + best;
}
let mut sync = 0u32;
for q in 0..n {
if reachable[q] && q != 0 {
if let Some(&id) = index.get(&key(0, q)) {
sync = sync.max(dist[id]);
}
}
}
SyncClass::Bounded(sync)
}
#[must_use]
pub fn catalog_sync_overlap(rules: &[vyre_runtime::megakernel::BatchRuleProgram]) -> Option<u32> {
let mut overlap = 0u32;
for rule in rules {
let sync = dfa_sync_distance(&rule.transitions, rule.state_count)?;
overlap = overlap.max(sync);
}
Some(overlap)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::collections::BTreeSet;
use vyre_libs::scan::classic_ac::{classic_ac_compile, classic_ac_scan, ClassicAcAutomaton};
struct AcDfa {
goto: Vec<[i32; 256]>, fail: Vec<usize>,
out: Vec<Vec<usize>>, max_len: u32,
}
impl AcDfa {
fn build(patterns: &[&[u8]]) -> Self {
let mut goto = vec![[-1i32; 256]];
let mut out: Vec<Vec<usize>> = vec![Vec::new()];
let mut max_len = 0u32;
for (pid, pat) in patterns.iter().enumerate() {
max_len = max_len.max(pat.len() as u32);
let mut s = 0usize;
for &b in pat.iter() {
let nx = goto[s][b as usize];
if nx == -1 {
let new = goto.len();
goto.push([-1i32; 256]);
out.push(Vec::new());
goto[s][b as usize] = new as i32;
s = new;
} else {
s = nx as usize;
}
}
out[s].push(pid);
}
let mut fail = vec![0usize; goto.len()];
let mut queue = std::collections::VecDeque::new();
for b in 0..256 {
let t = goto[0][b];
if t > 0 {
fail[t as usize] = 0;
queue.push_back(t as usize);
} else if t == -1 {
goto[0][b] = 0;
}
}
while let Some(r) = queue.pop_front() {
for b in 0..256 {
let t = goto[r][b];
if t == -1 {
continue;
}
let t = t as usize;
queue.push_back(t);
let mut f = fail[r];
while goto[f][b] == -1 {
f = fail[f];
}
fail[t] = goto[f][b] as usize;
let merged = out[fail[t]].clone();
out[t].extend(merged);
}
}
AcDfa {
goto,
fail,
out,
max_len,
}
}
fn step(&self, mut state: usize, byte: u8) -> usize {
while self.goto[state][byte as usize] == -1 {
state = self.fail[state];
}
self.goto[state][byte as usize] as usize
}
}
fn dense_scan(dfa: &AcDfa, text: &[u8]) -> BTreeSet<(usize, usize)> {
let mut hits = BTreeSet::new();
let mut state = 0usize;
for (i, &b) in text.iter().enumerate() {
state = dfa.step(state, b);
for &pid in &dfa.out[state] {
hits.insert((pid, i + 1)); }
}
hits
}
fn segmented_scan(
dfa: &AcDfa,
text: &[u8],
seg_len: u32,
overlap: u32,
) -> BTreeSet<(usize, usize)> {
let mut hits = BTreeSet::new();
for seg in plan_segments(&[text.len() as u32], seg_len, overlap) {
let mut state = 0usize;
for i in seg.scan_start..seg.emit_end {
state = dfa.step(state, text[i as usize]);
let end = i + 1;
if end > seg.emit_start && end <= seg.emit_end {
for &pid in &dfa.out[state] {
hits.insert((pid, end as usize));
}
}
}
}
hits
}
fn materialize_dense(dfa: &AcDfa) -> (Vec<u32>, u32) {
let n = dfa.goto.len();
let mut transitions = vec![0u32; n * 256];
for s in 0..n {
for b in 0..256usize {
transitions[s * 256 + b] = dfa.step(s, b as u8) as u32;
}
}
(transitions, n as u32)
}
fn run_dense(transitions: &[u32], start: usize, w: &[u8]) -> usize {
let mut s = start;
for &b in w {
s = transitions[s * 256 + b as usize] as usize;
}
s
}
#[test]
fn sync_distance_single_byte_literal_is_one() {
let dfa = AcDfa::build(&[b"a"]);
let (transitions, n) = materialize_dense(&dfa);
assert_eq!(dfa_sync_distance(&transitions, n), Some(1));
}
#[test]
fn sync_distance_two_byte_literal_is_two() {
let dfa = AcDfa::build(&[b"ab"]);
let (transitions, n) = materialize_dense(&dfa);
assert_eq!(dfa_sync_distance(&transitions, n), Some(2));
}
#[test]
fn sync_distance_trivial_single_state_is_zero() {
let transitions = vec![0u32; 256];
assert_eq!(dfa_sync_distance(&transitions, 1), Some(0));
}
#[test]
fn sync_distance_unbounded_gap_pattern_is_none() {
let mut t = vec![0u32; 3 * 256];
for b in 0..256usize {
t[0 * 256 + b] = 0; t[1 * 256 + b] = 1; t[2 * 256 + b] = 2; }
t[0 * 256 + b'a' as usize] = 1;
t[1 * 256 + b'b' as usize] = 2;
assert_eq!(dfa_sync_distance(&t, 3), None);
}
#[test]
fn sync_distance_parity_dfa_is_none() {
let mut t = vec![0u32; 2 * 256];
for b in 0..256usize {
t[0 * 256 + b] = 0;
t[1 * 256 + b] = 1;
}
t[0 * 256 + b'a' as usize] = 1;
t[1 * 256 + b'a' as usize] = 0;
assert_eq!(dfa_sync_distance(&t, 2), None);
}
#[test]
fn sync_class_distinguishes_cycle_from_bounded() {
let mut gap = vec![0u32; 3 * 256];
for b in 0..256usize {
gap[b] = 0;
gap[256 + b] = 1;
gap[512 + b] = 2;
}
gap[b'a' as usize] = 1;
gap[256 + b'b' as usize] = 2;
assert_eq!(dfa_sync_class(&gap, 3), SyncClass::UnboundedCycle);
let dfa = AcDfa::build(&[b"ab"]);
let (transitions, n) = materialize_dense(&dfa);
assert_eq!(dfa_sync_class(&transitions, n), SyncClass::Bounded(2));
assert_eq!(dfa_sync_class(&transitions, n).bounded(), Some(2));
assert_eq!(dfa_sync_class(&vec![0u32; 256], 1), SyncClass::Bounded(0));
}
fn rule_from_patterns(
rule_idx: u32,
pats: &[&[u8]],
) -> vyre_runtime::megakernel::BatchRuleProgram {
let dfa = AcDfa::build(pats);
let (transitions, n) = materialize_dense(&dfa);
let mut accept = vec![0u32; n as usize];
for s in 0..n as usize {
if !dfa.out[s].is_empty() {
accept[s] = 1;
}
}
vyre_runtime::megakernel::BatchRuleProgram::new(rule_idx, transitions, accept, n)
.expect("materialized AC DFA is a valid rule program")
}
#[test]
fn catalog_sync_overlap_is_max_over_rules() {
let r_ab = rule_from_patterns(0, &[b"ab"]);
let r_abc = rule_from_patterns(1, &[b"abc"]);
assert_eq!(catalog_sync_overlap(&[r_ab, r_abc]), Some(3));
assert_eq!(catalog_sync_overlap(&[]), Some(0));
}
#[test]
fn catalog_sync_overlap_is_none_if_any_rule_unbounded() {
let r_ok = rule_from_patterns(0, &[b"ab"]);
let mut t_bad = vec![0u32; 3 * 256];
for b in 0..256usize {
t_bad[1 * 256 + b] = 1;
t_bad[2 * 256 + b] = 2;
}
t_bad[0 * 256 + b'a' as usize] = 1;
t_bad[1 * 256 + b'b' as usize] = 2;
let r_bad = vyre_runtime::megakernel::BatchRuleProgram::new(1, t_bad, vec![0, 0, 1], 3)
.expect("valid infinite-memory rule program");
assert_eq!(catalog_sync_overlap(&[r_ok, r_bad]), None);
}
proptest! {
#[test]
fn sync_distance_overlap_makes_segmentation_exact(
patterns in proptest::collection::vec(
proptest::collection::vec(b'a'..=b'd', 1..=6), 1..=4),
text in proptest::collection::vec(b'a'..=b'd', 0..400),
seg_len in 1u32..64,
) {
let pat_refs: Vec<&[u8]> = patterns.iter().map(|p| p.as_slice()).collect();
let dfa = AcDfa::build(&pat_refs);
let (transitions, n) = materialize_dense(&dfa);
let sync = dfa_sync_distance(&transitions, n)
.expect("a bounded-literal AC DFA has finite memory");
prop_assert!(sync <= dfa.max_len, "sync {} > max_len {}", sync, dfa.max_len);
prop_assert_eq!(
segmented_scan(&dfa, &text, seg_len, sync),
dense_scan(&dfa, &text),
"overlap = sync_distance({}) must make seg_len={} exact", sync, seg_len
);
}
#[test]
fn sync_distance_converges_from_every_reachable_start(
patterns in proptest::collection::vec(
proptest::collection::vec(b'a'..=b'c', 1..=5), 1..=3),
tail in proptest::collection::vec(b'a'..=b'c', 0..6),
prefix in proptest::collection::vec(b'a'..=b'c', 0..8),
) {
let pat_refs: Vec<&[u8]> = patterns.iter().map(|p| p.as_slice()).collect();
let dfa = AcDfa::build(&pat_refs);
let (transitions, n) = materialize_dense(&dfa);
let sync = dfa_sync_distance(&transitions, n).expect("finite memory") as usize;
let q = run_dense(&transitions, 0, &prefix);
let mut w = tail.clone();
while w.len() < sync {
w.push(b'a');
}
prop_assert_eq!(
run_dense(&transitions, 0, &w),
run_dense(&transitions, q, &w),
"states diverged after sync={} common bytes (|w|={})", sync, w.len()
);
}
}
#[test]
fn segmented_scan_matches_dense_on_a_known_case() {
let dfa = AcDfa::build(&[b"aws", b"key", b"secret"]);
let text = b"my aws key is secret and the aws secret key follows";
assert_eq!(
segmented_scan(&dfa, text, 4, 6),
dense_scan(&dfa, text),
"segmented scan must equal dense scan with adequate warm-up"
);
}
#[test]
fn tiles_a_single_file_contiguously() {
let segs = plan_segments(&[1000], 256, 64);
assert_eq!(segs.len(), 4);
assert_eq!(
segs,
vec![
Segment {
file_idx: 0,
scan_start: 0,
emit_start: 0,
emit_end: 256
},
Segment {
file_idx: 0,
scan_start: 192,
emit_start: 256,
emit_end: 512
},
Segment {
file_idx: 0,
scan_start: 448,
emit_start: 512,
emit_end: 768
},
Segment {
file_idx: 0,
scan_start: 704,
emit_start: 768,
emit_end: 1000
},
]
);
assert_eq!(segs[0].scan_len(), 256); assert_eq!(segs[1].scan_len(), 256 + 64);
}
#[test]
fn short_file_is_one_window_covering_everything() {
let segs = plan_segments(&[100], 512, 64);
assert_eq!(
segs,
vec![Segment {
file_idx: 0,
scan_start: 0,
emit_start: 0,
emit_end: 100
}]
);
}
#[test]
fn zero_length_file_yields_no_window() {
assert!(plan_segments(&[0], 256, 64).is_empty());
let segs = plan_segments(&[10, 0, 10], 256, 0);
assert_eq!(
segs.iter().map(|s| s.file_idx).collect::<Vec<_>>(),
vec![0, 2]
);
}
#[test]
fn overlap_zero_means_scan_equals_emit() {
let segs = plan_segments(&[800], 256, 0);
for s in &segs {
assert_eq!(s.scan_start, s.emit_start);
assert_eq!(s.scan_len(), s.emit_len());
}
}
#[test]
fn segment_count_matches_planned_len() {
let lens = [0u32, 1, 255, 256, 257, 4096, 8 * 1024 * 1024];
assert_eq!(
segment_count(&lens, 512),
plan_segments(&lens, 512, 64).len() as u64
);
}
#[test]
fn segment_table_flattens_planned_segments_in_order() {
let lens = [1000u32, 100];
let segs = plan_segments(&lens, 256, 64);
let table = segment_table(&lens, 256, 64);
assert_eq!(table.len(), segs.len() * SEGMENT_WORDS);
for (i, seg) in segs.iter().enumerate() {
let row = &table[i * SEGMENT_WORDS..(i + 1) * SEGMENT_WORDS];
assert_eq!(row, seg.abi_words(), "segment {i} ABI words mismatch");
assert_eq!(row[0], seg.file_idx);
assert_eq!(row[1], seg.scan_start);
assert_eq!(row[2], seg.emit_start);
assert_eq!(row[3], seg.emit_end);
}
}
#[test]
fn segment_table_first_row_is_file0_offset0() {
let table = segment_table(&[4096], 512, 64);
assert_eq!(&table[..SEGMENT_WORDS], &[0, 0, 0, 512]);
}
proptest! {
#[test]
fn segmented_scan_equals_dense_with_adequate_overlap(
patterns in proptest::collection::vec(
proptest::collection::vec(b'a'..=b'd', 1..=6), 1..=4),
text in proptest::collection::vec(b'a'..=b'd', 0..400),
seg_len in 1u32..64,
extra_overlap in 0u32..8,
) {
let pat_refs: Vec<&[u8]> = patterns.iter().map(|p| p.as_slice()).collect();
let dfa = AcDfa::build(&pat_refs);
let overlap = dfa.max_len + extra_overlap;
prop_assert_eq!(
segmented_scan(&dfa, &text, seg_len, overlap),
dense_scan(&dfa, &text),
"segmented (seg_len={}, overlap={}) != dense", seg_len, overlap
);
}
#[test]
fn segment_table_rows_are_in_bounds(
lens in proptest::collection::vec(0u32..4000, 0..5),
seg_len in 1u32..512,
overlap in 0u32..128,
) {
let table = segment_table(&lens, seg_len, overlap);
prop_assert_eq!(table.len() % SEGMENT_WORDS, 0);
for row in table.chunks_exact(SEGMENT_WORDS) {
let (file_idx, scan_start, emit_start, emit_end) =
(row[0], row[1], row[2], row[3]);
let file_len = lens[file_idx as usize];
prop_assert!(scan_start <= emit_start);
prop_assert!(emit_start < emit_end);
prop_assert!(emit_end <= file_len, "row reads past file end");
}
}
}
proptest! {
#[test]
fn emit_ranges_tile_each_file_exactly(
lens in proptest::collection::vec(0u32..5000, 0..6),
seg_len in 1u32..1024,
overlap in 0u32..256,
) {
let segs = plan_segments(&lens, seg_len, overlap);
for (file_idx, &len) in lens.iter().enumerate() {
let fsegs: Vec<&Segment> =
segs.iter().filter(|s| s.file_idx == file_idx as u32).collect();
if len == 0 {
prop_assert!(fsegs.is_empty(), "zero-length file must yield no window");
continue;
}
prop_assert_eq!(fsegs.first().unwrap().emit_start, 0, "first window must own offset 0");
prop_assert_eq!(fsegs.last().unwrap().emit_end, len, "last window must reach file end");
let mut cursor = 0u32;
for s in &fsegs {
prop_assert_eq!(s.emit_start, cursor, "gap or overlap between windows");
prop_assert!(s.emit_end > s.emit_start, "empty owned range");
prop_assert!(s.emit_end <= len, "window owns past file end");
prop_assert_eq!(s.scan_start, s.emit_start.saturating_sub(overlap));
prop_assert!(s.scan_start <= s.emit_start, "warm-up cannot start after owned region");
prop_assert!(s.scan_len() >= s.emit_len(), "scan must cover the owned region");
cursor = s.emit_end;
}
prop_assert_eq!(cursor, len, "emit ranges must cover [0,len) with no remainder");
prop_assert_eq!(fsegs.len() as u32, len.div_ceil(seg_len));
}
}
#[test]
fn every_offset_owned_exactly_once(
len in 1u32..600,
seg_len in 1u32..200,
overlap in 0u32..128,
) {
let segs = plan_segments(&[len], seg_len, overlap);
for pos in 0..len {
let owners = segs
.iter()
.filter(|s| pos >= s.emit_start && pos < s.emit_end)
.count();
prop_assert_eq!(owners, 1, "offset {} owned by {} windows, expected 1", pos, owners);
}
}
}
fn combined_segmented_scan(
ac: &ClassicAcAutomaton,
text: &[u8],
seg_len: u32,
overlap: u32,
) -> BTreeSet<(u32, u32)> {
let dfa = &ac.dfa;
let mut hits = BTreeSet::new();
for seg in plan_segments(&[text.len() as u32], seg_len, overlap) {
let mut state = 0u32;
for i in seg.scan_start..seg.emit_end {
state = dfa.transitions[(state as usize) * DFA_BYTE_COLUMNS + text[i as usize] as usize];
if i >= seg.emit_start {
let begin = dfa.output_offsets[state as usize] as usize;
let end = dfa.output_offsets[state as usize + 1] as usize;
for &pattern_id in &dfa.output_records[begin..end] {
hits.insert((pattern_id, i));
}
}
}
}
hits
}
#[test]
fn combined_segmented_equals_linear_on_overlapping_patterns() {
let ac = classic_ac_compile(&[b"he", b"she", b"his", b"hers"]);
let text = b"ushers";
let overlap = ac.dfa.max_pattern_len; let linear: BTreeSet<(u32, u32)> = classic_ac_scan(&ac, text).into_iter().collect();
assert!(linear.contains(&(1, 3)), "linear must contain she@3: {linear:?}");
assert!(linear.contains(&(0, 3)), "linear must contain he@3: {linear:?}");
assert!(linear.contains(&(3, 5)), "linear must contain hers@5: {linear:?}");
for seg_len in 1u32..=8 {
let segmented = combined_segmented_scan(&ac, text, seg_len, overlap);
assert_eq!(
segmented, linear,
"combined segmented (seg_len={seg_len}, overlap={overlap}) != linear classic_ac_scan"
);
}
}
proptest! {
#[test]
fn combined_segmented_equals_linear_classic_ac_scan(
patterns in proptest::collection::vec(
proptest::collection::vec(b'a'..=b'd', 1..=6), 1..=5),
text in proptest::collection::vec(b'a'..=b'd', 0..400),
seg_len in 1u32..64,
extra_overlap in 0u32..8,
) {
let pat_refs: Vec<&[u8]> = patterns.iter().map(|p| p.as_slice()).collect();
let ac = classic_ac_compile(&pat_refs);
let overlap = ac.dfa.max_pattern_len + extra_overlap;
let linear: BTreeSet<(u32, u32)> = classic_ac_scan(&ac, &text).into_iter().collect();
let segmented = combined_segmented_scan(&ac, &text, seg_len, overlap);
prop_assert_eq!(
segmented, linear,
"combined segmented (seg_len={}, overlap={}) != linear", seg_len, overlap
);
}
}
}