1use regex_syntax::hir::{Class, Hir, HirKind, Look, Repetition};
37
38use crate::scan::nfa::NfaPlan;
39
40const LANES: usize = vyre_primitives::nfa::subgroup_nfa::LANES_PER_SUBGROUP;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum CaptureMode {
57 NonCapture,
59 Count,
61 Span,
63 NamedCapture,
66 RepeatedCapture,
69 GroupExtraction,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct CaptureModeContract {
80 pub mode_id: &'static str,
82 pub output_shape: &'static str,
84 pub accelerator_eligible: bool,
86 pub verifier_required: bool,
88 pub null_policy: &'static str,
90}
91
92impl CaptureMode {
93 pub const ALL: [CaptureMode; 6] = [
95 CaptureMode::NonCapture,
96 CaptureMode::Count,
97 CaptureMode::Span,
98 CaptureMode::NamedCapture,
99 CaptureMode::RepeatedCapture,
100 CaptureMode::GroupExtraction,
101 ];
102
103 #[must_use]
106 pub const fn contract_row(self) -> CaptureModeContract {
107 match self {
108 CaptureMode::NonCapture => CaptureModeContract {
109 mode_id: "noncapture",
110 output_shape: "whole_match_only",
111 accelerator_eligible: true,
112 verifier_required: false,
113 null_policy: "not_applicable",
114 },
115 CaptureMode::Count => CaptureModeContract {
116 mode_id: "count",
117 output_shape: "match_count_per_pattern",
118 accelerator_eligible: true,
119 verifier_required: false,
120 null_policy: "not_applicable",
121 },
122 CaptureMode::Span => CaptureModeContract {
123 mode_id: "span",
124 output_shape: "whole_match_span",
125 accelerator_eligible: true,
126 verifier_required: false,
127 null_policy: "absent-match-has-no-span",
128 },
129 CaptureMode::NamedCapture => CaptureModeContract {
130 mode_id: "named_capture",
131 output_shape: "named_group_span_records",
132 accelerator_eligible: false,
133 verifier_required: true,
134 null_policy: "unmatched-group-null",
135 },
136 CaptureMode::RepeatedCapture => CaptureModeContract {
137 mode_id: "repeated_capture",
138 output_shape: "ordered_group_span_list",
139 accelerator_eligible: false,
140 verifier_required: true,
141 null_policy: "empty-repeat-yields-empty-list",
142 },
143 CaptureMode::GroupExtraction => CaptureModeContract {
144 mode_id: "group_extraction",
145 output_shape: "row_group_value_table",
146 accelerator_eligible: false,
147 verifier_required: true,
148 null_policy: "unmatched-group-null",
149 },
150 }
151 }
152
153 #[must_use]
156 pub const fn accelerator_eligible(self) -> bool {
157 self.contract_row().accelerator_eligible
158 }
159
160 #[must_use]
166 pub const fn verifier_required(self) -> bool {
167 self.contract_row().verifier_required
168 }
169
170 #[must_use]
174 pub fn from_mode_id(mode_id: &str) -> Option<CaptureMode> {
175 CaptureMode::ALL
176 .into_iter()
177 .find(|mode| mode.contract_row().mode_id == mode_id)
178 }
179}
180
181#[derive(Debug, Clone)]
184#[non_exhaustive]
185pub enum RegexCompileError {
186 Parse {
189 pattern_index: usize,
191 message: String,
193 },
194 Unsupported {
198 pattern_index: usize,
200 feature: &'static str,
202 },
203 TooManyStates {
207 states: usize,
209 cap: usize,
211 },
212 PatternCountOverflow {
214 count: usize,
216 },
217 MatchLengthOverflow {
219 pattern_index: usize,
221 len: usize,
223 },
224 TableWordCountOverflow {
226 table: &'static str,
228 },
229 StorageReserveFailed {
231 field: &'static str,
233 requested: usize,
235 message: String,
237 },
238}
239
240impl RegexCompileError {
241 #[must_use]
259 pub fn diagnostic_code(&self) -> Option<&'static str> {
260 match self {
261 Self::Unsupported { feature, .. } => {
265 regex_feature_construct(feature).map(regex_construct_diagnostic_code)
266 }
267 _ => None,
268 }
269 }
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
281#[non_exhaustive]
282pub enum RegexConstruct {
283 Backreference,
285 Lookaround,
287 UnicodeClassesGpu,
289 CaptureExtraction,
292 HugeAlternation,
294 NestedRepeats,
296}
297
298#[must_use]
301pub fn regex_construct_diagnostic_code(construct: RegexConstruct) -> &'static str {
302 match construct {
303 RegexConstruct::Backreference => "VYRE_SCAN_UNSUPPORTED_BACKREFERENCE",
304 RegexConstruct::Lookaround => "VYRE_SCAN_APPROXIMATED_LOOKAROUND_REQUIRES_VERIFIER",
305 RegexConstruct::UnicodeClassesGpu => "VYRE_SCAN_UNSUPPORTED_UNICODE_MODE_GPU",
306 RegexConstruct::CaptureExtraction => "VYRE_SCAN_CAPTURE_EXTRACTION_REQUIRES_VERIFIER",
307 RegexConstruct::HugeAlternation => "VYRE_SCAN_UNSUPPORTED_HUGE_ALTERNATION_BUDGET",
308 RegexConstruct::NestedRepeats => "VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET",
309 }
310}
311
312const FEATURE_LOOKAROUND: &str = "non-edge lookaround assertion";
316const FEATURE_UNICODE_CLASS_CAP: &str = "unicode character class exceeded expansion cap";
317const FEATURE_BACKREFERENCE: &str = "backreference";
318const FEATURE_HUGE_ALTERNATION: &str = "huge alternation exceeds budget";
319const FEATURE_NESTED_REPEATS: &str = "nested repeat exceeds budget";
320
321fn regex_feature_construct(feature: &str) -> Option<RegexConstruct> {
325 match feature {
326 FEATURE_LOOKAROUND => Some(RegexConstruct::Lookaround),
327 FEATURE_UNICODE_CLASS_CAP => Some(RegexConstruct::UnicodeClassesGpu),
328 FEATURE_BACKREFERENCE => Some(RegexConstruct::Backreference),
329 FEATURE_HUGE_ALTERNATION => Some(RegexConstruct::HugeAlternation),
330 FEATURE_NESTED_REPEATS => Some(RegexConstruct::NestedRepeats),
331 _ => None,
332 }
333}
334
335impl std::fmt::Display for RegexCompileError {
336 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337 match self {
338 Self::Parse {
339 pattern_index,
340 message,
341 } => write!(
342 f,
343 "regex_compile: pattern {pattern_index} parse error: {message}. \
344 Fix: review the regex syntax."
345 ),
346 Self::Unsupported {
347 pattern_index,
348 feature,
349 } => write!(
350 f,
351 "regex_compile: pattern {pattern_index} uses unsupported feature `{feature}`. \
352 Fix: rewrite the detector into supported GPU-NFA syntax or split it into GPU-compatible rules."
353 ),
354 Self::TooManyStates { states, cap } => write!(
355 f,
356 "regex_compile: NFA needs {states} states; per-pipeline cap is {cap}. \
357 Fix: split the pattern set across multiple pipelines."
358 ),
359 Self::PatternCountOverflow { count } => write!(
360 f,
361 "regex_compile: pattern count {count} exceeds u32 capacity. Fix: shard the pattern set before GPU regex compilation."
362 ),
363 Self::MatchLengthOverflow {
364 pattern_index,
365 len,
366 } => write!(
367 f,
368 "regex_compile: pattern {pattern_index} match length {len} exceeds u32 capacity. Fix: bound or shard the regex before GPU compilation."
369 ),
370 Self::TableWordCountOverflow { table } => write!(
371 f,
372 "regex_compile: {table} table word count overflows host usize. Fix: shard the regex pattern set before table construction."
373 ),
374 Self::StorageReserveFailed {
375 field,
376 requested,
377 message,
378 } => write!(
379 f,
380 "regex_compile: could not reserve {requested} {field} slot(s): {message}. Fix: shard the regex pattern set before GPU compilation."
381 ),
382 }
383 }
384}
385
386impl std::error::Error for RegexCompileError {}
387
388#[derive(Debug, Clone)]
393pub struct CompiledRegexSet {
394 pub plan: NfaPlan,
396 pub transition_table: Vec<u32>,
399 pub epsilon_table: Vec<u32>,
402 pub captures_present: bool,
413}
414
415impl CompiledRegexSet {
416 #[must_use]
424 pub fn capture_extraction_diagnostic_code(&self) -> Option<&'static str> {
425 self.captures_present
426 .then_some(regex_construct_diagnostic_code(
427 RegexConstruct::CaptureExtraction,
428 ))
429 }
430}
431
432const STATE_CAP: usize = LANES * 32;
433
434const MAX_ALTERNATION_ARMS: usize = STATE_CAP;
441
442const NESTED_REPEAT_UNROLL_BUDGET: u64 = STATE_CAP as u64;
449
450struct ConstructScan {
452 captures_present: bool,
455}
456
457fn scan_constructs(
463 hir: &Hir,
464 pid: usize,
465 scan: &mut ConstructScan,
466) -> Result<u64, RegexCompileError> {
467 match hir.kind() {
468 HirKind::Alternation(alts) => {
469 if alts.len() > MAX_ALTERNATION_ARMS {
470 return Err(RegexCompileError::Unsupported {
471 pattern_index: pid,
472 feature: FEATURE_HUGE_ALTERNATION,
473 });
474 }
475 let mut worst = 1u64;
476 for a in alts {
477 worst = worst.max(scan_constructs(a, pid, scan)?);
478 }
479 Ok(worst)
480 }
481 HirKind::Concat(parts) => {
482 let mut worst = 1u64;
483 for p in parts {
484 worst = worst.max(scan_constructs(p, pid, scan)?);
485 }
486 Ok(worst)
487 }
488 HirKind::Repetition(rep) => {
489 let inner = scan_constructs(&rep.sub, pid, scan)?;
490 match rep.max {
491 Some(m) => {
492 let product = u64::from(m).saturating_mul(inner.max(1));
493 if inner > 1 && product > NESTED_REPEAT_UNROLL_BUDGET {
498 return Err(RegexCompileError::Unsupported {
499 pattern_index: pid,
500 feature: FEATURE_NESTED_REPEATS,
501 });
502 }
503 Ok(product)
504 }
505 None => Ok(inner.max(1)),
508 }
509 }
510 HirKind::Capture(c) => {
511 scan.captures_present = true;
512 scan_constructs(&c.sub, pid, scan)
513 }
514 _ => Ok(1),
515 }
516}
517
518fn pattern_uses_backreference(pat: &str) -> bool {
526 let bytes = pat.as_bytes();
527 let mut i = 0;
528 while i < bytes.len() {
529 match bytes[i] {
530 b'\\' => {
531 if let Some(&c) = bytes.get(i + 1) {
532 if c.is_ascii_digit() && c != b'0' {
534 return true;
535 }
536 if c == b'k' && matches!(bytes.get(i + 2), Some(b'<' | b'\'' | b'{')) {
538 return true;
539 }
540 }
541 i += 2;
543 }
544 b'(' if pat[i..].starts_with("(?P=") => return true,
547 _ => i += 1,
548 }
549 }
550 false
551}
552
553pub fn compile_regex_set(patterns: &[&str]) -> Result<CompiledRegexSet, RegexCompileError> {
558 let mut builder = NfaBuilder::new();
559 let _pattern_count =
560 u32::try_from(patterns.len()).map_err(|_| RegexCompileError::PatternCountOverflow {
561 count: patterns.len(),
562 })?;
563 let mut accept_states = Vec::new();
564 reserve_vec(&mut accept_states, patterns.len(), "accept state")?;
565 let mut accept_state_ids = Vec::new();
566 reserve_vec(&mut accept_state_ids, patterns.len(), "accept state id")?;
567 let mut accept_start_anchored = Vec::new();
568 reserve_vec(
569 &mut accept_start_anchored,
570 patterns.len(),
571 "accept start-anchor flag",
572 )?;
573 let mut accept_end_anchored = Vec::new();
574 reserve_vec(
575 &mut accept_end_anchored,
576 patterns.len(),
577 "accept end-anchor flag",
578 )?;
579 let entry = builder.fresh_state()?; let mut captures_present = false;
581
582 for (pid, pat) in patterns.iter().enumerate() {
588 let hir = match regex_syntax::ParserBuilder::new()
596 .unicode(false)
597 .utf8(false)
598 .build()
599 .parse(pat)
600 {
601 Ok(h) => h,
602 Err(byte_mode_err) => match regex_syntax::ParserBuilder::new()
603 .unicode(true)
604 .utf8(false)
605 .build()
606 .parse(pat)
607 {
608 Ok(h) => h,
609 Err(_unicode_err) => {
610 if pattern_uses_backreference(pat) {
618 return Err(RegexCompileError::Unsupported {
619 pattern_index: pid,
620 feature: FEATURE_BACKREFERENCE,
621 });
622 }
623 return Err(RegexCompileError::Parse {
624 pattern_index: pid,
625 message: format!("{byte_mode_err}"),
626 });
627 }
628 },
629 };
630 let mut construct_scan = ConstructScan {
634 captures_present: false,
635 };
636 scan_constructs(&hir, pid, &mut construct_scan)?;
637 captures_present |= construct_scan.captures_present;
638 let (frag, anchors) = build_pattern_hir(&mut builder, &hir, pid)?;
639 builder.add_epsilon(entry, frag.start);
641 let pid_u32 = u32::try_from(pid).map_err(|_| RegexCompileError::PatternCountOverflow {
642 count: patterns.len(),
643 })?;
644 let match_len_u32 =
645 u32::try_from(frag.match_len).map_err(|_| RegexCompileError::MatchLengthOverflow {
646 pattern_index: pid,
647 len: frag.match_len,
648 })?;
649 accept_states.push((pid_u32, match_len_u32));
650 accept_state_ids.push(frag.end);
651 accept_start_anchored.push(anchors.start);
652 accept_end_anchored.push(anchors.end);
653 }
654
655 if builder.state_count() > STATE_CAP {
656 return Err(RegexCompileError::TooManyStates {
657 states: builder.state_count(),
658 cap: STATE_CAP,
659 });
660 }
661
662 let plan = NfaPlan {
663 num_states: u32::try_from(builder.state_count()).map_err(|_| {
664 RegexCompileError::TooManyStates {
665 states: builder.state_count(),
666 cap: STATE_CAP,
667 }
668 })?,
669 input_len: 0,
670 accept_states,
671 accept_state_ids,
672 accept_start_anchored,
673 accept_end_anchored,
674 };
675 let (transition_table, epsilon_table) = builder.emit_lane_major_tables()?;
676 Ok(CompiledRegexSet {
677 plan,
678 transition_table,
679 epsilon_table,
680 captures_present,
681 })
682}
683
684pub fn build_rule_pipeline_from_regex(
692 patterns: &[&str],
693 input_buf: &str,
694 hit_buf: &str,
695 input_len: u32,
696) -> Result<crate::scan::RulePipeline, RegexCompileError> {
697 let compiled = compile_regex_set(patterns)?;
698 let has_epsilon = compiled.epsilon_table.iter().any(|word| *word != 0);
699 let program = crate::scan::nfa::nfa_scan_with_plan(
700 &compiled.plan,
701 has_epsilon,
702 input_buf,
703 hit_buf,
704 input_len,
705 )
706 .map_err(|_| RegexCompileError::TooManyStates {
707 states: compiled.plan.num_states as usize,
708 cap: STATE_CAP,
709 })?;
710 Ok(crate::scan::RulePipeline {
711 program,
712 transition_table: compiled.transition_table,
713 epsilon_table: compiled.epsilon_table,
714 plan: compiled.plan.for_input_len(input_len),
715 })
716}
717
718#[derive(Debug)]
721struct NfaBuilder {
722 state_count: usize,
723 transitions: Vec<ByteTransition>,
726 epsilons: Vec<(u32, u32)>,
728}
729
730#[derive(Debug, Clone)]
731struct ByteTransition {
732 src: u32,
733 set: ByteSet,
734 dst: u32,
735}
736
737#[derive(Debug, Clone)]
738struct ByteSet {
739 bits: [u64; 4], }
741
742impl ByteSet {
743 fn new() -> Self {
744 Self { bits: [0; 4] }
745 }
746 fn insert(&mut self, b: u8) {
747 self.bits[(b / 64) as usize] |= 1u64 << (b % 64);
748 }
749 fn from_byte(b: u8) -> Self {
750 let mut s = Self::new();
751 s.insert(b);
752 s
753 }
754 fn from_range(lo: u8, hi: u8) -> Self {
755 let mut s = Self::new();
756 for b in lo..=hi {
757 s.insert(b);
758 }
759 s
760 }
761 fn for_each_set_byte(&self, mut f: impl FnMut(u8)) {
762 for (word_idx, &word) in self.bits.iter().enumerate() {
763 let mut bits = word;
764 while bits != 0 {
765 let bit = bits.trailing_zeros() as usize;
766 f((word_idx * 64 + bit) as u8);
767 bits &= bits - 1;
768 }
769 }
770 }
771}
772
773#[derive(Debug, Clone, Copy)]
774struct Fragment {
775 start: u32,
776 end: u32,
777 match_len: usize,
780}
781
782#[derive(Debug, Clone, Copy, Default)]
783struct PatternAnchors {
784 start: bool,
785 end: bool,
786}
787
788impl NfaBuilder {
789 fn new() -> Self {
790 Self {
791 state_count: 0,
792 transitions: Vec::new(),
793 epsilons: Vec::new(),
794 }
795 }
796
797 fn state_count(&self) -> usize {
798 self.state_count
799 }
800
801 fn fresh_state(&mut self) -> Result<u32, RegexCompileError> {
802 if self.state_count >= STATE_CAP {
803 return Err(RegexCompileError::TooManyStates {
804 states: self.state_count.saturating_add(1),
805 cap: STATE_CAP,
806 });
807 }
808 let state =
809 u32::try_from(self.state_count).map_err(|_| RegexCompileError::TooManyStates {
810 states: self.state_count,
811 cap: STATE_CAP,
812 })?;
813 self.state_count =
814 self.state_count
815 .checked_add(1)
816 .ok_or(RegexCompileError::TooManyStates {
817 states: usize::MAX,
818 cap: STATE_CAP,
819 })?;
820 Ok(state)
821 }
822
823 fn add_byte_transition(&mut self, src: u32, set: ByteSet, dst: u32) {
824 self.transitions.push(ByteTransition { src, set, dst });
825 }
826
827 fn add_epsilon(&mut self, src: u32, dst: u32) {
828 self.epsilons.push((src, dst));
829 }
830
831 fn emit_lane_major_tables(&self) -> Result<(Vec<u32>, Vec<u32>), RegexCompileError> {
834 let n = self.state_count();
835 let mut transitions = zeroed_u32_table(
836 table_word_count(n, 256, "transition")?,
837 "transition table word",
838 )?;
839 let mut epsilons =
840 zeroed_u32_table(table_word_count(n, 1, "epsilon")?, "epsilon table word")?;
841
842 for edge in &self.transitions {
843 let src = edge.src as usize;
844 let dst_lane = (edge.dst / 32) as usize;
845 let dst_bit = 1u32 << (edge.dst % 32);
846 edge.set.for_each_set_byte(|byte| {
847 let idx = src * 256 * LANES + (byte as usize) * LANES + dst_lane;
848 transitions[idx] |= dst_bit;
849 });
850 }
851 for &(src, dst) in &self.epsilons {
852 let dst_lane = (dst / 32) as usize;
853 let dst_bit = 1u32 << (dst % 32);
854 let idx = src as usize * LANES + dst_lane;
855 epsilons[idx] |= dst_bit;
856 }
857 Ok((transitions, epsilons))
858 }
859}
860
861fn table_word_count(
862 states: usize,
863 byte_columns: usize,
864 table: &'static str,
865) -> Result<usize, RegexCompileError> {
866 states
867 .checked_mul(byte_columns)
868 .and_then(|words| words.checked_mul(LANES))
869 .ok_or(RegexCompileError::TableWordCountOverflow { table })
870}
871
872fn zeroed_u32_table(words: usize, field: &'static str) -> Result<Vec<u32>, RegexCompileError> {
873 let mut table = Vec::new();
874 reserve_vec(&mut table, words, field)?;
875 table.resize(words, 0);
876 Ok(table)
877}
878
879fn reserve_vec<T>(
880 vec: &mut Vec<T>,
881 requested: usize,
882 field: &'static str,
883) -> Result<(), RegexCompileError> {
884 vyre_foundation::allocation::try_reserve_vec_to_capacity(vec, requested).map_err(|source| {
885 RegexCompileError::StorageReserveFailed {
886 field,
887 requested,
888 message: source.to_string(),
889 }
890 })
891}
892
893fn empty_fragment(b: &mut NfaBuilder) -> Result<Fragment, RegexCompileError> {
894 let s = b.fresh_state()?;
895 Ok(Fragment {
896 start: s,
897 end: s,
898 match_len: 0,
899 })
900}
901
902fn build_pattern_hir(
903 b: &mut NfaBuilder,
904 hir: &Hir,
905 pid: usize,
906) -> Result<(Fragment, PatternAnchors), RegexCompileError> {
907 match hir.kind() {
908 HirKind::Look(Look::Start) => Ok((
909 empty_fragment(b)?,
910 PatternAnchors {
911 start: true,
912 end: false,
913 },
914 )),
915 HirKind::Look(Look::End) => Ok((
916 empty_fragment(b)?,
917 PatternAnchors {
918 start: false,
919 end: true,
920 },
921 )),
922 HirKind::Concat(parts) => {
923 let mut first = 0usize;
924 let mut last = parts.len();
925 let mut anchors = PatternAnchors::default();
926
927 if first < last && is_text_start_look(&parts[first]) {
928 anchors.start = true;
929 first += 1;
930 }
931 if first < last && is_text_end_look(&parts[last - 1]) {
932 anchors.end = true;
933 last -= 1;
934 }
935
936 Ok((build_hir_slice(b, &parts[first..last], pid)?, anchors))
937 }
938 _ => Ok((build_hir(b, hir, pid)?, PatternAnchors::default())),
939 }
940}
941
942fn is_text_start_look(hir: &Hir) -> bool {
943 matches!(hir.kind(), HirKind::Look(Look::Start))
944}
945
946fn is_text_end_look(hir: &Hir) -> bool {
947 matches!(hir.kind(), HirKind::Look(Look::End))
948}
949
950fn build_hir_slice(
951 b: &mut NfaBuilder,
952 parts: &[Hir],
953 pid: usize,
954) -> Result<Fragment, RegexCompileError> {
955 let Some(first_part) = parts.first() else {
956 return empty_fragment(b);
957 };
958 let mut acc = build_hir(b, first_part, pid)?;
959 for child in &parts[1..] {
960 let next = build_hir(b, child, pid)?;
961 b.add_epsilon(acc.end, next.start);
962 acc = Fragment {
963 start: acc.start,
964 end: next.end,
965 match_len: acc.match_len + next.match_len,
966 };
967 }
968 Ok(acc)
969}
970
971fn build_hir(b: &mut NfaBuilder, hir: &Hir, pid: usize) -> Result<Fragment, RegexCompileError> {
972 match hir.kind() {
973 HirKind::Empty => empty_fragment(b),
974 HirKind::Literal(lit) => {
975 let start = b.fresh_state()?;
977 let mut prev = start;
978 for &byte in lit.0.iter() {
979 let next = b.fresh_state()?;
980 b.add_byte_transition(prev, ByteSet::from_byte(byte), next);
981 prev = next;
982 }
983 Ok(Fragment {
984 start,
985 end: prev,
986 match_len: lit.0.len(),
987 })
988 }
989 HirKind::Class(cls) => build_class(b, cls, pid),
990 HirKind::Repetition(rep) => build_repetition(b, rep, pid),
991 HirKind::Concat(parts) => build_hir_slice(b, parts, pid),
992 HirKind::Alternation(alts) => {
993 let fork = b.fresh_state()?;
995 let join = b.fresh_state()?;
996 let mut max_len = 0usize;
997 for child in alts {
998 let frag = build_hir(b, child, pid)?;
999 b.add_epsilon(fork, frag.start);
1000 b.add_epsilon(frag.end, join);
1001 if frag.match_len > max_len {
1002 max_len = frag.match_len;
1003 }
1004 }
1005 Ok(Fragment {
1006 start: fork,
1007 end: join,
1008 match_len: max_len,
1009 })
1010 }
1011 HirKind::Look(_) => Err(RegexCompileError::Unsupported {
1012 pattern_index: pid,
1013 feature: FEATURE_LOOKAROUND,
1014 }),
1015 HirKind::Capture(c) => {
1016 build_hir(b, &c.sub, pid)
1019 }
1020 }
1021}
1022
1023fn build_repetition(
1024 b: &mut NfaBuilder,
1025 rep: &Repetition,
1026 pid: usize,
1027) -> Result<Fragment, RegexCompileError> {
1028 let min = rep.min;
1029 let max = rep.max;
1030
1031 if let Some(m) = max {
1035 if m as usize > STATE_CAP {
1036 return Err(RegexCompileError::TooManyStates {
1037 states: m as usize,
1038 cap: STATE_CAP,
1039 });
1040 }
1041 }
1042 if min as usize > STATE_CAP {
1043 return Err(RegexCompileError::TooManyStates {
1044 states: min as usize,
1045 cap: STATE_CAP,
1046 });
1047 }
1048
1049 let start = b.fresh_state()?;
1053 let mut tail = start;
1054 let mut total_len = 0usize;
1055
1056 for _ in 0..min {
1057 let frag = build_hir(b, &rep.sub, pid)?;
1058 b.add_epsilon(tail, frag.start);
1059 tail = frag.end;
1060 total_len += frag.match_len;
1061 }
1062
1063 match max {
1064 None => {
1065 let join = b.fresh_state()?;
1068 let frag = build_hir(b, &rep.sub, pid)?;
1069 b.add_epsilon(tail, frag.start);
1070 b.add_epsilon(frag.end, frag.start); b.add_epsilon(frag.end, join);
1072 b.add_epsilon(tail, join); tail = join;
1074 }
1075 Some(m) => {
1076 for _ in min..m {
1077 let frag = build_hir(b, &rep.sub, pid)?;
1078 let join = b.fresh_state()?;
1079 b.add_epsilon(tail, frag.start);
1080 b.add_epsilon(frag.end, join);
1081 b.add_epsilon(tail, join); tail = join;
1083 total_len += frag.match_len;
1095 }
1096 }
1097 }
1098 Ok(Fragment {
1099 start,
1100 end: tail,
1101 match_len: total_len,
1102 })
1103}
1104
1105fn build_class(b: &mut NfaBuilder, cls: &Class, pid: usize) -> Result<Fragment, RegexCompileError> {
1126 if let Some(set) = try_class_as_ascii_byte_set(cls) {
1127 let start = b.fresh_state()?;
1128 let end = b.fresh_state()?;
1129 b.add_byte_transition(start, set, end);
1130 return Ok(Fragment {
1131 start,
1132 end,
1133 match_len: 1,
1134 });
1135 }
1136 let sequences = class_to_utf8_sequences(cls, pid)?;
1137 if sequences.is_empty() {
1138 return Err(RegexCompileError::Unsupported {
1139 pattern_index: pid,
1140 feature: "empty character class after Unicode expansion",
1141 });
1142 }
1143 let start = b.fresh_state()?;
1144 let end = b.fresh_state()?;
1145 let mut max_len = 1usize;
1146 for seq in &sequences {
1147 if seq.is_empty() {
1148 continue;
1149 }
1150 let arm_start = b.fresh_state()?;
1153 b.add_epsilon(start, arm_start);
1154 let mut prev = arm_start;
1155 for &byte in seq {
1156 let next = b.fresh_state()?;
1157 b.add_byte_transition(prev, ByteSet::from_byte(byte), next);
1158 prev = next;
1159 }
1160 b.add_epsilon(prev, end);
1161 if seq.len() > max_len {
1162 max_len = seq.len();
1163 }
1164 }
1165 Ok(Fragment {
1166 start,
1167 end,
1168 match_len: max_len,
1169 })
1170}
1171
1172fn try_class_as_ascii_byte_set(cls: &Class) -> Option<ByteSet> {
1176 let mut out = ByteSet::new();
1177 match cls {
1178 Class::Bytes(byte_class) => {
1179 for r in byte_class.iter() {
1183 let merged = ByteSet::from_range(r.start(), r.end());
1184 for w in 0..4 {
1185 out.bits[w] |= merged.bits[w];
1186 }
1187 }
1188 Some(out)
1189 }
1190 Class::Unicode(uni) => {
1191 for r in uni.iter() {
1194 if (r.end() as u32) > 0x7F {
1195 return None;
1196 }
1197 let merged = ByteSet::from_range(r.start() as u8, r.end() as u8);
1198 for w in 0..4 {
1199 out.bits[w] |= merged.bits[w];
1200 }
1201 }
1202 Some(out)
1203 }
1204 }
1205}
1206
1207const MAX_CLASS_EXPANSION_CODEPOINTS: usize = 256;
1214
1215fn class_to_utf8_sequences(cls: &Class, pid: usize) -> Result<Vec<Vec<u8>>, RegexCompileError> {
1220 let mut sequences: Vec<Vec<u8>> = Vec::new();
1221 let mut budget = MAX_CLASS_EXPANSION_CODEPOINTS;
1222 match cls {
1223 Class::Bytes(byte_class) => {
1224 for r in byte_class.iter() {
1225 for byte in r.start()..=r.end() {
1226 if budget == 0 {
1227 return Err(RegexCompileError::Unsupported {
1228 pattern_index: pid,
1229 feature: "byte character class exceeded expansion cap",
1230 });
1231 }
1232 sequences.push(vec![byte]);
1233 budget -= 1;
1234 }
1235 }
1236 }
1237 Class::Unicode(uni) => {
1238 for r in uni.iter() {
1239 let lo = r.start() as u32;
1240 let hi = r.end() as u32;
1241 for cp in lo..=hi {
1242 if budget == 0 {
1243 return Err(RegexCompileError::Unsupported {
1244 pattern_index: pid,
1245 feature: FEATURE_UNICODE_CLASS_CAP,
1246 });
1247 }
1248 if let Some(c) = char::from_u32(cp) {
1255 let mut buf = [0u8; 4];
1256 let encoded = c.encode_utf8(&mut buf);
1257 sequences.push(encoded.as_bytes().to_vec());
1258 budget -= 1;
1259 }
1260 }
1261 }
1262 }
1263 }
1264 Ok(sequences)
1265}
1266
1267#[cfg(test)]
1268mod tests {
1269 use super::*;
1270
1271 fn states_of(s: &str) -> u32 {
1272 compile_regex_set(&[s]).unwrap().plan.num_states
1273 }
1274
1275 #[test]
1276 fn capture_mode_routing_splits_accelerator_from_verifier() {
1277 for mode in CaptureMode::ALL {
1282 assert_eq!(
1283 mode.accelerator_eligible(),
1284 !mode.verifier_required(),
1285 "{mode:?}: accelerator_eligible must be the complement of verifier_required"
1286 );
1287 }
1288 let accel: Vec<CaptureMode> = CaptureMode::ALL
1289 .into_iter()
1290 .filter(|m| m.accelerator_eligible())
1291 .collect();
1292 assert_eq!(
1293 accel,
1294 vec![
1295 CaptureMode::NonCapture,
1296 CaptureMode::Count,
1297 CaptureMode::Span
1298 ],
1299 "only the whole-match modes are accelerator-eligible"
1300 );
1301 }
1302
1303 #[test]
1304 fn capture_mode_id_round_trips_and_is_unique() {
1305 use std::collections::BTreeSet;
1306 let mut ids = BTreeSet::new();
1307 for mode in CaptureMode::ALL {
1308 let id = mode.contract_row().mode_id;
1309 assert!(ids.insert(id), "duplicate mode_id `{id}`");
1310 assert_eq!(
1311 CaptureMode::from_mode_id(id),
1312 Some(mode),
1313 "mode_id `{id}` must round-trip back to {mode:?}"
1314 );
1315 }
1316 assert_eq!(ids.len(), 6, "all six modes must have distinct ids");
1317 assert_eq!(CaptureMode::from_mode_id("no_such_mode"), None);
1318 }
1319
1320 #[test]
1321 fn literal_compiles() {
1322 let r = compile_regex_set(&["abc"]).unwrap();
1323 assert_eq!(r.plan.num_states, 5);
1325 assert_eq!(r.plan.accept_states.len(), 1);
1326 }
1327
1328 #[test]
1329 fn alternation_compiles() {
1330 let r = compile_regex_set(&["a|b"]).unwrap();
1331 assert!(r.plan.num_states > 0);
1334 assert_eq!(r.plan.accept_states.len(), 1);
1335 }
1336
1337 #[test]
1338 fn class_compiles() {
1339 let r = compile_regex_set(&["[a-z]"]).unwrap();
1340 assert!(r.plan.num_states > 0);
1341 }
1344
1345 #[test]
1346 fn text_anchors_compile_to_accept_flags() {
1347 let r = compile_regex_set(&["^foo$"]).unwrap();
1348 assert_eq!(r.plan.accept_start_anchored, vec![true]);
1349 assert_eq!(r.plan.accept_end_anchored, vec![true]);
1350 }
1351
1352 #[test]
1353 fn bounded_repetition_above_old_cap_compiles_under_state_cap() {
1354 let r = compile_regex_set(&["a{0,128}"]).unwrap();
1355 assert!(r.plan.num_states > 64);
1356 assert!(r.plan.num_states <= STATE_CAP as u32);
1357 }
1358
1359 #[test]
1360 fn regex_compile_preserves_accept_metadata_through_checked_paths() {
1361 let r = compile_regex_set(&["a", "bc", "^de$"]).unwrap();
1362
1363 assert_eq!(r.plan.accept_states, vec![(0, 1), (1, 2), (2, 2)]);
1364 assert_eq!(r.plan.accept_state_ids.len(), 3);
1365 assert_eq!(r.plan.accept_start_anchored, vec![false, false, true]);
1366 assert_eq!(r.plan.accept_end_anchored, vec![false, false, true]);
1367 assert_eq!(
1368 r.transition_table.len(),
1369 r.plan.num_states as usize * 256 * LANES
1370 );
1371 assert_eq!(r.epsilon_table.len(), r.plan.num_states as usize * LANES);
1372 }
1373
1374 #[test]
1375 fn regex_compile_uses_checked_abi_and_table_allocation_paths() {
1376 let production = include_str!("regex_compile.rs")
1377 .split("#[cfg(test)]")
1378 .next()
1379 .expect("Fix: regex_compile.rs must contain production section");
1380
1381 assert!(
1382 production.contains("u32::try_from(pid)")
1383 && production.contains("u32::try_from(frag.match_len)")
1384 && production.contains("u32::try_from(builder.state_count())")
1385 && production.contains("u32::try_from(self.state_count)")
1386 && production.contains("checked_add(1)")
1387 && production.contains("try_reserve_vec_to_capacity")
1388 && !production.contains("pid as u32")
1389 && !production.contains("frag.match_len as u32")
1390 && !production.contains("builder.state_count() as u32")
1391 && !production.contains("self.state_count as u32")
1392 && !production.contains("vec![0u32;")
1393 && !production.contains("Vec::with_capacity(patterns.len())"),
1394 "Fix: regex compilation must not truncate ids/counts or allocate NFA tables with infallible zero-vector construction."
1395 );
1396 }
1397
1398 #[test]
1399 fn regex_pipeline_uses_compiled_plan_instead_of_literal_source_plan() {
1400 let compiled = compile_regex_set(&["a|bc"]).unwrap();
1401 let pipeline = build_rule_pipeline_from_regex(&["a|bc"], "input", "hits", 64).unwrap();
1402
1403 assert_eq!(pipeline.plan.num_states, compiled.plan.num_states);
1404 assert_eq!(
1405 pipeline.plan.accept_state_ids,
1406 compiled.plan.accept_state_ids
1407 );
1408 assert_eq!(
1409 pipeline.epsilon_table.iter().any(|word| *word != 0),
1410 compiled.epsilon_table.iter().any(|word| *word != 0)
1411 );
1412 assert_ne!(
1413 pipeline.plan.num_states,
1414 crate::scan::nfa::compile(&["a|bc"]).num_states,
1415 "regex pipeline must not rebuild the scan program from literal regex source bytes"
1416 );
1417 }
1418
1419 #[test]
1420 fn states_count_grows_with_concat() {
1421 let one = states_of("a");
1422 let two = states_of("ab");
1423 let three = states_of("abc");
1424 assert!(two > one);
1425 assert!(three > two);
1426 }
1427
1428 #[test]
1429 fn state_cap_enforced() {
1430 let huge: String = (0..(STATE_CAP + 4)).map(|_| 'a').collect();
1433 let err = compile_regex_set(&[&huge]).unwrap_err();
1434 assert!(matches!(err, RegexCompileError::TooManyStates { .. }));
1435 }
1436
1437 #[test]
1438 fn unsupported_regex_diagnostic_does_not_route_to_cpu_backend() {
1439 let err = compile_regex_set(&[r"\bsecret\b"]).unwrap_err();
1440 let message = err.to_string().to_ascii_lowercase();
1441 assert!(
1442 !message.contains("cpu"),
1443 "unsupported GPU-NFA regex diagnostics must not recommend host-side routing: {message}"
1444 );
1445 assert!(
1446 message.contains("gpu"),
1447 "unsupported GPU-NFA regex diagnostics must name the GPU-compatible rewrite contract: {message}"
1448 );
1449 }
1450
1451 #[test]
1458 fn unicode_class_outside_ascii_compiles_via_utf8_expansion() {
1459 let pat = "[hнһh]f_[a-zA-Z0-9]{4}";
1463 let result = compile_regex_set(&[pat]);
1464 let compiled = match result {
1465 Ok(c) => c,
1466 Err(e) => {
1467 panic!("unicode-extended character class must compile via UTF-8 expansion; got {e}")
1468 }
1469 };
1470 assert!(
1476 compiled.plan.num_states > 4,
1477 "expanded NFA must have non-trivial state count"
1478 );
1479 assert_eq!(compiled.plan.accept_states.len(), 1);
1483 }
1484
1485 #[test]
1489 fn ascii_only_class_keeps_single_byte_transition_path() {
1490 let r = compile_regex_set(&["[ab]"]).unwrap();
1493 assert_eq!(
1494 r.plan.num_states, 3,
1495 "[ab] must stay on the single-transition fast path (entry + 2 class states); got {} states",
1496 r.plan.num_states
1497 );
1498 }
1499
1500 #[test]
1504 fn unicode_class_above_expansion_cap_errors_cleanly() {
1505 let pat = "[\u{0100}-\u{0200}]";
1507 let err = compile_regex_set(&[pat]).unwrap_err();
1508 match err {
1509 RegexCompileError::Unsupported { feature, .. } => {
1510 assert!(
1511 feature.contains("expansion cap"),
1512 "over-cap expansion must name the cap in its diagnostic: {feature}"
1513 );
1514 }
1515 other => panic!("expected Unsupported expansion-cap error, got {other:?}"),
1516 }
1517 }
1518
1519 #[test]
1523 fn regex_compile_diagnostic_codes() {
1524 let look_err = compile_regex_set(&[r"a\bc"]).expect_err("word boundary is unsupported");
1526 assert_eq!(
1527 look_err.diagnostic_code(),
1528 Some("VYRE_SCAN_APPROXIMATED_LOOKAROUND_REQUIRES_VERIFIER"),
1529 "non-edge lookaround must map to its verifier diagnostic code; error was: {look_err}"
1530 );
1531
1532 let uni_err =
1534 compile_regex_set(&["[\u{0100}-\u{0200}]"]).expect_err("over-cap unicode class");
1535 assert_eq!(
1536 uni_err.diagnostic_code(),
1537 Some("VYRE_SCAN_UNSUPPORTED_UNICODE_MODE_GPU"),
1538 "over-cap unicode class must map to its diagnostic code; error was: {uni_err}"
1539 );
1540
1541 assert!(
1543 compile_regex_set(&["^abc$"]).is_ok(),
1544 "start/end anchors must compile, not be flagged as unsupported lookaround"
1545 );
1546
1547 let parse_err = compile_regex_set(&["("]).expect_err("unbalanced group is a parse error");
1549 assert_eq!(
1550 parse_err.diagnostic_code(),
1551 None,
1552 "a parse error must not claim a registry diagnostic code"
1553 );
1554
1555 let backref_err =
1558 compile_regex_set(&[r"(a)\1"]).expect_err("backreferences are unsupported");
1559 assert_eq!(
1560 backref_err.diagnostic_code(),
1561 Some("VYRE_SCAN_UNSUPPORTED_BACKREFERENCE"),
1562 "a backreference must map to its distinct code, not fall back to Parse; error was: {backref_err}"
1563 );
1564
1565 let huge: String = (0..(MAX_ALTERNATION_ARMS + 8))
1568 .map(|i| format!("v{i}"))
1569 .collect::<Vec<_>>()
1570 .join("|");
1571 let alt_err = compile_regex_set(&[huge.as_str()]).expect_err("over-budget alternation");
1572 assert_eq!(
1573 alt_err.diagnostic_code(),
1574 Some("VYRE_SCAN_UNSUPPORTED_HUGE_ALTERNATION_BUDGET"),
1575 "a huge alternation must map to its budget code, not TooManyStates; error was: {alt_err}"
1576 );
1577
1578 let nested_err =
1581 compile_regex_set(&[r"(?:a{40}){40}"]).expect_err("nested-repeat unroll blowup");
1582 assert_eq!(
1583 nested_err.diagnostic_code(),
1584 Some("VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET"),
1585 "nested bounded repeats must map to their budget code; error was: {nested_err}"
1586 );
1587 }
1588
1589 #[test]
1593 fn backreference_detector_is_escaping_aware() {
1594 assert!(pattern_uses_backreference(r"\1"));
1596 assert!(pattern_uses_backreference(r"(a)\1"));
1597 assert!(pattern_uses_backreference(r"foo\9bar"));
1598 assert!(pattern_uses_backreference(r"\k<name>"));
1600 assert!(pattern_uses_backreference(r"\k'name'"));
1601 assert!(pattern_uses_backreference("(?P=name)"));
1602
1603 assert!(!pattern_uses_backreference(r"\0"));
1607 assert!(
1608 !pattern_uses_backreference(r"\\1"),
1609 "an escaped backslash then a literal 1 is not a backreference"
1610 );
1611 assert!(!pattern_uses_backreference(r"\d+\w*"));
1612 assert!(!pattern_uses_backreference(r"[a-z]{3}"));
1613 assert!(!pattern_uses_backreference("plain text"));
1614 assert!(pattern_uses_backreference(r"\\\1"));
1616 }
1617
1618 #[test]
1622 fn captures_compile_and_surface_the_verifier_diagnostic() {
1623 let with_cap = compile_regex_set(&[r"(abc)def"]).expect("captures compile for whole-match");
1626 assert!(with_cap.captures_present, "the capture group must be noted");
1627 assert_eq!(
1628 with_cap.capture_extraction_diagnostic_code(),
1629 Some("VYRE_SCAN_CAPTURE_EXTRACTION_REQUIRES_VERIFIER"),
1630 "a captured pattern must surface the capture-verifier code without erroring"
1631 );
1632
1633 let no_cap = compile_regex_set(&[r"abcdef"]).expect("plain pattern compiles");
1635 assert!(!no_cap.captures_present);
1636 assert_eq!(no_cap.capture_extraction_diagnostic_code(), None);
1637
1638 let noncap = compile_regex_set(&[r"(?:abc)def"]).expect("non-capturing group compiles");
1640 assert!(
1641 !noncap.captures_present,
1642 "a (?:…) non-capturing group must not be flagged as a capture"
1643 );
1644 }
1645
1646 #[test]
1650 fn budget_reclassification_does_not_regress_compiling_patterns() {
1651 let ok_alt: String = ('a'..='z')
1655 .chain('A'..='Z')
1656 .chain('0'..='9')
1657 .map(|c| c.to_string())
1658 .collect::<Vec<_>>()
1659 .join("|");
1660 let compiled = compile_regex_set(&[ok_alt.as_str()])
1661 .expect("a 62-arm single-byte alternation must still compile");
1662 assert!(compiled.plan.num_states > 0);
1664
1665 assert!(
1668 compile_regex_set(&[r"(?:a{20}){20}"]).is_ok(),
1669 "a nested repeat under the unroll budget must still compile"
1670 );
1671
1672 assert_eq!(
1674 regex_construct_diagnostic_code(RegexConstruct::Backreference),
1675 "VYRE_SCAN_UNSUPPORTED_BACKREFERENCE"
1676 );
1677 assert_eq!(
1678 regex_construct_diagnostic_code(RegexConstruct::NestedRepeats),
1679 "VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET"
1680 );
1681 }
1682
1683 #[test]
1691 fn every_compile_error_variant_names_its_owner_and_fix_path() {
1692 let variants = [
1693 RegexCompileError::Parse {
1694 pattern_index: 0,
1695 message: "unclosed group".to_string(),
1696 },
1697 RegexCompileError::Unsupported {
1698 pattern_index: 1,
1699 feature: "backreference",
1700 },
1701 RegexCompileError::TooManyStates {
1702 states: 5_000,
1703 cap: 1_024,
1704 },
1705 RegexCompileError::PatternCountOverflow { count: usize::MAX },
1706 RegexCompileError::MatchLengthOverflow {
1707 pattern_index: 2,
1708 len: usize::MAX,
1709 },
1710 RegexCompileError::TableWordCountOverflow {
1711 table: "transition",
1712 },
1713 RegexCompileError::StorageReserveFailed {
1714 field: "epsilon",
1715 requested: 9,
1716 message: "allocator refused".to_string(),
1717 },
1718 ];
1719
1720 fn assert_covers_every_variant(error: &RegexCompileError) {
1725 match error {
1726 RegexCompileError::Parse { .. }
1727 | RegexCompileError::Unsupported { .. }
1728 | RegexCompileError::TooManyStates { .. }
1729 | RegexCompileError::PatternCountOverflow { .. }
1730 | RegexCompileError::MatchLengthOverflow { .. }
1731 | RegexCompileError::TableWordCountOverflow { .. }
1732 | RegexCompileError::StorageReserveFailed { .. } => {}
1733 }
1734 }
1735
1736 for error in &variants {
1737 assert_covers_every_variant(error);
1738 let rendered = error.to_string();
1739 assert!(
1740 rendered.starts_with("regex_compile:"),
1741 "a RegexCompileError variant lacks the `regex_compile:` owner prefix: {rendered}"
1742 );
1743 assert!(
1744 rendered.contains("Fix:"),
1745 "a RegexCompileError variant lacks a `Fix:` remedy clause: {rendered}"
1746 );
1747 }
1748 }
1749}