use vyre::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
use vyre_foundation::match_result::Match;
use vyre_primitives::matching::CompiledDfa;
use crate::region::wrap_anonymous;
use crate::scan::builders::{append_match, load_packed_byte};
#[derive(Debug, Clone, Copy)]
pub struct AnchoredWindowValidator<'dfa> {
dfa: &'dfa CompiledDfa,
dead_state: Option<u32>,
}
impl<'dfa> AnchoredWindowValidator<'dfa> {
#[must_use]
pub fn new(dfa: &'dfa CompiledDfa) -> Self {
Self {
dead_state: detect_dead_state(dfa),
dfa,
}
}
#[must_use]
pub fn window_len(&self) -> u32 {
self.dfa.max_pattern_len
}
pub fn validate_candidate(&self, haystack: &[u8], origin: u32, out: &mut Vec<Match>) {
let origin_idx = origin as usize;
if origin_idx >= haystack.len() {
return;
}
let window = (self.dfa.max_pattern_len as usize).min(haystack.len() - origin_idx);
let mut state = 0u32;
for step in 0..window {
let byte = haystack[origin_idx + step];
state = self.dfa.transitions[state as usize * 256 + byte as usize];
if Some(state) == self.dead_state {
break;
}
let end = origin + step as u32 + 1;
let lo = self.dfa.output_offsets[state as usize] as usize;
let hi = self.dfa.output_offsets[state as usize + 1] as usize;
for &pattern_id in &self.dfa.output_records[lo..hi] {
out.push(Match::new(pattern_id, origin, end));
}
}
}
#[must_use]
pub fn validate_candidates(&self, haystack: &[u8], origins: &[u32]) -> Vec<Match> {
let mut matches = Vec::new();
for &origin in origins {
self.validate_candidate(haystack, origin, &mut matches);
}
matches.sort_unstable_by_key(|m| (m.start, m.end, m.pattern_id));
matches.dedup();
matches
}
pub fn validate_candidate_leftmost_longest(
&self,
haystack: &[u8],
origin: u32,
out: &mut Vec<Match>,
) {
let origin_idx = origin as usize;
if origin_idx >= haystack.len() {
return;
}
let window = (self.dfa.max_pattern_len as usize).min(haystack.len() - origin_idx);
let mut state = 0u32;
let mut longest: Vec<(u32, u32)> = Vec::new();
for step in 0..window {
let byte = haystack[origin_idx + step];
state = self.dfa.transitions[state as usize * 256 + byte as usize];
if Some(state) == self.dead_state {
break;
}
let end = origin + step as u32 + 1;
let lo = self.dfa.output_offsets[state as usize] as usize;
let hi = self.dfa.output_offsets[state as usize + 1] as usize;
for &pattern_id in &self.dfa.output_records[lo..hi] {
match longest.iter_mut().find(|(pid, _)| *pid == pattern_id) {
Some(slot) => slot.1 = end,
None => longest.push((pattern_id, end)),
}
}
}
for (pattern_id, end) in longest {
out.push(Match::new(pattern_id, origin, end));
}
}
#[must_use]
pub fn validate_candidates_leftmost_longest(
&self,
haystack: &[u8],
origins: &[u32],
) -> Vec<Match> {
let mut matches = Vec::new();
for &origin in origins {
self.validate_candidate_leftmost_longest(haystack, origin, &mut matches);
}
matches.sort_unstable_by_key(|m| (m.start, m.end, m.pattern_id));
matches.dedup();
matches
}
}
fn detect_dead_state(dfa: &CompiledDfa) -> Option<u32> {
for state in 0..dfa.state_count {
let s = state as usize;
if dfa.accept[s] != 0 {
continue;
}
if dfa.output_offsets[s] != dfa.output_offsets[s + 1] {
continue;
}
let base = s * 256;
if dfa.transitions[base..base + 256]
.iter()
.all(|&next| next == state)
{
return Some(state);
}
}
None
}
pub const ANCHORED_WINDOW_MATCH_COUNT_BINDING: u32 = 7;
pub const ANCHORED_WINDOW_MATCHES_BINDING: u32 = 8;
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn anchored_window_extract_program(
haystack: &str,
transitions: &str,
output_offsets: &str,
output_records: &str,
candidates: &str,
candidate_count: &str,
haystack_len: &str,
match_count: &str,
matches: &str,
state_count: u32,
output_records_len: u32,
max_candidates: u32,
max_matches: u32,
max_pattern_len: u32,
) -> Program {
let max_pattern_len = max_pattern_len.max(1);
let (load_step_byte, step_byte) = load_packed_byte(haystack, Expr::var("step"));
let walk_step = vec![
load_step_byte,
Node::assign(
"state",
Expr::load(
transitions,
Expr::add(Expr::mul(Expr::var("state"), Expr::u32(256)), step_byte),
),
),
Node::let_bind("out_begin", Expr::load(output_offsets, Expr::var("state"))),
Node::let_bind(
"out_end",
Expr::load(output_offsets, Expr::add(Expr::var("state"), Expr::u32(1))),
),
Node::loop_for(
"out_idx",
Expr::var("out_begin"),
Expr::var("out_end"),
vec![
Node::let_bind(
"pattern_id",
Expr::load(output_records, Expr::var("out_idx")),
),
append_match(
matches,
match_count,
Expr::var("pattern_id"),
Expr::var("origin"),
Expr::add(Expr::var("step"), Expr::u32(1)),
),
],
),
];
let uncapped_end = Expr::add(Expr::var("origin"), Expr::u32(max_pattern_len));
let window_end = Expr::select(
Expr::lt(uncapped_end.clone(), Expr::load(haystack_len, Expr::u32(0))),
uncapped_end,
Expr::load(haystack_len, Expr::u32(0)),
);
let per_candidate = vec![
Node::let_bind("origin", Expr::load(candidates, Expr::var("i"))),
Node::if_then(
Expr::lt(Expr::var("origin"), Expr::load(haystack_len, Expr::u32(0))),
vec![
Node::let_bind("state", Expr::u32(0)),
Node::let_bind("win_end", window_end),
Node::loop_for("step", Expr::var("origin"), Expr::var("win_end"), walk_step),
],
),
];
let walk_body = vec![
Node::let_bind("i", Expr::InvocationId { axis: 0 }),
Node::if_then(
Expr::lt(Expr::var("i"), Expr::load(candidate_count, Expr::u32(0))),
per_candidate,
),
];
Program::wrapped(
vec![
BufferDecl::storage(haystack, 0, BufferAccess::ReadOnly, DataType::U32),
BufferDecl::storage(transitions, 1, BufferAccess::ReadOnly, DataType::U32)
.with_count(state_count.saturating_mul(256)),
BufferDecl::storage(output_offsets, 2, BufferAccess::ReadOnly, DataType::U32)
.with_count(state_count.saturating_add(1)),
BufferDecl::storage(output_records, 3, BufferAccess::ReadOnly, DataType::U32)
.with_count(output_records_len),
BufferDecl::storage(candidates, 4, BufferAccess::ReadOnly, DataType::U32)
.with_count(max_candidates),
BufferDecl::storage(candidate_count, 5, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::storage(haystack_len, 6, BufferAccess::ReadOnly, DataType::U32)
.with_count(1),
BufferDecl::read_write(
match_count,
ANCHORED_WINDOW_MATCH_COUNT_BINDING,
DataType::U32,
)
.with_count(1),
BufferDecl::output(matches, ANCHORED_WINDOW_MATCHES_BINDING, DataType::U32)
.with_count(max_matches.saturating_mul(3)),
],
[128, 1, 1],
vec![wrap_anonymous(
"vyre-libs::matching::regex_anchored_window",
walk_body,
)],
)
}
#[cfg(all(test, feature = "matching-regex", feature = "matching-dfa"))]
mod tests {
use super::*;
use crate::scan::regex_dfa::build_regex_dfa_pipeline;
const MAX_MATCHES: u32 = 4096;
const MAX_DFA_STATES: usize = 16_384;
fn validator_for<'p>(patterns: &[&str]) -> CompiledDfa {
build_regex_dfa_pipeline(patterns, MAX_MATCHES, MAX_DFA_STATES)
.expect("Fix: test patterns must compile to an anchored regex DFA")
.dfa
}
#[test]
fn matches_only_at_exact_candidate_origin() {
let dfa = validator_for(&["abc"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"..abc..";
assert_eq!(
validator.validate_candidates(haystack, &[2]),
vec![Match::new(0, 2, 5)],
"candidate at the true start must extract the match with start==origin"
);
assert!(
validator.validate_candidates(haystack, &[1]).is_empty(),
"candidate one byte before the match start must NOT match (anchored, not unanchored)"
);
assert!(
validator.validate_candidates(haystack, &[3]).is_empty(),
"candidate one byte after the match start must NOT match"
);
}
#[test]
fn short_match_does_not_re_accept_deeper_in_window() {
let dfa = validator_for(&["abc"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"abcabcabc";
assert_eq!(
validator.validate_candidates(haystack, &[0]),
vec![Match::new(0, 0, 3)],
"only the anchored match at the origin may surface; later 'abc's start at other origins"
);
}
#[test]
fn shared_prefix_patterns_emit_every_accept_length_at_one_origin() {
let dfa = validator_for(&["abc", "abcde"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"abcde";
let got = validator.validate_candidates(haystack, &[0]);
assert_eq!(
got,
vec![Match::new(0, 0, 3), Match::new(1, 0, 5)],
"both the length-3 and length-5 pattern must extract at the shared origin"
);
}
#[test]
fn bounded_repetition_pattern_extracts_faithful_anchored_matches() {
let dfa = validator_for(&["a{2,4}"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"aaaaa";
let origins: Vec<u32> = (0..haystack.len() as u32).collect();
let got = validator.validate_candidates(haystack, &origins);
let expected = direct_walk_all_origins(&dfa, haystack);
assert_eq!(
got, expected,
"validator must extract exactly what a direct walk of the same DFA accepts"
);
assert!(
!got.is_empty(),
"a bounded repetition anchored at a matching origin must extract at least one match"
);
assert!(
got.iter()
.all(|m| haystack[m.start as usize..m.end as usize]
.iter()
.all(|&b| b == b'a')),
"every anchored-window match must be a real run of the repeated byte"
);
assert_eq!(
dfa.max_pattern_len, 4,
"the {{n,m}} lowering must size the window to the MAX repetition (4), \
not the min (2), so the windowed walk can reach the longer accepts"
);
let origin0_ends: Vec<u32> = got
.iter()
.filter(|m| m.start == 0)
.map(|m| m.end - m.start)
.collect();
assert_eq!(
origin0_ends,
vec![2, 3, 4],
"raw fan-out must now surface every admissible {{2,4}} length at origin 0"
);
assert_eq!(
validator.validate_candidates_leftmost_longest(haystack, &[0]),
vec![Match::new(0, 0, 4)],
"leftmost-longest must emit exactly the longest {{2,4}} match at origin 0"
);
}
fn direct_walk_all_origins(dfa: &CompiledDfa, haystack: &[u8]) -> Vec<Match> {
let mut out = Vec::new();
for origin in 0..haystack.len() {
let window = (dfa.max_pattern_len as usize).min(haystack.len() - origin);
let mut state = 0u32;
for step in 0..window {
state = dfa.transitions[state as usize * 256 + haystack[origin + step] as usize];
let lo = dfa.output_offsets[state as usize] as usize;
let hi = dfa.output_offsets[state as usize + 1] as usize;
for &pid in &dfa.output_records[lo..hi] {
out.push(Match::new(pid, origin as u32, (origin + step + 1) as u32));
}
}
}
out.sort_unstable_by_key(|m| (m.start, m.end, m.pattern_id));
out.dedup();
out
}
#[test]
fn distinct_patterns_extract_at_their_own_origins() {
let dfa = validator_for(&["abc", "bcd"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"abcd";
assert_eq!(
validator.validate_candidates(haystack, &[0, 1]),
vec![Match::new(0, 0, 3), Match::new(1, 1, 4)],
"each pattern extracts at the origin where it starts"
);
}
#[test]
fn origins_at_and_past_eof_are_safe_and_windows_truncate() {
let dfa = validator_for(&["abcd"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"xxabc"; assert!(
validator.validate_candidates(haystack, &[2]).is_empty(),
"a pattern that runs off the end of the haystack must not match"
);
assert!(
validator
.validate_candidates(haystack, &[haystack.len() as u32])
.is_empty(),
"origin == haystack.len() must be ignored, not indexed"
);
assert!(
validator
.validate_candidates(haystack, &[haystack.len() as u32 + 9])
.is_empty(),
"origin past EOF must be ignored"
);
}
#[test]
fn duplicate_origins_collapse_to_a_set() {
let dfa = validator_for(&["abc"]);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"abc";
assert_eq!(
validator.validate_candidates(haystack, &[0, 0, 0]),
vec![Match::new(0, 0, 3)],
"repeated origins must not duplicate the extracted match"
);
}
#[test]
fn empty_candidate_batch_is_empty() {
let dfa = validator_for(&["abc"]);
let validator = AnchoredWindowValidator::new(&dfa);
assert!(validator.validate_candidates(b"abcabc", &[]).is_empty());
}
fn with_reference_dispatch_lanes(program: Program, lanes: u32) -> Program {
let buffers = program
.buffers()
.iter()
.cloned()
.map(|buffer| {
if buffer.name() == "match_count" {
buffer.with_count(lanes.max(1)).with_output_byte_range(0..4)
} else {
buffer
}
})
.collect();
program.with_rewritten_buffers(buffers)
}
fn decode_match_triples(outputs: &[vyre_reference::value::Value]) -> Vec<(u32, u32, u32)> {
let words = |value: &vyre_reference::value::Value| -> Vec<u32> {
value
.to_bytes()
.chunks_exact(4)
.map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
};
let count = words(&outputs[0])[0] as usize;
let matches = words(&outputs[1]);
matches[..count.saturating_mul(3)]
.chunks_exact(3)
.map(|chunk| (chunk[0], chunk[1], chunk[2]))
.collect()
}
#[test]
fn extract_program_reference_eval_matches_cpu_oracle() {
use crate::scan::{pack_haystack_u32, pack_u32_slice};
let patterns = ["abc", "abcde", "bcd", "x"];
let dfa = validator_for(&patterns);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"zabcdex bcd abc x abcde";
let candidates: Vec<u32> = vec![0, 1, 2, 8, 12, 16, 18, haystack.len() as u32 - 1];
let mut expected = validator.validate_candidates(haystack, &candidates);
expected.sort_unstable_by_key(|m| (m.start, m.end, m.pattern_id));
let num_candidates = candidates.len() as u32;
let max_matches = 4096u32;
let program = with_reference_dispatch_lanes(
anchored_window_extract_program(
"haystack",
"transitions",
"output_offsets",
"output_records",
"candidates",
"candidate_count",
"haystack_len",
"match_count",
"matches",
dfa.state_count,
dfa.output_records.len() as u32,
num_candidates,
max_matches,
dfa.max_pattern_len,
),
num_candidates,
);
let inputs = vec![
vyre_reference::value::Value::from(pack_haystack_u32(haystack)),
vyre_reference::value::Value::from(pack_u32_slice(&dfa.transitions)),
vyre_reference::value::Value::from(pack_u32_slice(&dfa.output_offsets)),
vyre_reference::value::Value::from(pack_u32_slice(&dfa.output_records)),
vyre_reference::value::Value::from(pack_u32_slice(&candidates)),
vyre_reference::value::Value::from(pack_u32_slice(&[num_candidates])),
vyre_reference::value::Value::from(pack_u32_slice(&[haystack.len() as u32])),
vyre_reference::value::Value::from(vec![0_u8; num_candidates as usize * 4]),
];
let outputs = vyre_reference::reference_eval(&program, &inputs)
.expect("Fix: anchored-window extract program must evaluate in the reference backend");
let mut actual: Vec<Match> = decode_match_triples(&outputs)
.into_iter()
.map(|(pid, start, end)| Match::new(pid, start, end))
.collect();
actual.sort_unstable_by_key(|m| (m.start, m.end, m.pattern_id));
actual.dedup();
assert_eq!(
actual, expected,
"reference-eval of the anchored-window program must equal the CPU oracle's extraction"
);
assert!(
!expected.is_empty(),
"parity test is vacuous: the oracle extracted no matches for these candidates"
);
}
#[test]
fn differential_vs_naive_anchored_substring_oracle() {
let patterns = ["ab", "abc", "bcx", "x", "cab"];
let dfa = validator_for(&patterns);
let validator = AnchoredWindowValidator::new(&dfa);
let alphabet = b"abcx";
let mut state: u32 = 0x1234_5678;
let mut haystack = Vec::with_capacity(600);
for _ in 0..600 {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
haystack.push(alphabet[(state >> 24) as usize % alphabet.len()]);
}
let origins: Vec<u32> = (0..haystack.len() as u32).collect();
let got = validator.validate_candidates(&haystack, &origins);
let mut oracle: Vec<Match> = Vec::new();
for (pid, pat) in patterns.iter().enumerate() {
let pb = pat.as_bytes();
if pb.len() <= haystack.len() {
for start in 0..=haystack.len() - pb.len() {
if &haystack[start..start + pb.len()] == pb {
oracle.push(Match::new(
pid as u32,
start as u32,
(start + pb.len()) as u32,
));
}
}
}
}
oracle.sort_unstable_by_key(|m| (m.start, m.end, m.pattern_id));
oracle.dedup();
assert_eq!(
got, oracle,
"anchored-window extraction must equal the naive anchored-substring oracle at every position"
);
assert!(
oracle.len() > 50,
"differential is vacuous: oracle found only {} matches",
oracle.len()
);
let distinct_pids: std::collections::BTreeSet<u32> =
oracle.iter().map(|m| m.pattern_id).collect();
assert!(
distinct_pids.len() >= 4,
"differential should exercise most patterns; saw pids {distinct_pids:?}"
);
}
#[test]
fn dead_state_early_out_equals_full_window_walk() {
let patterns = ["abc", "abcde", "bx"];
let dfa = validator_for(&patterns);
let validator = AnchoredWindowValidator::new(&dfa);
let haystack = b"abcdefabxabc";
let origins: Vec<u32> = (0..haystack.len() as u32).collect();
let optimized = validator.validate_candidates(haystack, &origins);
let full = direct_walk_all_origins(&dfa, haystack);
assert_eq!(
optimized, full,
"dead-state early-out must be a pure optimization, identical extraction to the full walk"
);
}
#[test]
fn detected_dead_state_is_non_start_non_accepting_self_loop() {
let dfa = validator_for(&["abc"]);
let dead =
detect_dead_state(&dfa).expect("an anchored DFA with a rejecting path has a dead sink");
assert_ne!(dead, 0, "the start state must not be classified as dead");
assert_eq!(dfa.accept[dead as usize], 0, "dead state must not accept");
for byte in 0..=255u16 {
assert_eq!(
dfa.transitions[dead as usize * 256 + byte as usize],
dead,
"dead state must self-loop on every byte"
);
}
}
}