use regex_syntax::hir::{Class, Hir, HirKind, Look, Repetition};
use crate::scan::nfa::NfaPlan;
const LANES: usize = vyre_primitives::nfa::subgroup_nfa::LANES_PER_SUBGROUP;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CaptureMode {
NonCapture,
Count,
Span,
NamedCapture,
RepeatedCapture,
GroupExtraction,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CaptureModeContract {
pub mode_id: &'static str,
pub output_shape: &'static str,
pub accelerator_eligible: bool,
pub verifier_required: bool,
pub null_policy: &'static str,
}
impl CaptureMode {
pub const ALL: [CaptureMode; 6] = [
CaptureMode::NonCapture,
CaptureMode::Count,
CaptureMode::Span,
CaptureMode::NamedCapture,
CaptureMode::RepeatedCapture,
CaptureMode::GroupExtraction,
];
#[must_use]
pub const fn contract_row(self) -> CaptureModeContract {
match self {
CaptureMode::NonCapture => CaptureModeContract {
mode_id: "noncapture",
output_shape: "whole_match_only",
accelerator_eligible: true,
verifier_required: false,
null_policy: "not_applicable",
},
CaptureMode::Count => CaptureModeContract {
mode_id: "count",
output_shape: "match_count_per_pattern",
accelerator_eligible: true,
verifier_required: false,
null_policy: "not_applicable",
},
CaptureMode::Span => CaptureModeContract {
mode_id: "span",
output_shape: "whole_match_span",
accelerator_eligible: true,
verifier_required: false,
null_policy: "absent-match-has-no-span",
},
CaptureMode::NamedCapture => CaptureModeContract {
mode_id: "named_capture",
output_shape: "named_group_span_records",
accelerator_eligible: false,
verifier_required: true,
null_policy: "unmatched-group-null",
},
CaptureMode::RepeatedCapture => CaptureModeContract {
mode_id: "repeated_capture",
output_shape: "ordered_group_span_list",
accelerator_eligible: false,
verifier_required: true,
null_policy: "empty-repeat-yields-empty-list",
},
CaptureMode::GroupExtraction => CaptureModeContract {
mode_id: "group_extraction",
output_shape: "row_group_value_table",
accelerator_eligible: false,
verifier_required: true,
null_policy: "unmatched-group-null",
},
}
}
#[must_use]
pub const fn accelerator_eligible(self) -> bool {
self.contract_row().accelerator_eligible
}
#[must_use]
pub const fn verifier_required(self) -> bool {
self.contract_row().verifier_required
}
#[must_use]
pub fn from_mode_id(mode_id: &str) -> Option<CaptureMode> {
CaptureMode::ALL
.into_iter()
.find(|mode| mode.contract_row().mode_id == mode_id)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum RegexCompileError {
Parse {
pattern_index: usize,
message: String,
},
Unsupported {
pattern_index: usize,
feature: &'static str,
},
TooManyStates {
states: usize,
cap: usize,
},
PatternCountOverflow {
count: usize,
},
MatchLengthOverflow {
pattern_index: usize,
len: usize,
},
TableWordCountOverflow {
table: &'static str,
},
StorageReserveFailed {
field: &'static str,
requested: usize,
message: String,
},
}
impl RegexCompileError {
#[must_use]
pub fn diagnostic_code(&self) -> Option<&'static str> {
match self {
Self::Unsupported { feature, .. } => {
regex_feature_construct(feature).map(regex_construct_diagnostic_code)
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum RegexConstruct {
Backreference,
Lookaround,
UnicodeClassesGpu,
CaptureExtraction,
HugeAlternation,
NestedRepeats,
}
#[must_use]
pub fn regex_construct_diagnostic_code(construct: RegexConstruct) -> &'static str {
match construct {
RegexConstruct::Backreference => "VYRE_SCAN_UNSUPPORTED_BACKREFERENCE",
RegexConstruct::Lookaround => "VYRE_SCAN_APPROXIMATED_LOOKAROUND_REQUIRES_VERIFIER",
RegexConstruct::UnicodeClassesGpu => "VYRE_SCAN_UNSUPPORTED_UNICODE_MODE_GPU",
RegexConstruct::CaptureExtraction => "VYRE_SCAN_CAPTURE_EXTRACTION_REQUIRES_VERIFIER",
RegexConstruct::HugeAlternation => "VYRE_SCAN_UNSUPPORTED_HUGE_ALTERNATION_BUDGET",
RegexConstruct::NestedRepeats => "VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET",
}
}
const FEATURE_LOOKAROUND: &str = "non-edge lookaround assertion";
const FEATURE_UNICODE_CLASS_CAP: &str = "unicode character class exceeded expansion cap";
const FEATURE_BACKREFERENCE: &str = "backreference";
const FEATURE_HUGE_ALTERNATION: &str = "huge alternation exceeds budget";
const FEATURE_NESTED_REPEATS: &str = "nested repeat exceeds budget";
fn regex_feature_construct(feature: &str) -> Option<RegexConstruct> {
match feature {
FEATURE_LOOKAROUND => Some(RegexConstruct::Lookaround),
FEATURE_UNICODE_CLASS_CAP => Some(RegexConstruct::UnicodeClassesGpu),
FEATURE_BACKREFERENCE => Some(RegexConstruct::Backreference),
FEATURE_HUGE_ALTERNATION => Some(RegexConstruct::HugeAlternation),
FEATURE_NESTED_REPEATS => Some(RegexConstruct::NestedRepeats),
_ => None,
}
}
impl std::fmt::Display for RegexCompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse {
pattern_index,
message,
} => write!(
f,
"regex_compile: pattern {pattern_index} parse error: {message}. \
Fix: review the regex syntax."
),
Self::Unsupported {
pattern_index,
feature,
} => write!(
f,
"regex_compile: pattern {pattern_index} uses unsupported feature `{feature}`. \
Fix: rewrite the detector into supported GPU-NFA syntax or split it into GPU-compatible rules."
),
Self::TooManyStates { states, cap } => write!(
f,
"regex_compile: NFA needs {states} states; per-pipeline cap is {cap}. \
Fix: split the pattern set across multiple pipelines."
),
Self::PatternCountOverflow { count } => write!(
f,
"regex_compile: pattern count {count} exceeds u32 capacity. Fix: shard the pattern set before GPU regex compilation."
),
Self::MatchLengthOverflow {
pattern_index,
len,
} => write!(
f,
"regex_compile: pattern {pattern_index} match length {len} exceeds u32 capacity. Fix: bound or shard the regex before GPU compilation."
),
Self::TableWordCountOverflow { table } => write!(
f,
"regex_compile: {table} table word count overflows host usize. Fix: shard the regex pattern set before table construction."
),
Self::StorageReserveFailed {
field,
requested,
message,
} => write!(
f,
"regex_compile: could not reserve {requested} {field} slot(s): {message}. Fix: shard the regex pattern set before GPU compilation."
),
}
}
}
impl std::error::Error for RegexCompileError {}
#[derive(Debug, Clone)]
pub struct CompiledRegexSet {
pub plan: NfaPlan,
pub transition_table: Vec<u32>,
pub epsilon_table: Vec<u32>,
pub captures_present: bool,
}
impl CompiledRegexSet {
#[must_use]
pub fn capture_extraction_diagnostic_code(&self) -> Option<&'static str> {
self.captures_present
.then_some(regex_construct_diagnostic_code(
RegexConstruct::CaptureExtraction,
))
}
}
const STATE_CAP: usize = LANES * 32;
const MAX_ALTERNATION_ARMS: usize = STATE_CAP;
const NESTED_REPEAT_UNROLL_BUDGET: u64 = STATE_CAP as u64;
struct ConstructScan {
captures_present: bool,
}
fn scan_constructs(
hir: &Hir,
pid: usize,
scan: &mut ConstructScan,
) -> Result<u64, RegexCompileError> {
match hir.kind() {
HirKind::Alternation(alts) => {
if alts.len() > MAX_ALTERNATION_ARMS {
return Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: FEATURE_HUGE_ALTERNATION,
});
}
let mut worst = 1u64;
for a in alts {
worst = worst.max(scan_constructs(a, pid, scan)?);
}
Ok(worst)
}
HirKind::Concat(parts) => {
let mut worst = 1u64;
for p in parts {
worst = worst.max(scan_constructs(p, pid, scan)?);
}
Ok(worst)
}
HirKind::Repetition(rep) => {
let inner = scan_constructs(&rep.sub, pid, scan)?;
match rep.max {
Some(m) => {
let product = u64::from(m).saturating_mul(inner.max(1));
if inner > 1 && product > NESTED_REPEAT_UNROLL_BUDGET {
return Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: FEATURE_NESTED_REPEATS,
});
}
Ok(product)
}
None => Ok(inner.max(1)),
}
}
HirKind::Capture(c) => {
scan.captures_present = true;
scan_constructs(&c.sub, pid, scan)
}
_ => Ok(1),
}
}
fn pattern_uses_backreference(pat: &str) -> bool {
let bytes = pat.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'\\' => {
if let Some(&c) = bytes.get(i + 1) {
if c.is_ascii_digit() && c != b'0' {
return true;
}
if c == b'k' && matches!(bytes.get(i + 2), Some(b'<' | b'\'' | b'{')) {
return true;
}
}
i += 2;
}
b'(' if pat[i..].starts_with("(?P=") => return true,
_ => i += 1,
}
}
false
}
pub fn compile_regex_set(patterns: &[&str]) -> Result<CompiledRegexSet, RegexCompileError> {
let mut builder = NfaBuilder::new();
let _pattern_count =
u32::try_from(patterns.len()).map_err(|_| RegexCompileError::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",
)?;
let mut accept_end_anchored = Vec::new();
reserve_vec(
&mut accept_end_anchored,
patterns.len(),
"accept end-anchor flag",
)?;
let entry = builder.fresh_state()?; let mut captures_present = false;
for (pid, pat) in patterns.iter().enumerate() {
let hir = match regex_syntax::ParserBuilder::new()
.unicode(false)
.utf8(false)
.build()
.parse(pat)
{
Ok(h) => h,
Err(byte_mode_err) => match regex_syntax::ParserBuilder::new()
.unicode(true)
.utf8(false)
.build()
.parse(pat)
{
Ok(h) => h,
Err(_unicode_err) => {
if pattern_uses_backreference(pat) {
return Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: FEATURE_BACKREFERENCE,
});
}
return Err(RegexCompileError::Parse {
pattern_index: pid,
message: format!("{byte_mode_err}"),
});
}
},
};
let mut construct_scan = ConstructScan {
captures_present: false,
};
scan_constructs(&hir, pid, &mut construct_scan)?;
captures_present |= construct_scan.captures_present;
let (frag, anchors) = build_pattern_hir(&mut builder, &hir, pid)?;
builder.add_epsilon(entry, frag.start);
let pid_u32 = u32::try_from(pid).map_err(|_| RegexCompileError::PatternCountOverflow {
count: patterns.len(),
})?;
let match_len_u32 =
u32::try_from(frag.match_len).map_err(|_| RegexCompileError::MatchLengthOverflow {
pattern_index: pid,
len: frag.match_len,
})?;
accept_states.push((pid_u32, match_len_u32));
accept_state_ids.push(frag.end);
accept_start_anchored.push(anchors.start);
accept_end_anchored.push(anchors.end);
}
if builder.state_count() > STATE_CAP {
return Err(RegexCompileError::TooManyStates {
states: builder.state_count(),
cap: STATE_CAP,
});
}
let plan = NfaPlan {
num_states: u32::try_from(builder.state_count()).map_err(|_| {
RegexCompileError::TooManyStates {
states: builder.state_count(),
cap: STATE_CAP,
}
})?,
input_len: 0,
accept_states,
accept_state_ids,
accept_start_anchored,
accept_end_anchored,
};
let (transition_table, epsilon_table) = builder.emit_lane_major_tables()?;
Ok(CompiledRegexSet {
plan,
transition_table,
epsilon_table,
captures_present,
})
}
pub fn build_rule_pipeline_from_regex(
patterns: &[&str],
input_buf: &str,
hit_buf: &str,
input_len: u32,
) -> Result<crate::scan::RulePipeline, RegexCompileError> {
let compiled = compile_regex_set(patterns)?;
let has_epsilon = compiled.epsilon_table.iter().any(|word| *word != 0);
let program = crate::scan::nfa::nfa_scan_with_plan(
&compiled.plan,
has_epsilon,
input_buf,
hit_buf,
input_len,
)
.map_err(|_| RegexCompileError::TooManyStates {
states: compiled.plan.num_states as usize,
cap: STATE_CAP,
})?;
Ok(crate::scan::RulePipeline {
program,
transition_table: compiled.transition_table,
epsilon_table: compiled.epsilon_table,
plan: compiled.plan.for_input_len(input_len),
})
}
#[derive(Debug)]
struct NfaBuilder {
state_count: usize,
transitions: Vec<ByteTransition>,
epsilons: Vec<(u32, u32)>,
}
#[derive(Debug, Clone)]
struct ByteTransition {
src: u32,
set: ByteSet,
dst: u32,
}
#[derive(Debug, Clone)]
struct ByteSet {
bits: [u64; 4], }
impl ByteSet {
fn new() -> Self {
Self { bits: [0; 4] }
}
fn insert(&mut self, b: u8) {
self.bits[(b / 64) as usize] |= 1u64 << (b % 64);
}
fn from_byte(b: u8) -> Self {
let mut s = Self::new();
s.insert(b);
s
}
fn from_range(lo: u8, hi: u8) -> Self {
let mut s = Self::new();
for b in lo..=hi {
s.insert(b);
}
s
}
fn for_each_set_byte(&self, mut f: impl FnMut(u8)) {
for (word_idx, &word) in self.bits.iter().enumerate() {
let mut bits = word;
while bits != 0 {
let bit = bits.trailing_zeros() as usize;
f((word_idx * 64 + bit) as u8);
bits &= bits - 1;
}
}
}
}
#[derive(Debug, Clone, Copy)]
struct Fragment {
start: u32,
end: u32,
match_len: usize,
}
#[derive(Debug, Clone, Copy, Default)]
struct PatternAnchors {
start: bool,
end: bool,
}
impl NfaBuilder {
fn new() -> Self {
Self {
state_count: 0,
transitions: Vec::new(),
epsilons: Vec::new(),
}
}
fn state_count(&self) -> usize {
self.state_count
}
fn fresh_state(&mut self) -> Result<u32, RegexCompileError> {
if self.state_count >= STATE_CAP {
return Err(RegexCompileError::TooManyStates {
states: self.state_count.saturating_add(1),
cap: STATE_CAP,
});
}
let state =
u32::try_from(self.state_count).map_err(|_| RegexCompileError::TooManyStates {
states: self.state_count,
cap: STATE_CAP,
})?;
self.state_count =
self.state_count
.checked_add(1)
.ok_or(RegexCompileError::TooManyStates {
states: usize::MAX,
cap: STATE_CAP,
})?;
Ok(state)
}
fn add_byte_transition(&mut self, src: u32, set: ByteSet, dst: u32) {
self.transitions.push(ByteTransition { src, set, dst });
}
fn add_epsilon(&mut self, src: u32, dst: u32) {
self.epsilons.push((src, dst));
}
fn emit_lane_major_tables(&self) -> Result<(Vec<u32>, Vec<u32>), RegexCompileError> {
let n = self.state_count();
let mut transitions = zeroed_u32_table(
table_word_count(n, 256, "transition")?,
"transition table word",
)?;
let mut epsilons =
zeroed_u32_table(table_word_count(n, 1, "epsilon")?, "epsilon table word")?;
for edge in &self.transitions {
let src = edge.src as usize;
let dst_lane = (edge.dst / 32) as usize;
let dst_bit = 1u32 << (edge.dst % 32);
edge.set.for_each_set_byte(|byte| {
let idx = src * 256 * LANES + (byte as usize) * LANES + dst_lane;
transitions[idx] |= dst_bit;
});
}
for &(src, dst) in &self.epsilons {
let dst_lane = (dst / 32) as usize;
let dst_bit = 1u32 << (dst % 32);
let idx = src as usize * LANES + dst_lane;
epsilons[idx] |= dst_bit;
}
Ok((transitions, epsilons))
}
}
fn table_word_count(
states: usize,
byte_columns: usize,
table: &'static str,
) -> Result<usize, RegexCompileError> {
states
.checked_mul(byte_columns)
.and_then(|words| words.checked_mul(LANES))
.ok_or(RegexCompileError::TableWordCountOverflow { table })
}
fn zeroed_u32_table(words: usize, field: &'static str) -> Result<Vec<u32>, RegexCompileError> {
let mut table = Vec::new();
reserve_vec(&mut table, words, field)?;
table.resize(words, 0);
Ok(table)
}
fn reserve_vec<T>(
vec: &mut Vec<T>,
requested: usize,
field: &'static str,
) -> Result<(), RegexCompileError> {
vyre_foundation::allocation::try_reserve_vec_to_capacity(vec, requested).map_err(|source| {
RegexCompileError::StorageReserveFailed {
field,
requested,
message: source.to_string(),
}
})
}
fn empty_fragment(b: &mut NfaBuilder) -> Result<Fragment, RegexCompileError> {
let s = b.fresh_state()?;
Ok(Fragment {
start: s,
end: s,
match_len: 0,
})
}
fn build_pattern_hir(
b: &mut NfaBuilder,
hir: &Hir,
pid: usize,
) -> Result<(Fragment, PatternAnchors), RegexCompileError> {
match hir.kind() {
HirKind::Look(Look::Start) => Ok((
empty_fragment(b)?,
PatternAnchors {
start: true,
end: false,
},
)),
HirKind::Look(Look::End) => Ok((
empty_fragment(b)?,
PatternAnchors {
start: false,
end: true,
},
)),
HirKind::Concat(parts) => {
let mut first = 0usize;
let mut last = parts.len();
let mut anchors = PatternAnchors::default();
if first < last && is_text_start_look(&parts[first]) {
anchors.start = true;
first += 1;
}
if first < last && is_text_end_look(&parts[last - 1]) {
anchors.end = true;
last -= 1;
}
Ok((build_hir_slice(b, &parts[first..last], pid)?, anchors))
}
_ => Ok((build_hir(b, hir, pid)?, PatternAnchors::default())),
}
}
fn is_text_start_look(hir: &Hir) -> bool {
matches!(hir.kind(), HirKind::Look(Look::Start))
}
fn is_text_end_look(hir: &Hir) -> bool {
matches!(hir.kind(), HirKind::Look(Look::End))
}
fn build_hir_slice(
b: &mut NfaBuilder,
parts: &[Hir],
pid: usize,
) -> Result<Fragment, RegexCompileError> {
let Some(first_part) = parts.first() else {
return empty_fragment(b);
};
let mut acc = build_hir(b, first_part, pid)?;
for child in &parts[1..] {
let next = build_hir(b, child, pid)?;
b.add_epsilon(acc.end, next.start);
acc = Fragment {
start: acc.start,
end: next.end,
match_len: acc.match_len + next.match_len,
};
}
Ok(acc)
}
fn build_hir(b: &mut NfaBuilder, hir: &Hir, pid: usize) -> Result<Fragment, RegexCompileError> {
match hir.kind() {
HirKind::Empty => empty_fragment(b),
HirKind::Literal(lit) => {
let start = b.fresh_state()?;
let mut prev = start;
for &byte in lit.0.iter() {
let next = b.fresh_state()?;
b.add_byte_transition(prev, ByteSet::from_byte(byte), next);
prev = next;
}
Ok(Fragment {
start,
end: prev,
match_len: lit.0.len(),
})
}
HirKind::Class(cls) => build_class(b, cls, pid),
HirKind::Repetition(rep) => build_repetition(b, rep, pid),
HirKind::Concat(parts) => build_hir_slice(b, parts, pid),
HirKind::Alternation(alts) => {
let fork = b.fresh_state()?;
let join = b.fresh_state()?;
let mut max_len = 0usize;
for child in alts {
let frag = build_hir(b, child, pid)?;
b.add_epsilon(fork, frag.start);
b.add_epsilon(frag.end, join);
if frag.match_len > max_len {
max_len = frag.match_len;
}
}
Ok(Fragment {
start: fork,
end: join,
match_len: max_len,
})
}
HirKind::Look(_) => Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: FEATURE_LOOKAROUND,
}),
HirKind::Capture(c) => {
build_hir(b, &c.sub, pid)
}
}
}
fn build_repetition(
b: &mut NfaBuilder,
rep: &Repetition,
pid: usize,
) -> Result<Fragment, RegexCompileError> {
let min = rep.min;
let max = rep.max;
if let Some(m) = max {
if m as usize > STATE_CAP {
return Err(RegexCompileError::TooManyStates {
states: m as usize,
cap: STATE_CAP,
});
}
}
if min as usize > STATE_CAP {
return Err(RegexCompileError::TooManyStates {
states: min as usize,
cap: STATE_CAP,
});
}
let start = b.fresh_state()?;
let mut tail = start;
let mut total_len = 0usize;
for _ in 0..min {
let frag = build_hir(b, &rep.sub, pid)?;
b.add_epsilon(tail, frag.start);
tail = frag.end;
total_len += frag.match_len;
}
match max {
None => {
let join = b.fresh_state()?;
let frag = build_hir(b, &rep.sub, pid)?;
b.add_epsilon(tail, frag.start);
b.add_epsilon(frag.end, frag.start); b.add_epsilon(frag.end, join);
b.add_epsilon(tail, join); tail = join;
}
Some(m) => {
for _ in min..m {
let frag = build_hir(b, &rep.sub, pid)?;
let join = b.fresh_state()?;
b.add_epsilon(tail, frag.start);
b.add_epsilon(frag.end, join);
b.add_epsilon(tail, join); tail = join;
total_len += frag.match_len;
}
}
}
Ok(Fragment {
start,
end: tail,
match_len: total_len,
})
}
fn build_class(b: &mut NfaBuilder, cls: &Class, pid: usize) -> Result<Fragment, RegexCompileError> {
if let Some(set) = try_class_as_ascii_byte_set(cls) {
let start = b.fresh_state()?;
let end = b.fresh_state()?;
b.add_byte_transition(start, set, end);
return Ok(Fragment {
start,
end,
match_len: 1,
});
}
let sequences = class_to_utf8_sequences(cls, pid)?;
if sequences.is_empty() {
return Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: "empty character class after Unicode expansion",
});
}
let start = b.fresh_state()?;
let end = b.fresh_state()?;
let mut max_len = 1usize;
for seq in &sequences {
if seq.is_empty() {
continue;
}
let arm_start = b.fresh_state()?;
b.add_epsilon(start, arm_start);
let mut prev = arm_start;
for &byte in seq {
let next = b.fresh_state()?;
b.add_byte_transition(prev, ByteSet::from_byte(byte), next);
prev = next;
}
b.add_epsilon(prev, end);
if seq.len() > max_len {
max_len = seq.len();
}
}
Ok(Fragment {
start,
end,
match_len: max_len,
})
}
fn try_class_as_ascii_byte_set(cls: &Class) -> Option<ByteSet> {
let mut out = ByteSet::new();
match cls {
Class::Bytes(byte_class) => {
for r in byte_class.iter() {
let merged = ByteSet::from_range(r.start(), r.end());
for w in 0..4 {
out.bits[w] |= merged.bits[w];
}
}
Some(out)
}
Class::Unicode(uni) => {
for r in uni.iter() {
if (r.end() as u32) > 0x7F {
return None;
}
let merged = ByteSet::from_range(r.start() as u8, r.end() as u8);
for w in 0..4 {
out.bits[w] |= merged.bits[w];
}
}
Some(out)
}
}
}
const MAX_CLASS_EXPANSION_CODEPOINTS: usize = 256;
fn class_to_utf8_sequences(cls: &Class, pid: usize) -> Result<Vec<Vec<u8>>, RegexCompileError> {
let mut sequences: Vec<Vec<u8>> = Vec::new();
let mut budget = MAX_CLASS_EXPANSION_CODEPOINTS;
match cls {
Class::Bytes(byte_class) => {
for r in byte_class.iter() {
for byte in r.start()..=r.end() {
if budget == 0 {
return Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: "byte character class exceeded expansion cap",
});
}
sequences.push(vec![byte]);
budget -= 1;
}
}
}
Class::Unicode(uni) => {
for r in uni.iter() {
let lo = r.start() as u32;
let hi = r.end() as u32;
for cp in lo..=hi {
if budget == 0 {
return Err(RegexCompileError::Unsupported {
pattern_index: pid,
feature: FEATURE_UNICODE_CLASS_CAP,
});
}
if let Some(c) = char::from_u32(cp) {
let mut buf = [0u8; 4];
let encoded = c.encode_utf8(&mut buf);
sequences.push(encoded.as_bytes().to_vec());
budget -= 1;
}
}
}
}
}
Ok(sequences)
}
#[cfg(test)]
mod tests {
use super::*;
fn states_of(s: &str) -> u32 {
compile_regex_set(&[s]).unwrap().plan.num_states
}
#[test]
fn capture_mode_routing_splits_accelerator_from_verifier() {
for mode in CaptureMode::ALL {
assert_eq!(
mode.accelerator_eligible(),
!mode.verifier_required(),
"{mode:?}: accelerator_eligible must be the complement of verifier_required"
);
}
let accel: Vec<CaptureMode> = CaptureMode::ALL
.into_iter()
.filter(|m| m.accelerator_eligible())
.collect();
assert_eq!(
accel,
vec![
CaptureMode::NonCapture,
CaptureMode::Count,
CaptureMode::Span
],
"only the whole-match modes are accelerator-eligible"
);
}
#[test]
fn capture_mode_id_round_trips_and_is_unique() {
use std::collections::BTreeSet;
let mut ids = BTreeSet::new();
for mode in CaptureMode::ALL {
let id = mode.contract_row().mode_id;
assert!(ids.insert(id), "duplicate mode_id `{id}`");
assert_eq!(
CaptureMode::from_mode_id(id),
Some(mode),
"mode_id `{id}` must round-trip back to {mode:?}"
);
}
assert_eq!(ids.len(), 6, "all six modes must have distinct ids");
assert_eq!(CaptureMode::from_mode_id("no_such_mode"), None);
}
#[test]
fn literal_compiles() {
let r = compile_regex_set(&["abc"]).unwrap();
assert_eq!(r.plan.num_states, 5);
assert_eq!(r.plan.accept_states.len(), 1);
}
#[test]
fn alternation_compiles() {
let r = compile_regex_set(&["a|b"]).unwrap();
assert!(r.plan.num_states > 0);
assert_eq!(r.plan.accept_states.len(), 1);
}
#[test]
fn class_compiles() {
let r = compile_regex_set(&["[a-z]"]).unwrap();
assert!(r.plan.num_states > 0);
}
#[test]
fn text_anchors_compile_to_accept_flags() {
let r = compile_regex_set(&["^foo$"]).unwrap();
assert_eq!(r.plan.accept_start_anchored, vec![true]);
assert_eq!(r.plan.accept_end_anchored, vec![true]);
}
#[test]
fn bounded_repetition_above_old_cap_compiles_under_state_cap() {
let r = compile_regex_set(&["a{0,128}"]).unwrap();
assert!(r.plan.num_states > 64);
assert!(r.plan.num_states <= STATE_CAP as u32);
}
#[test]
fn regex_compile_preserves_accept_metadata_through_checked_paths() {
let r = compile_regex_set(&["a", "bc", "^de$"]).unwrap();
assert_eq!(r.plan.accept_states, vec![(0, 1), (1, 2), (2, 2)]);
assert_eq!(r.plan.accept_state_ids.len(), 3);
assert_eq!(r.plan.accept_start_anchored, vec![false, false, true]);
assert_eq!(r.plan.accept_end_anchored, vec![false, false, true]);
assert_eq!(
r.transition_table.len(),
r.plan.num_states as usize * 256 * LANES
);
assert_eq!(r.epsilon_table.len(), r.plan.num_states as usize * LANES);
}
#[test]
fn regex_compile_uses_checked_abi_and_table_allocation_paths() {
let production = include_str!("regex_compile.rs")
.split("#[cfg(test)]")
.next()
.expect("Fix: regex_compile.rs must contain production section");
assert!(
production.contains("u32::try_from(pid)")
&& production.contains("u32::try_from(frag.match_len)")
&& production.contains("u32::try_from(builder.state_count())")
&& production.contains("u32::try_from(self.state_count)")
&& production.contains("checked_add(1)")
&& production.contains("try_reserve_vec_to_capacity")
&& !production.contains("pid as u32")
&& !production.contains("frag.match_len as u32")
&& !production.contains("builder.state_count() as u32")
&& !production.contains("self.state_count as u32")
&& !production.contains("vec![0u32;")
&& !production.contains("Vec::with_capacity(patterns.len())"),
"Fix: regex compilation must not truncate ids/counts or allocate NFA tables with infallible zero-vector construction."
);
}
#[test]
fn regex_pipeline_uses_compiled_plan_instead_of_literal_source_plan() {
let compiled = compile_regex_set(&["a|bc"]).unwrap();
let pipeline = build_rule_pipeline_from_regex(&["a|bc"], "input", "hits", 64).unwrap();
assert_eq!(pipeline.plan.num_states, compiled.plan.num_states);
assert_eq!(
pipeline.plan.accept_state_ids,
compiled.plan.accept_state_ids
);
assert_eq!(
pipeline.epsilon_table.iter().any(|word| *word != 0),
compiled.epsilon_table.iter().any(|word| *word != 0)
);
assert_ne!(
pipeline.plan.num_states,
crate::scan::nfa::compile(&["a|bc"]).num_states,
"regex pipeline must not rebuild the scan program from literal regex source bytes"
);
}
#[test]
fn states_count_grows_with_concat() {
let one = states_of("a");
let two = states_of("ab");
let three = states_of("abc");
assert!(two > one);
assert!(three > two);
}
#[test]
fn state_cap_enforced() {
let huge: String = (0..(STATE_CAP + 4)).map(|_| 'a').collect();
let err = compile_regex_set(&[&huge]).unwrap_err();
assert!(matches!(err, RegexCompileError::TooManyStates { .. }));
}
#[test]
fn unsupported_regex_diagnostic_does_not_route_to_cpu_backend() {
let err = compile_regex_set(&[r"\bsecret\b"]).unwrap_err();
let message = err.to_string().to_ascii_lowercase();
assert!(
!message.contains("cpu"),
"unsupported GPU-NFA regex diagnostics must not recommend host-side routing: {message}"
);
assert!(
message.contains("gpu"),
"unsupported GPU-NFA regex diagnostics must name the GPU-compatible rewrite contract: {message}"
);
}
#[test]
fn unicode_class_outside_ascii_compiles_via_utf8_expansion() {
let pat = "[hнһh]f_[a-zA-Z0-9]{4}";
let result = compile_regex_set(&[pat]);
let compiled = match result {
Ok(c) => c,
Err(e) => {
panic!("unicode-extended character class must compile via UTF-8 expansion; got {e}")
}
};
assert!(
compiled.plan.num_states > 4,
"expanded NFA must have non-trivial state count"
);
assert_eq!(compiled.plan.accept_states.len(), 1);
}
#[test]
fn ascii_only_class_keeps_single_byte_transition_path() {
let r = compile_regex_set(&["[ab]"]).unwrap();
assert_eq!(
r.plan.num_states, 3,
"[ab] must stay on the single-transition fast path (entry + 2 class states); got {} states",
r.plan.num_states
);
}
#[test]
fn unicode_class_above_expansion_cap_errors_cleanly() {
let pat = "[\u{0100}-\u{0200}]";
let err = compile_regex_set(&[pat]).unwrap_err();
match err {
RegexCompileError::Unsupported { feature, .. } => {
assert!(
feature.contains("expansion cap"),
"over-cap expansion must name the cap in its diagnostic: {feature}"
);
}
other => panic!("expected Unsupported expansion-cap error, got {other:?}"),
}
}
#[test]
fn regex_compile_diagnostic_codes() {
let look_err = compile_regex_set(&[r"a\bc"]).expect_err("word boundary is unsupported");
assert_eq!(
look_err.diagnostic_code(),
Some("VYRE_SCAN_APPROXIMATED_LOOKAROUND_REQUIRES_VERIFIER"),
"non-edge lookaround must map to its verifier diagnostic code; error was: {look_err}"
);
let uni_err =
compile_regex_set(&["[\u{0100}-\u{0200}]"]).expect_err("over-cap unicode class");
assert_eq!(
uni_err.diagnostic_code(),
Some("VYRE_SCAN_UNSUPPORTED_UNICODE_MODE_GPU"),
"over-cap unicode class must map to its diagnostic code; error was: {uni_err}"
);
assert!(
compile_regex_set(&["^abc$"]).is_ok(),
"start/end anchors must compile, not be flagged as unsupported lookaround"
);
let parse_err = compile_regex_set(&["("]).expect_err("unbalanced group is a parse error");
assert_eq!(
parse_err.diagnostic_code(),
None,
"a parse error must not claim a registry diagnostic code"
);
let backref_err =
compile_regex_set(&[r"(a)\1"]).expect_err("backreferences are unsupported");
assert_eq!(
backref_err.diagnostic_code(),
Some("VYRE_SCAN_UNSUPPORTED_BACKREFERENCE"),
"a backreference must map to its distinct code, not fall back to Parse; error was: {backref_err}"
);
let huge: String = (0..(MAX_ALTERNATION_ARMS + 8))
.map(|i| format!("v{i}"))
.collect::<Vec<_>>()
.join("|");
let alt_err = compile_regex_set(&[huge.as_str()]).expect_err("over-budget alternation");
assert_eq!(
alt_err.diagnostic_code(),
Some("VYRE_SCAN_UNSUPPORTED_HUGE_ALTERNATION_BUDGET"),
"a huge alternation must map to its budget code, not TooManyStates; error was: {alt_err}"
);
let nested_err =
compile_regex_set(&[r"(?:a{40}){40}"]).expect_err("nested-repeat unroll blowup");
assert_eq!(
nested_err.diagnostic_code(),
Some("VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET"),
"nested bounded repeats must map to their budget code; error was: {nested_err}"
);
}
#[test]
fn backreference_detector_is_escaping_aware() {
assert!(pattern_uses_backreference(r"\1"));
assert!(pattern_uses_backreference(r"(a)\1"));
assert!(pattern_uses_backreference(r"foo\9bar"));
assert!(pattern_uses_backreference(r"\k<name>"));
assert!(pattern_uses_backreference(r"\k'name'"));
assert!(pattern_uses_backreference("(?P=name)"));
assert!(!pattern_uses_backreference(r"\0"));
assert!(
!pattern_uses_backreference(r"\\1"),
"an escaped backslash then a literal 1 is not a backreference"
);
assert!(!pattern_uses_backreference(r"\d+\w*"));
assert!(!pattern_uses_backreference(r"[a-z]{3}"));
assert!(!pattern_uses_backreference("plain text"));
assert!(pattern_uses_backreference(r"\\\1"));
}
#[test]
fn captures_compile_and_surface_the_verifier_diagnostic() {
let with_cap = compile_regex_set(&[r"(abc)def"]).expect("captures compile for whole-match");
assert!(with_cap.captures_present, "the capture group must be noted");
assert_eq!(
with_cap.capture_extraction_diagnostic_code(),
Some("VYRE_SCAN_CAPTURE_EXTRACTION_REQUIRES_VERIFIER"),
"a captured pattern must surface the capture-verifier code without erroring"
);
let no_cap = compile_regex_set(&[r"abcdef"]).expect("plain pattern compiles");
assert!(!no_cap.captures_present);
assert_eq!(no_cap.capture_extraction_diagnostic_code(), None);
let noncap = compile_regex_set(&[r"(?:abc)def"]).expect("non-capturing group compiles");
assert!(
!noncap.captures_present,
"a (?:…) non-capturing group must not be flagged as a capture"
);
}
#[test]
fn budget_reclassification_does_not_regress_compiling_patterns() {
let ok_alt: String = ('a'..='z')
.chain('A'..='Z')
.chain('0'..='9')
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("|");
let compiled = compile_regex_set(&[ok_alt.as_str()])
.expect("a 62-arm single-byte alternation must still compile");
assert!(compiled.plan.num_states > 0);
assert!(
compile_regex_set(&[r"(?:a{20}){20}"]).is_ok(),
"a nested repeat under the unroll budget must still compile"
);
assert_eq!(
regex_construct_diagnostic_code(RegexConstruct::Backreference),
"VYRE_SCAN_UNSUPPORTED_BACKREFERENCE"
);
assert_eq!(
regex_construct_diagnostic_code(RegexConstruct::NestedRepeats),
"VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET"
);
}
#[test]
fn every_compile_error_variant_names_its_owner_and_fix_path() {
let variants = [
RegexCompileError::Parse {
pattern_index: 0,
message: "unclosed group".to_string(),
},
RegexCompileError::Unsupported {
pattern_index: 1,
feature: "backreference",
},
RegexCompileError::TooManyStates {
states: 5_000,
cap: 1_024,
},
RegexCompileError::PatternCountOverflow { count: usize::MAX },
RegexCompileError::MatchLengthOverflow {
pattern_index: 2,
len: usize::MAX,
},
RegexCompileError::TableWordCountOverflow {
table: "transition",
},
RegexCompileError::StorageReserveFailed {
field: "epsilon",
requested: 9,
message: "allocator refused".to_string(),
},
];
fn assert_covers_every_variant(error: &RegexCompileError) {
match error {
RegexCompileError::Parse { .. }
| RegexCompileError::Unsupported { .. }
| RegexCompileError::TooManyStates { .. }
| RegexCompileError::PatternCountOverflow { .. }
| RegexCompileError::MatchLengthOverflow { .. }
| RegexCompileError::TableWordCountOverflow { .. }
| RegexCompileError::StorageReserveFailed { .. } => {}
}
}
for error in &variants {
assert_covers_every_variant(error);
let rendered = error.to_string();
assert!(
rendered.starts_with("regex_compile:"),
"a RegexCompileError variant lacks the `regex_compile:` owner prefix: {rendered}"
);
assert!(
rendered.contains("Fix:"),
"a RegexCompileError variant lacks a `Fix:` remedy clause: {rendered}"
);
}
}
}