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;
41pub const DEFAULT_OPEN_ENDED_REPLAY_LIMIT_BYTES: u32 = 4096;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct RegexReplayPolicy {
50 pub open_ended_limit_bytes: u32,
53}
54
55impl Default for RegexReplayPolicy {
56 fn default() -> Self {
57 Self {
58 open_ended_limit_bytes: DEFAULT_OPEN_ENDED_REPLAY_LIMIT_BYTES,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct RegexPatternExtent {
66 pub min_bytes: u32,
68 pub max_bytes: Option<u32>,
70 pub replay_limit_bytes: u32,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88pub enum CaptureMode {
89 NonCapture,
91 Count,
93 Span,
95 NamedCapture,
98 RepeatedCapture,
101 GroupExtraction,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub struct CaptureModeContract {
112 pub mode_id: &'static str,
114 pub output_shape: &'static str,
116 pub accelerator_eligible: bool,
118 pub verifier_required: bool,
120 pub null_policy: &'static str,
122}
123
124impl CaptureMode {
125 pub const ALL: [CaptureMode; 6] = [
127 CaptureMode::NonCapture,
128 CaptureMode::Count,
129 CaptureMode::Span,
130 CaptureMode::NamedCapture,
131 CaptureMode::RepeatedCapture,
132 CaptureMode::GroupExtraction,
133 ];
134
135 #[must_use]
138 pub const fn contract_row(self) -> CaptureModeContract {
139 match self {
140 CaptureMode::NonCapture => CaptureModeContract {
141 mode_id: "noncapture",
142 output_shape: "whole_match_only",
143 accelerator_eligible: true,
144 verifier_required: false,
145 null_policy: "not_applicable",
146 },
147 CaptureMode::Count => CaptureModeContract {
148 mode_id: "count",
149 output_shape: "match_count_per_pattern",
150 accelerator_eligible: true,
151 verifier_required: false,
152 null_policy: "not_applicable",
153 },
154 CaptureMode::Span => CaptureModeContract {
155 mode_id: "span",
156 output_shape: "whole_match_span",
157 accelerator_eligible: true,
158 verifier_required: false,
159 null_policy: "absent-match-has-no-span",
160 },
161 CaptureMode::NamedCapture => CaptureModeContract {
162 mode_id: "named_capture",
163 output_shape: "named_group_span_records",
164 accelerator_eligible: false,
165 verifier_required: true,
166 null_policy: "unmatched-group-null",
167 },
168 CaptureMode::RepeatedCapture => CaptureModeContract {
169 mode_id: "repeated_capture",
170 output_shape: "ordered_group_span_list",
171 accelerator_eligible: false,
172 verifier_required: true,
173 null_policy: "empty-repeat-yields-empty-list",
174 },
175 CaptureMode::GroupExtraction => CaptureModeContract {
176 mode_id: "group_extraction",
177 output_shape: "row_group_value_table",
178 accelerator_eligible: false,
179 verifier_required: true,
180 null_policy: "unmatched-group-null",
181 },
182 }
183 }
184
185 #[must_use]
188 pub const fn accelerator_eligible(self) -> bool {
189 self.contract_row().accelerator_eligible
190 }
191
192 #[must_use]
198 pub const fn verifier_required(self) -> bool {
199 self.contract_row().verifier_required
200 }
201
202 #[must_use]
206 pub fn from_mode_id(mode_id: &str) -> Option<CaptureMode> {
207 CaptureMode::ALL
208 .into_iter()
209 .find(|mode| mode.contract_row().mode_id == mode_id)
210 }
211}
212
213#[derive(Debug, Clone)]
216#[non_exhaustive]
217pub enum RegexCompileError {
218 Parse {
221 pattern_index: usize,
223 message: String,
225 },
226 Unsupported {
230 pattern_index: usize,
232 feature: &'static str,
234 },
235 TooManyStates {
239 states: usize,
241 cap: usize,
243 },
244 PatternCountOverflow {
246 count: usize,
248 },
249 MatchLengthOverflow {
251 pattern_index: usize,
253 len: usize,
255 },
256 MatchLengthArithmeticOverflow {
258 pattern_index: usize,
260 },
261 OpenEndedReplayLimitTooSmall {
263 pattern_index: usize,
265 minimum: u32,
267 limit: u32,
269 },
270 TableWordCountOverflow {
272 table: &'static str,
274 },
275 StorageReserveFailed {
277 field: &'static str,
279 requested: usize,
281 message: String,
283 },
284}
285
286impl RegexCompileError {
287 #[must_use]
305 pub fn diagnostic_code(&self) -> Option<&'static str> {
306 match self {
307 Self::Unsupported { feature, .. } => {
311 regex_feature_construct(feature).map(regex_construct_diagnostic_code)
312 }
313 _ => None,
314 }
315 }
316}
317
318#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327#[non_exhaustive]
328pub enum RegexConstruct {
329 Backreference,
331 Lookaround,
333 UnicodeClassesGpu,
335 CaptureExtraction,
338 HugeAlternation,
340 NestedRepeats,
342}
343
344#[must_use]
347pub fn regex_construct_diagnostic_code(construct: RegexConstruct) -> &'static str {
348 match construct {
349 RegexConstruct::Backreference => "VYRE_SCAN_UNSUPPORTED_BACKREFERENCE",
350 RegexConstruct::Lookaround => "VYRE_SCAN_APPROXIMATED_LOOKAROUND_REQUIRES_VERIFIER",
351 RegexConstruct::UnicodeClassesGpu => "VYRE_SCAN_UNSUPPORTED_UNICODE_MODE_GPU",
352 RegexConstruct::CaptureExtraction => "VYRE_SCAN_CAPTURE_EXTRACTION_REQUIRES_VERIFIER",
353 RegexConstruct::HugeAlternation => "VYRE_SCAN_UNSUPPORTED_HUGE_ALTERNATION_BUDGET",
354 RegexConstruct::NestedRepeats => "VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET",
355 }
356}
357
358const FEATURE_LOOKAROUND: &str = "non-edge lookaround assertion";
362const FEATURE_UNICODE_CLASS_CAP: &str = "unicode character class exceeded expansion cap";
363const FEATURE_BACKREFERENCE: &str = "backreference";
364const FEATURE_HUGE_ALTERNATION: &str = "huge alternation exceeds budget";
365const FEATURE_NESTED_REPEATS: &str = "nested repeat exceeds budget";
366
367fn regex_feature_construct(feature: &str) -> Option<RegexConstruct> {
371 match feature {
372 FEATURE_LOOKAROUND => Some(RegexConstruct::Lookaround),
373 FEATURE_UNICODE_CLASS_CAP => Some(RegexConstruct::UnicodeClassesGpu),
374 FEATURE_BACKREFERENCE => Some(RegexConstruct::Backreference),
375 FEATURE_HUGE_ALTERNATION => Some(RegexConstruct::HugeAlternation),
376 FEATURE_NESTED_REPEATS => Some(RegexConstruct::NestedRepeats),
377 _ => None,
378 }
379}
380
381impl std::fmt::Display for RegexCompileError {
382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 match self {
384 Self::Parse {
385 pattern_index,
386 message,
387 } => write!(
388 f,
389 "regex_compile: pattern {pattern_index} parse error: {message}. \
390 Fix: review the regex syntax."
391 ),
392 Self::Unsupported {
393 pattern_index,
394 feature,
395 } => write!(
396 f,
397 "regex_compile: pattern {pattern_index} uses unsupported feature `{feature}`. \
398 Fix: rewrite the detector into supported GPU-NFA syntax or split it into GPU-compatible rules."
399 ),
400 Self::TooManyStates { states, cap } => write!(
401 f,
402 "regex_compile: NFA needs {states} states; per-pipeline cap is {cap}. \
403 Fix: split the pattern set across multiple pipelines."
404 ),
405 Self::PatternCountOverflow { count } => write!(
406 f,
407 "regex_compile: pattern count {count} exceeds u32 capacity. Fix: shard the pattern set before GPU regex compilation."
408 ),
409 Self::MatchLengthOverflow {
410 pattern_index,
411 len,
412 } => write!(
413 f,
414 "regex_compile: pattern {pattern_index} match length {len} exceeds u32 capacity. Fix: bound or shard the regex before GPU compilation."
415 ),
416 Self::MatchLengthArithmeticOverflow { pattern_index } => write!(
417 f,
418 "regex_compile: pattern {pattern_index} match-length arithmetic overflowed host usize. Fix: reduce repetition bounds before GPU compilation."
419 ),
420 Self::OpenEndedReplayLimitTooSmall {
421 pattern_index,
422 minimum,
423 limit,
424 } => write!(
425 f,
426 "regex_compile: pattern {pattern_index} needs at least {minimum} byte(s), but the open-ended replay limit is {limit}. Fix: raise RegexReplayPolicy::open_ended_limit_bytes to at least {minimum}."
427 ),
428 Self::TableWordCountOverflow { table } => write!(
429 f,
430 "regex_compile: {table} table word count overflows host usize. Fix: shard the regex pattern set before table construction."
431 ),
432 Self::StorageReserveFailed {
433 field,
434 requested,
435 message,
436 } => write!(
437 f,
438 "regex_compile: could not reserve {requested} {field} slot(s): {message}. Fix: shard the regex pattern set before GPU compilation."
439 ),
440 }
441 }
442}
443
444impl std::error::Error for RegexCompileError {}
445
446#[derive(Debug, Clone)]
451pub struct CompiledRegexSet {
452 pub plan: NfaPlan,
454 pub transition_table: Vec<u32>,
457 pub epsilon_table: Vec<u32>,
460 pub pattern_extents: Vec<RegexPatternExtent>,
465 pub captures_present: bool,
476}
477
478impl CompiledRegexSet {
479 #[must_use]
487 pub fn capture_extraction_diagnostic_code(&self) -> Option<&'static str> {
488 self.captures_present
489 .then_some(regex_construct_diagnostic_code(
490 RegexConstruct::CaptureExtraction,
491 ))
492 }
493}
494
495const STATE_CAP: usize = LANES * 32;
496
497const MAX_ALTERNATION_ARMS: usize = STATE_CAP;
504
505const NESTED_REPEAT_UNROLL_BUDGET: u64 = STATE_CAP as u64;
512
513struct ConstructScan {
515 captures_present: bool,
518}
519
520fn scan_constructs(
526 hir: &Hir,
527 pid: usize,
528 scan: &mut ConstructScan,
529) -> Result<u64, RegexCompileError> {
530 match hir.kind() {
531 HirKind::Alternation(alts) => {
532 if alts.len() > MAX_ALTERNATION_ARMS {
533 return Err(RegexCompileError::Unsupported {
534 pattern_index: pid,
535 feature: FEATURE_HUGE_ALTERNATION,
536 });
537 }
538 let mut worst = 1u64;
539 for a in alts {
540 worst = worst.max(scan_constructs(a, pid, scan)?);
541 }
542 Ok(worst)
543 }
544 HirKind::Concat(parts) => {
545 let mut worst = 1u64;
546 for p in parts {
547 worst = worst.max(scan_constructs(p, pid, scan)?);
548 }
549 Ok(worst)
550 }
551 HirKind::Repetition(rep) => {
552 let inner = scan_constructs(&rep.sub, pid, scan)?;
553 match rep.max {
554 Some(m) => {
555 let product = u64::from(m).saturating_mul(inner.max(1));
556 if inner > 1 && product > NESTED_REPEAT_UNROLL_BUDGET {
561 return Err(RegexCompileError::Unsupported {
562 pattern_index: pid,
563 feature: FEATURE_NESTED_REPEATS,
564 });
565 }
566 Ok(product)
567 }
568 None => Ok(inner.max(1)),
571 }
572 }
573 HirKind::Capture(c) => {
574 scan.captures_present = true;
575 scan_constructs(&c.sub, pid, scan)
576 }
577 _ => Ok(1),
578 }
579}
580
581fn pattern_uses_backreference(pat: &str) -> bool {
589 let bytes = pat.as_bytes();
590 let mut i = 0;
591 while i < bytes.len() {
592 match bytes[i] {
593 b'\\' => {
594 if let Some(&c) = bytes.get(i + 1) {
595 if c.is_ascii_digit() && c != b'0' {
597 return true;
598 }
599 if c == b'k' && matches!(bytes.get(i + 2), Some(b'<' | b'\'' | b'{')) {
601 return true;
602 }
603 }
604 i += 2;
606 }
607 b'(' if pat[i..].starts_with("(?P=") => return true,
610 _ => i += 1,
611 }
612 }
613 false
614}
615#[derive(Debug, Clone, Copy, PartialEq, Eq)]
616struct MatchExtent {
617 min: usize,
618 max: Option<usize>,
619}
620
621fn extent_overflow(pattern_index: usize) -> RegexCompileError {
622 RegexCompileError::MatchLengthArithmeticOverflow { pattern_index }
623}
624
625fn checked_extent_add(
626 left: usize,
627 right: usize,
628 pattern_index: usize,
629) -> Result<usize, RegexCompileError> {
630 left.checked_add(right)
631 .ok_or_else(|| extent_overflow(pattern_index))
632}
633
634fn checked_extent_mul(
635 left: usize,
636 right: usize,
637 pattern_index: usize,
638) -> Result<usize, RegexCompileError> {
639 left.checked_mul(right)
640 .ok_or_else(|| extent_overflow(pattern_index))
641}
642
643fn analyze_match_extent(hir: &Hir, pattern_index: usize) -> Result<MatchExtent, RegexCompileError> {
644 match hir.kind() {
645 HirKind::Empty | HirKind::Look(_) => Ok(MatchExtent {
646 min: 0,
647 max: Some(0),
648 }),
649 HirKind::Literal(literal) => Ok(MatchExtent {
650 min: literal.0.len(),
651 max: Some(literal.0.len()),
652 }),
653 HirKind::Class(class) => {
654 if try_class_as_ascii_byte_set(class).is_some() {
655 return Ok(MatchExtent {
656 min: 1,
657 max: Some(1),
658 });
659 }
660 let sequences = class_to_utf8_sequences(class, pattern_index)?;
661 let min = sequences.iter().map(Vec::len).min().unwrap_or(0);
662 let max = sequences.iter().map(Vec::len).max().unwrap_or(0);
663 Ok(MatchExtent {
664 min,
665 max: Some(max),
666 })
667 }
668 HirKind::Capture(capture) => analyze_match_extent(&capture.sub, pattern_index),
669 HirKind::Concat(parts) => {
670 let mut extent = MatchExtent {
671 min: 0,
672 max: Some(0),
673 };
674 for part in parts {
675 let next = analyze_match_extent(part, pattern_index)?;
676 extent.min = checked_extent_add(extent.min, next.min, pattern_index)?;
677 extent.max = match (extent.max, next.max) {
678 (Some(left), Some(right)) => {
679 Some(checked_extent_add(left, right, pattern_index)?)
680 }
681 _ => None,
682 };
683 }
684 Ok(extent)
685 }
686 HirKind::Alternation(alternatives) => {
687 let mut min = usize::MAX;
688 let mut max = Some(0usize);
689 for alternative in alternatives {
690 let extent = analyze_match_extent(alternative, pattern_index)?;
691 min = min.min(extent.min);
692 max = match (max, extent.max) {
693 (Some(left), Some(right)) => Some(left.max(right)),
694 _ => None,
695 };
696 }
697 Ok(MatchExtent {
698 min: if alternatives.is_empty() { 0 } else { min },
699 max,
700 })
701 }
702 HirKind::Repetition(repetition) => {
703 let sub = analyze_match_extent(&repetition.sub, pattern_index)?;
704 let min = checked_extent_mul(sub.min, repetition.min as usize, pattern_index)?;
705 let max = match repetition.max {
706 Some(0) => Some(0),
707 Some(count) => match sub.max {
708 Some(sub_max) => {
709 Some(checked_extent_mul(sub_max, count as usize, pattern_index)?)
710 }
711 None => None,
712 },
713 None if sub.max == Some(0) => Some(0),
714 None => None,
715 };
716 Ok(MatchExtent { min, max })
717 }
718 }
719}
720
721pub fn compile_regex_set(patterns: &[&str]) -> Result<CompiledRegexSet, RegexCompileError> {
726 compile_regex_set_with_policy(patterns, RegexReplayPolicy::default())
727}
728
729pub fn compile_regex_set_with_policy(
734 patterns: &[&str],
735 replay_policy: RegexReplayPolicy,
736) -> Result<CompiledRegexSet, RegexCompileError> {
737 compile_regex_set_inner(patterns, replay_policy)
738}
739
740fn compile_regex_set_inner(
741 patterns: &[&str],
742 replay_policy: RegexReplayPolicy,
743) -> Result<CompiledRegexSet, RegexCompileError> {
744 let mut builder = NfaBuilder::new();
745 let _pattern_count =
746 u32::try_from(patterns.len()).map_err(|_| RegexCompileError::PatternCountOverflow {
747 count: patterns.len(),
748 })?;
749 let mut accept_states = Vec::new();
750 reserve_vec(&mut accept_states, patterns.len(), "accept state")?;
751 let mut accept_state_ids = Vec::new();
752 reserve_vec(&mut accept_state_ids, patterns.len(), "accept state id")?;
753 let mut accept_start_anchored = Vec::new();
754 reserve_vec(
755 &mut accept_start_anchored,
756 patterns.len(),
757 "accept start-anchor flag",
758 )?;
759 let mut accept_end_anchored = Vec::new();
760 reserve_vec(
761 &mut accept_end_anchored,
762 patterns.len(),
763 "accept end-anchor flag",
764 )?;
765 let mut pattern_extents = Vec::new();
766 reserve_vec(&mut pattern_extents, patterns.len(), "pattern extent")?;
767 let entry = builder.fresh_state()?; let mut captures_present = false;
769
770 for (pid, pat) in patterns.iter().enumerate() {
776 let hir = match regex_syntax::ParserBuilder::new()
784 .unicode(false)
785 .utf8(false)
786 .build()
787 .parse(pat)
788 {
789 Ok(h) => h,
790 Err(byte_mode_err) => match regex_syntax::ParserBuilder::new()
791 .unicode(true)
792 .utf8(false)
793 .build()
794 .parse(pat)
795 {
796 Ok(h) => h,
797 Err(_unicode_err) => {
798 if pattern_uses_backreference(pat) {
806 return Err(RegexCompileError::Unsupported {
807 pattern_index: pid,
808 feature: FEATURE_BACKREFERENCE,
809 });
810 }
811 return Err(RegexCompileError::Parse {
812 pattern_index: pid,
813 message: format!("{byte_mode_err}"),
814 });
815 }
816 },
817 };
818 let mut construct_scan = ConstructScan {
822 captures_present: false,
823 };
824 scan_constructs(&hir, pid, &mut construct_scan)?;
825 captures_present |= construct_scan.captures_present;
826 let extent = analyze_match_extent(&hir, pid)?;
827 let (frag, anchors) = build_pattern_hir(&mut builder, &hir, pid)?;
828 builder.add_epsilon(entry, frag.start);
830 let pid_u32 = u32::try_from(pid).map_err(|_| RegexCompileError::PatternCountOverflow {
831 count: patterns.len(),
832 })?;
833 let min_bytes =
834 u32::try_from(extent.min).map_err(|_| RegexCompileError::MatchLengthOverflow {
835 pattern_index: pid,
836 len: extent.min,
837 })?;
838 let max_bytes = extent
839 .max
840 .map(|max| {
841 u32::try_from(max).map_err(|_| RegexCompileError::MatchLengthOverflow {
842 pattern_index: pid,
843 len: max,
844 })
845 })
846 .transpose()?;
847 let replay_limit_bytes = match max_bytes {
848 Some(max) => max,
849 None => {
850 let required = min_bytes.max(1);
851 if replay_policy.open_ended_limit_bytes < required {
852 return Err(RegexCompileError::OpenEndedReplayLimitTooSmall {
853 pattern_index: pid,
854 minimum: required,
855 limit: replay_policy.open_ended_limit_bytes,
856 });
857 }
858 replay_policy.open_ended_limit_bytes
859 }
860 };
861 accept_states.push((pid_u32, replay_limit_bytes));
862 pattern_extents.push(RegexPatternExtent {
863 min_bytes,
864 max_bytes,
865 replay_limit_bytes,
866 });
867 accept_state_ids.push(frag.end);
868 accept_start_anchored.push(anchors.start);
869 accept_end_anchored.push(anchors.end);
870 }
871
872 if builder.state_count() > STATE_CAP {
873 return Err(RegexCompileError::TooManyStates {
874 states: builder.state_count(),
875 cap: STATE_CAP,
876 });
877 }
878
879 let plan = NfaPlan {
880 num_states: u32::try_from(builder.state_count()).map_err(|_| {
881 RegexCompileError::TooManyStates {
882 states: builder.state_count(),
883 cap: STATE_CAP,
884 }
885 })?,
886 input_len: 0,
887 accept_states,
888 accept_state_ids,
889 accept_start_anchored,
890 accept_end_anchored,
891 };
892 let (transition_table, epsilon_table) = builder.emit_lane_major_tables()?;
893 Ok(CompiledRegexSet {
894 plan,
895 transition_table,
896 epsilon_table,
897 pattern_extents,
898 captures_present,
899 })
900}
901
902pub fn build_scan_program_from_regex(
910 patterns: &[&str],
911 input_buf: &str,
912 hit_buf: &str,
913 input_len: u32,
914) -> Result<crate::scan::ScanProgram, RegexCompileError> {
915 let compiled = compile_regex_set(patterns)?;
916 let has_epsilon = compiled.epsilon_table.iter().any(|word| *word != 0);
917 let program = crate::scan::nfa::nfa_scan_with_plan(
918 &compiled.plan,
919 has_epsilon,
920 input_buf,
921 hit_buf,
922 input_len,
923 )
924 .map_err(|_| RegexCompileError::TooManyStates {
925 states: compiled.plan.num_states as usize,
926 cap: STATE_CAP,
927 })?;
928 Ok(crate::scan::ScanProgram {
929 program,
930 transition_table: compiled.transition_table,
931 epsilon_table: compiled.epsilon_table,
932 plan: compiled.plan.for_input_len(input_len),
933 })
934}
935
936#[derive(Debug)]
939struct NfaBuilder {
940 state_count: usize,
941 transitions: Vec<ByteTransition>,
944 epsilons: Vec<(u32, u32)>,
946}
947
948#[derive(Debug, Clone)]
949struct ByteTransition {
950 src: u32,
951 set: ByteSet,
952 dst: u32,
953}
954
955#[derive(Debug, Clone)]
956struct ByteSet {
957 bits: [u64; 4], }
959
960impl ByteSet {
961 fn new() -> Self {
962 Self { bits: [0; 4] }
963 }
964 fn insert(&mut self, b: u8) {
965 self.bits[(b / 64) as usize] |= 1u64 << (b % 64);
966 }
967 fn from_byte(b: u8) -> Self {
968 let mut s = Self::new();
969 s.insert(b);
970 s
971 }
972 fn from_range(lo: u8, hi: u8) -> Self {
973 let mut s = Self::new();
974 for b in lo..=hi {
975 s.insert(b);
976 }
977 s
978 }
979 fn for_each_set_byte(&self, mut f: impl FnMut(u8)) {
980 for (word_idx, &word) in self.bits.iter().enumerate() {
981 let mut bits = word;
982 while bits != 0 {
983 let bit = bits.trailing_zeros() as usize;
984 f((word_idx * 64 + bit) as u8);
985 bits &= bits - 1;
986 }
987 }
988 }
989}
990
991#[derive(Debug, Clone, Copy)]
992struct Fragment {
993 start: u32,
994 end: u32,
995 match_len: usize,
998}
999
1000#[derive(Debug, Clone, Copy, Default)]
1001struct PatternAnchors {
1002 start: bool,
1003 end: bool,
1004}
1005
1006impl NfaBuilder {
1007 fn new() -> Self {
1008 Self {
1009 state_count: 0,
1010 transitions: Vec::new(),
1011 epsilons: Vec::new(),
1012 }
1013 }
1014
1015 fn state_count(&self) -> usize {
1016 self.state_count
1017 }
1018
1019 fn fresh_state(&mut self) -> Result<u32, RegexCompileError> {
1020 if self.state_count >= STATE_CAP {
1021 return Err(RegexCompileError::TooManyStates {
1022 states: self.state_count.saturating_add(1),
1023 cap: STATE_CAP,
1024 });
1025 }
1026 let state =
1027 u32::try_from(self.state_count).map_err(|_| RegexCompileError::TooManyStates {
1028 states: self.state_count,
1029 cap: STATE_CAP,
1030 })?;
1031 self.state_count =
1032 self.state_count
1033 .checked_add(1)
1034 .ok_or(RegexCompileError::TooManyStates {
1035 states: usize::MAX,
1036 cap: STATE_CAP,
1037 })?;
1038 Ok(state)
1039 }
1040
1041 fn add_byte_transition(&mut self, src: u32, set: ByteSet, dst: u32) {
1042 self.transitions.push(ByteTransition { src, set, dst });
1043 }
1044
1045 fn add_epsilon(&mut self, src: u32, dst: u32) {
1046 self.epsilons.push((src, dst));
1047 }
1048
1049 fn emit_lane_major_tables(&self) -> Result<(Vec<u32>, Vec<u32>), RegexCompileError> {
1052 let n = self.state_count();
1053 let mut transitions = zeroed_u32_table(
1054 table_word_count(n, 256, "transition")?,
1055 "transition table word",
1056 )?;
1057 let mut epsilons =
1058 zeroed_u32_table(table_word_count(n, 1, "epsilon")?, "epsilon table word")?;
1059
1060 for edge in &self.transitions {
1061 let src = edge.src as usize;
1062 let dst_lane = (edge.dst / 32) as usize;
1063 let dst_bit = 1u32 << (edge.dst % 32);
1064 edge.set.for_each_set_byte(|byte| {
1065 let idx = src * 256 * LANES + (byte as usize) * LANES + dst_lane;
1066 transitions[idx] |= dst_bit;
1067 });
1068 }
1069 for &(src, dst) in &self.epsilons {
1070 let dst_lane = (dst / 32) as usize;
1071 let dst_bit = 1u32 << (dst % 32);
1072 let idx = src as usize * LANES + dst_lane;
1073 epsilons[idx] |= dst_bit;
1074 }
1075 Ok((transitions, epsilons))
1076 }
1077}
1078
1079fn table_word_count(
1080 states: usize,
1081 byte_columns: usize,
1082 table: &'static str,
1083) -> Result<usize, RegexCompileError> {
1084 states
1085 .checked_mul(byte_columns)
1086 .and_then(|words| words.checked_mul(LANES))
1087 .ok_or(RegexCompileError::TableWordCountOverflow { table })
1088}
1089
1090fn zeroed_u32_table(words: usize, field: &'static str) -> Result<Vec<u32>, RegexCompileError> {
1091 let mut table = Vec::new();
1092 reserve_vec(&mut table, words, field)?;
1093 table.resize(words, 0);
1094 Ok(table)
1095}
1096
1097fn reserve_vec<T>(
1098 vec: &mut Vec<T>,
1099 requested: usize,
1100 field: &'static str,
1101) -> Result<(), RegexCompileError> {
1102 vyre_foundation::allocation::try_reserve_vec_to_capacity(vec, requested).map_err(|source| {
1103 RegexCompileError::StorageReserveFailed {
1104 field,
1105 requested,
1106 message: source.to_string(),
1107 }
1108 })
1109}
1110
1111fn empty_fragment(b: &mut NfaBuilder) -> Result<Fragment, RegexCompileError> {
1112 let s = b.fresh_state()?;
1113 Ok(Fragment {
1114 start: s,
1115 end: s,
1116 match_len: 0,
1117 })
1118}
1119
1120fn build_pattern_hir(
1121 b: &mut NfaBuilder,
1122 hir: &Hir,
1123 pid: usize,
1124) -> Result<(Fragment, PatternAnchors), RegexCompileError> {
1125 match hir.kind() {
1126 HirKind::Look(Look::Start) => Ok((
1127 empty_fragment(b)?,
1128 PatternAnchors {
1129 start: true,
1130 end: false,
1131 },
1132 )),
1133 HirKind::Look(Look::End) => Ok((
1134 empty_fragment(b)?,
1135 PatternAnchors {
1136 start: false,
1137 end: true,
1138 },
1139 )),
1140 HirKind::Concat(parts) => {
1141 let mut first = 0usize;
1142 let mut last = parts.len();
1143 let mut anchors = PatternAnchors::default();
1144
1145 if first < last && is_text_start_look(&parts[first]) {
1146 anchors.start = true;
1147 first += 1;
1148 }
1149 if first < last && is_text_end_look(&parts[last - 1]) {
1150 anchors.end = true;
1151 last -= 1;
1152 }
1153
1154 Ok((build_hir_slice(b, &parts[first..last], pid)?, anchors))
1155 }
1156 _ => Ok((build_hir(b, hir, pid)?, PatternAnchors::default())),
1157 }
1158}
1159
1160fn is_text_start_look(hir: &Hir) -> bool {
1161 matches!(hir.kind(), HirKind::Look(Look::Start))
1162}
1163
1164fn is_text_end_look(hir: &Hir) -> bool {
1165 matches!(hir.kind(), HirKind::Look(Look::End))
1166}
1167
1168fn build_hir_slice(
1169 b: &mut NfaBuilder,
1170 parts: &[Hir],
1171 pid: usize,
1172) -> Result<Fragment, RegexCompileError> {
1173 let Some(first_part) = parts.first() else {
1174 return empty_fragment(b);
1175 };
1176 let mut acc = build_hir(b, first_part, pid)?;
1177 for child in &parts[1..] {
1178 let next = build_hir(b, child, pid)?;
1179 b.add_epsilon(acc.end, next.start);
1180 acc = Fragment {
1181 start: acc.start,
1182 end: next.end,
1183 match_len: acc.match_len + next.match_len,
1184 };
1185 }
1186 Ok(acc)
1187}
1188
1189fn build_hir(b: &mut NfaBuilder, hir: &Hir, pid: usize) -> Result<Fragment, RegexCompileError> {
1190 match hir.kind() {
1191 HirKind::Empty => empty_fragment(b),
1192 HirKind::Literal(lit) => {
1193 let start = b.fresh_state()?;
1195 let mut prev = start;
1196 for &byte in lit.0.iter() {
1197 let next = b.fresh_state()?;
1198 b.add_byte_transition(prev, ByteSet::from_byte(byte), next);
1199 prev = next;
1200 }
1201 Ok(Fragment {
1202 start,
1203 end: prev,
1204 match_len: lit.0.len(),
1205 })
1206 }
1207 HirKind::Class(cls) => build_class(b, cls, pid),
1208 HirKind::Repetition(rep) => build_repetition(b, rep, pid),
1209 HirKind::Concat(parts) => build_hir_slice(b, parts, pid),
1210 HirKind::Alternation(alts) => {
1211 let fork = b.fresh_state()?;
1213 let join = b.fresh_state()?;
1214 let mut max_len = 0usize;
1215 for child in alts {
1216 let frag = build_hir(b, child, pid)?;
1217 b.add_epsilon(fork, frag.start);
1218 b.add_epsilon(frag.end, join);
1219 if frag.match_len > max_len {
1220 max_len = frag.match_len;
1221 }
1222 }
1223 Ok(Fragment {
1224 start: fork,
1225 end: join,
1226 match_len: max_len,
1227 })
1228 }
1229 HirKind::Look(_) => Err(RegexCompileError::Unsupported {
1230 pattern_index: pid,
1231 feature: FEATURE_LOOKAROUND,
1232 }),
1233 HirKind::Capture(c) => {
1234 build_hir(b, &c.sub, pid)
1237 }
1238 }
1239}
1240
1241fn build_repetition(
1242 b: &mut NfaBuilder,
1243 rep: &Repetition,
1244 pid: usize,
1245) -> Result<Fragment, RegexCompileError> {
1246 let min = rep.min;
1247 let max = rep.max;
1248
1249 if let Some(m) = max {
1253 if m as usize > STATE_CAP {
1254 return Err(RegexCompileError::TooManyStates {
1255 states: m as usize,
1256 cap: STATE_CAP,
1257 });
1258 }
1259 }
1260 if min as usize > STATE_CAP {
1261 return Err(RegexCompileError::TooManyStates {
1262 states: min as usize,
1263 cap: STATE_CAP,
1264 });
1265 }
1266
1267 let start = b.fresh_state()?;
1271 let mut tail = start;
1272 let mut total_len = 0usize;
1273
1274 for _ in 0..min {
1275 let frag = build_hir(b, &rep.sub, pid)?;
1276 b.add_epsilon(tail, frag.start);
1277 tail = frag.end;
1278 total_len += frag.match_len;
1279 }
1280
1281 match max {
1282 None => {
1283 let join = b.fresh_state()?;
1286 let frag = build_hir(b, &rep.sub, pid)?;
1287 b.add_epsilon(tail, frag.start);
1288 b.add_epsilon(frag.end, frag.start); b.add_epsilon(frag.end, join);
1290 b.add_epsilon(tail, join); tail = join;
1292 }
1293 Some(m) => {
1294 for _ in min..m {
1295 let frag = build_hir(b, &rep.sub, pid)?;
1296 let join = b.fresh_state()?;
1297 b.add_epsilon(tail, frag.start);
1298 b.add_epsilon(frag.end, join);
1299 b.add_epsilon(tail, join); tail = join;
1301 total_len += frag.match_len;
1313 }
1314 }
1315 }
1316 Ok(Fragment {
1317 start,
1318 end: tail,
1319 match_len: total_len,
1320 })
1321}
1322
1323fn build_class(b: &mut NfaBuilder, cls: &Class, pid: usize) -> Result<Fragment, RegexCompileError> {
1344 if let Some(set) = try_class_as_ascii_byte_set(cls) {
1345 let start = b.fresh_state()?;
1346 let end = b.fresh_state()?;
1347 b.add_byte_transition(start, set, end);
1348 return Ok(Fragment {
1349 start,
1350 end,
1351 match_len: 1,
1352 });
1353 }
1354 let sequences = class_to_utf8_sequences(cls, pid)?;
1355 if sequences.is_empty() {
1356 return Err(RegexCompileError::Unsupported {
1357 pattern_index: pid,
1358 feature: "empty character class after Unicode expansion",
1359 });
1360 }
1361 let start = b.fresh_state()?;
1362 let end = b.fresh_state()?;
1363 let mut max_len = 1usize;
1364 for seq in &sequences {
1365 if seq.is_empty() {
1366 continue;
1367 }
1368 let arm_start = b.fresh_state()?;
1371 b.add_epsilon(start, arm_start);
1372 let mut prev = arm_start;
1373 for &byte in seq {
1374 let next = b.fresh_state()?;
1375 b.add_byte_transition(prev, ByteSet::from_byte(byte), next);
1376 prev = next;
1377 }
1378 b.add_epsilon(prev, end);
1379 if seq.len() > max_len {
1380 max_len = seq.len();
1381 }
1382 }
1383 Ok(Fragment {
1384 start,
1385 end,
1386 match_len: max_len,
1387 })
1388}
1389
1390fn try_class_as_ascii_byte_set(cls: &Class) -> Option<ByteSet> {
1394 let mut out = ByteSet::new();
1395 match cls {
1396 Class::Bytes(byte_class) => {
1397 for r in byte_class.iter() {
1401 let merged = ByteSet::from_range(r.start(), r.end());
1402 for w in 0..4 {
1403 out.bits[w] |= merged.bits[w];
1404 }
1405 }
1406 Some(out)
1407 }
1408 Class::Unicode(uni) => {
1409 for r in uni.iter() {
1412 if (r.end() as u32) > 0x7F {
1413 return None;
1414 }
1415 let merged = ByteSet::from_range(r.start() as u8, r.end() as u8);
1416 for w in 0..4 {
1417 out.bits[w] |= merged.bits[w];
1418 }
1419 }
1420 Some(out)
1421 }
1422 }
1423}
1424
1425const MAX_CLASS_EXPANSION_CODEPOINTS: usize = 256;
1432
1433fn class_to_utf8_sequences(cls: &Class, pid: usize) -> Result<Vec<Vec<u8>>, RegexCompileError> {
1438 let mut sequences: Vec<Vec<u8>> = Vec::new();
1439 let mut budget = MAX_CLASS_EXPANSION_CODEPOINTS;
1440 match cls {
1441 Class::Bytes(byte_class) => {
1442 for r in byte_class.iter() {
1443 for byte in r.start()..=r.end() {
1444 if budget == 0 {
1445 return Err(RegexCompileError::Unsupported {
1446 pattern_index: pid,
1447 feature: "byte character class exceeded expansion cap",
1448 });
1449 }
1450 sequences.push(vec![byte]);
1451 budget -= 1;
1452 }
1453 }
1454 }
1455 Class::Unicode(uni) => {
1456 for r in uni.iter() {
1457 let lo = r.start() as u32;
1458 let hi = r.end() as u32;
1459 for cp in lo..=hi {
1460 if budget == 0 {
1461 return Err(RegexCompileError::Unsupported {
1462 pattern_index: pid,
1463 feature: FEATURE_UNICODE_CLASS_CAP,
1464 });
1465 }
1466 if let Some(c) = char::from_u32(cp) {
1473 let mut buf = [0u8; 4];
1474 let encoded = c.encode_utf8(&mut buf);
1475 sequences.push(encoded.as_bytes().to_vec());
1476 budget -= 1;
1477 }
1478 }
1479 }
1480 }
1481 }
1482 Ok(sequences)
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487 use super::*;
1488
1489 fn states_of(s: &str) -> u32 {
1490 compile_regex_set(&[s]).unwrap().plan.num_states
1491 }
1492
1493 #[test]
1494 fn capture_mode_routing_splits_accelerator_from_verifier() {
1495 for mode in CaptureMode::ALL {
1500 assert_eq!(
1501 mode.accelerator_eligible(),
1502 !mode.verifier_required(),
1503 "{mode:?}: accelerator_eligible must be the complement of verifier_required"
1504 );
1505 }
1506 let accel: Vec<CaptureMode> = CaptureMode::ALL
1507 .into_iter()
1508 .filter(|m| m.accelerator_eligible())
1509 .collect();
1510 assert_eq!(
1511 accel,
1512 vec![
1513 CaptureMode::NonCapture,
1514 CaptureMode::Count,
1515 CaptureMode::Span
1516 ],
1517 "only the whole-match modes are accelerator-eligible"
1518 );
1519 }
1520
1521 #[test]
1522 fn capture_mode_id_round_trips_and_is_unique() {
1523 use std::collections::BTreeSet;
1524 let mut ids = BTreeSet::new();
1525 for mode in CaptureMode::ALL {
1526 let id = mode.contract_row().mode_id;
1527 assert!(ids.insert(id), "duplicate mode_id `{id}`");
1528 assert_eq!(
1529 CaptureMode::from_mode_id(id),
1530 Some(mode),
1531 "mode_id `{id}` must round-trip back to {mode:?}"
1532 );
1533 }
1534 assert_eq!(ids.len(), 6, "all six modes must have distinct ids");
1535 assert_eq!(CaptureMode::from_mode_id("no_such_mode"), None);
1536 }
1537
1538 #[test]
1539 fn literal_compiles() {
1540 let r = compile_regex_set(&["abc"]).unwrap();
1541 assert_eq!(r.plan.num_states, 5);
1543 assert_eq!(r.plan.accept_states.len(), 1);
1544 }
1545
1546 #[test]
1547 fn alternation_compiles() {
1548 let r = compile_regex_set(&["a|b"]).unwrap();
1549 assert!(r.plan.num_states > 0);
1552 assert_eq!(r.plan.accept_states.len(), 1);
1553 }
1554
1555 #[test]
1556 fn class_compiles() {
1557 let r = compile_regex_set(&["[a-z]"]).unwrap();
1558 assert!(r.plan.num_states > 0);
1559 }
1562
1563 #[test]
1564 fn text_anchors_compile_to_accept_flags() {
1565 let r = compile_regex_set(&["^foo$"]).unwrap();
1566 assert_eq!(r.plan.accept_start_anchored, vec![true]);
1567 assert_eq!(r.plan.accept_end_anchored, vec![true]);
1568 }
1569
1570 #[test]
1571 fn bounded_repetition_above_old_cap_compiles_under_state_cap() {
1572 let r = compile_regex_set(&["a{0,128}"]).unwrap();
1573 assert!(r.plan.num_states > 64);
1574 assert!(r.plan.num_states <= STATE_CAP as u32);
1575 }
1576
1577 #[test]
1578 fn regex_compile_preserves_accept_metadata_through_checked_paths() {
1579 let r = compile_regex_set(&["a", "bc", "^de$"]).unwrap();
1580
1581 assert_eq!(r.plan.accept_states, vec![(0, 1), (1, 2), (2, 2)]);
1582 assert_eq!(r.plan.accept_state_ids.len(), 3);
1583 assert_eq!(r.plan.accept_start_anchored, vec![false, false, true]);
1584 assert_eq!(r.plan.accept_end_anchored, vec![false, false, true]);
1585 assert_eq!(
1586 r.transition_table.len(),
1587 r.plan.num_states as usize * 256 * LANES
1588 );
1589 assert_eq!(r.epsilon_table.len(), r.plan.num_states as usize * LANES);
1590 }
1591
1592 #[test]
1593 fn regex_pipeline_uses_compiled_plan_instead_of_literal_source_plan() {
1594 let compiled = compile_regex_set(&["a|bc"]).unwrap();
1595 let pipeline = build_scan_program_from_regex(&["a|bc"], "input", "hits", 64).unwrap();
1596
1597 assert_eq!(pipeline.plan.num_states, compiled.plan.num_states);
1598 assert_eq!(
1599 pipeline.plan.accept_state_ids,
1600 compiled.plan.accept_state_ids
1601 );
1602 assert_eq!(
1603 pipeline.epsilon_table.iter().any(|word| *word != 0),
1604 compiled.epsilon_table.iter().any(|word| *word != 0)
1605 );
1606 assert_ne!(
1607 pipeline.plan.num_states,
1608 crate::scan::nfa::compile(&["a|bc"]).num_states,
1609 "regex pipeline must not rebuild the scan program from literal regex source bytes"
1610 );
1611 }
1612
1613 #[test]
1614 fn states_count_grows_with_concat() {
1615 let one = states_of("a");
1616 let two = states_of("ab");
1617 let three = states_of("abc");
1618 assert!(two > one);
1619 assert!(three > two);
1620 }
1621
1622 #[test]
1623 fn state_cap_enforced() {
1624 let huge: String = (0..(STATE_CAP + 4)).map(|_| 'a').collect();
1627 let err = compile_regex_set(&[&huge]).unwrap_err();
1628 assert!(matches!(err, RegexCompileError::TooManyStates { .. }));
1629 }
1630
1631 #[test]
1632 fn unsupported_regex_diagnostic_does_not_route_to_cpu_backend() {
1633 let err = compile_regex_set(&[r"\bsecret\b"]).unwrap_err();
1634 let message = err.to_string().to_ascii_lowercase();
1635 assert!(
1636 !message.contains("cpu"),
1637 "unsupported GPU-NFA regex diagnostics must not recommend host-side routing: {message}"
1638 );
1639 assert!(
1640 message.contains("gpu"),
1641 "unsupported GPU-NFA regex diagnostics must name the GPU-compatible rewrite contract: {message}"
1642 );
1643 }
1644
1645 #[test]
1652 fn unicode_class_outside_ascii_compiles_via_utf8_expansion() {
1653 let pat = "[hнһh]f_[a-zA-Z0-9]{4}";
1657 let result = compile_regex_set(&[pat]);
1658 let compiled = match result {
1659 Ok(c) => c,
1660 Err(e) => {
1661 panic!("unicode-extended character class must compile via UTF-8 expansion; got {e}")
1662 }
1663 };
1664 assert!(
1670 compiled.plan.num_states > 4,
1671 "expanded NFA must have non-trivial state count"
1672 );
1673 assert_eq!(compiled.plan.accept_states.len(), 1);
1677 }
1678
1679 #[test]
1683 fn ascii_only_class_keeps_single_byte_transition_path() {
1684 let r = compile_regex_set(&["[ab]"]).unwrap();
1687 assert_eq!(
1688 r.plan.num_states, 3,
1689 "[ab] must stay on the single-transition fast path (entry + 2 class states); got {} states",
1690 r.plan.num_states
1691 );
1692 }
1693
1694 #[test]
1698 fn unicode_class_above_expansion_cap_errors_cleanly() {
1699 let pat = "[\u{0100}-\u{0200}]";
1701 let err = compile_regex_set(&[pat]).unwrap_err();
1702 match err {
1703 RegexCompileError::Unsupported { feature, .. } => {
1704 assert!(
1705 feature.contains("expansion cap"),
1706 "over-cap expansion must name the cap in its diagnostic: {feature}"
1707 );
1708 }
1709 other => panic!("expected Unsupported expansion-cap error, got {other:?}"),
1710 }
1711 }
1712
1713 #[test]
1717 fn regex_compile_diagnostic_codes() {
1718 let look_err = compile_regex_set(&[r"a\bc"]).expect_err("word boundary is unsupported");
1720 assert_eq!(
1721 look_err.diagnostic_code(),
1722 Some("VYRE_SCAN_APPROXIMATED_LOOKAROUND_REQUIRES_VERIFIER"),
1723 "non-edge lookaround must map to its verifier diagnostic code; error was: {look_err}"
1724 );
1725
1726 let uni_err =
1728 compile_regex_set(&["[\u{0100}-\u{0200}]"]).expect_err("over-cap unicode class");
1729 assert_eq!(
1730 uni_err.diagnostic_code(),
1731 Some("VYRE_SCAN_UNSUPPORTED_UNICODE_MODE_GPU"),
1732 "over-cap unicode class must map to its diagnostic code; error was: {uni_err}"
1733 );
1734
1735 assert!(
1737 compile_regex_set(&["^abc$"]).is_ok(),
1738 "start/end anchors must compile, not be flagged as unsupported lookaround"
1739 );
1740
1741 let parse_err = compile_regex_set(&["("]).expect_err("unbalanced group is a parse error");
1743 assert_eq!(
1744 parse_err.diagnostic_code(),
1745 None,
1746 "a parse error must not claim a registry diagnostic code"
1747 );
1748
1749 let backref_err =
1752 compile_regex_set(&[r"(a)\1"]).expect_err("backreferences are unsupported");
1753 assert_eq!(
1754 backref_err.diagnostic_code(),
1755 Some("VYRE_SCAN_UNSUPPORTED_BACKREFERENCE"),
1756 "a backreference must map to its distinct code, not fall back to Parse; error was: {backref_err}"
1757 );
1758
1759 let huge: String = (0..(MAX_ALTERNATION_ARMS + 8))
1762 .map(|i| format!("v{i}"))
1763 .collect::<Vec<_>>()
1764 .join("|");
1765 let alt_err = compile_regex_set(&[huge.as_str()]).expect_err("over-budget alternation");
1766 assert_eq!(
1767 alt_err.diagnostic_code(),
1768 Some("VYRE_SCAN_UNSUPPORTED_HUGE_ALTERNATION_BUDGET"),
1769 "a huge alternation must map to its budget code, not TooManyStates; error was: {alt_err}"
1770 );
1771
1772 let nested_err =
1775 compile_regex_set(&[r"(?:a{40}){40}"]).expect_err("nested-repeat unroll blowup");
1776 assert_eq!(
1777 nested_err.diagnostic_code(),
1778 Some("VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET"),
1779 "nested bounded repeats must map to their budget code; error was: {nested_err}"
1780 );
1781 }
1782
1783 #[test]
1787 fn backreference_detector_is_escaping_aware() {
1788 assert!(pattern_uses_backreference(r"\1"));
1790 assert!(pattern_uses_backreference(r"(a)\1"));
1791 assert!(pattern_uses_backreference(r"foo\9bar"));
1792 assert!(pattern_uses_backreference(r"\k<name>"));
1794 assert!(pattern_uses_backreference(r"\k'name'"));
1795 assert!(pattern_uses_backreference("(?P=name)"));
1796
1797 assert!(!pattern_uses_backreference(r"\0"));
1801 assert!(
1802 !pattern_uses_backreference(r"\\1"),
1803 "an escaped backslash then a literal 1 is not a backreference"
1804 );
1805 assert!(!pattern_uses_backreference(r"\d+\w*"));
1806 assert!(!pattern_uses_backreference(r"[a-z]{3}"));
1807 assert!(!pattern_uses_backreference("plain text"));
1808 assert!(pattern_uses_backreference(r"\\\1"));
1810 }
1811
1812 #[test]
1816 fn captures_compile_and_surface_the_verifier_diagnostic() {
1817 let with_cap = compile_regex_set(&[r"(abc)def"]).expect("captures compile for whole-match");
1820 assert!(with_cap.captures_present, "the capture group must be noted");
1821 assert_eq!(
1822 with_cap.capture_extraction_diagnostic_code(),
1823 Some("VYRE_SCAN_CAPTURE_EXTRACTION_REQUIRES_VERIFIER"),
1824 "a captured pattern must surface the capture-verifier code without erroring"
1825 );
1826
1827 let no_cap = compile_regex_set(&[r"abcdef"]).expect("plain pattern compiles");
1829 assert!(!no_cap.captures_present);
1830 assert_eq!(no_cap.capture_extraction_diagnostic_code(), None);
1831
1832 let noncap = compile_regex_set(&[r"(?:abc)def"]).expect("non-capturing group compiles");
1834 assert!(
1835 !noncap.captures_present,
1836 "a (?:…) non-capturing group must not be flagged as a capture"
1837 );
1838 }
1839
1840 #[test]
1844 fn budget_reclassification_does_not_regress_compiling_patterns() {
1845 let ok_alt: String = ('a'..='z')
1849 .chain('A'..='Z')
1850 .chain('0'..='9')
1851 .map(|c| c.to_string())
1852 .collect::<Vec<_>>()
1853 .join("|");
1854 let compiled = compile_regex_set(&[ok_alt.as_str()])
1855 .expect("a 62-arm single-byte alternation must still compile");
1856 assert!(compiled.plan.num_states > 0);
1858
1859 assert!(
1862 compile_regex_set(&[r"(?:a{20}){20}"]).is_ok(),
1863 "a nested repeat under the unroll budget must still compile"
1864 );
1865
1866 assert_eq!(
1868 regex_construct_diagnostic_code(RegexConstruct::Backreference),
1869 "VYRE_SCAN_UNSUPPORTED_BACKREFERENCE"
1870 );
1871 assert_eq!(
1872 regex_construct_diagnostic_code(RegexConstruct::NestedRepeats),
1873 "VYRE_SCAN_UNSUPPORTED_NESTED_REPEAT_BUDGET"
1874 );
1875 }
1876
1877 #[test]
1885 fn every_compile_error_variant_names_its_owner_and_fix_path() {
1886 let variants = [
1887 RegexCompileError::Parse {
1888 pattern_index: 0,
1889 message: "unclosed group".to_string(),
1890 },
1891 RegexCompileError::Unsupported {
1892 pattern_index: 1,
1893 feature: "backreference",
1894 },
1895 RegexCompileError::TooManyStates {
1896 states: 5_000,
1897 cap: 1_024,
1898 },
1899 RegexCompileError::PatternCountOverflow { count: usize::MAX },
1900 RegexCompileError::MatchLengthOverflow {
1901 pattern_index: 2,
1902 len: usize::MAX,
1903 },
1904 RegexCompileError::MatchLengthArithmeticOverflow { pattern_index: 3 },
1905 RegexCompileError::OpenEndedReplayLimitTooSmall {
1906 pattern_index: 4,
1907 minimum: 12,
1908 limit: 8,
1909 },
1910 RegexCompileError::TableWordCountOverflow {
1911 table: "transition",
1912 },
1913 RegexCompileError::StorageReserveFailed {
1914 field: "epsilon",
1915 requested: 9,
1916 message: "allocator refused".to_string(),
1917 },
1918 ];
1919
1920 fn assert_covers_every_variant(error: &RegexCompileError) {
1925 match error {
1926 RegexCompileError::Parse { .. }
1927 | RegexCompileError::Unsupported { .. }
1928 | RegexCompileError::TooManyStates { .. }
1929 | RegexCompileError::PatternCountOverflow { .. }
1930 | RegexCompileError::MatchLengthOverflow { .. }
1931 | RegexCompileError::MatchLengthArithmeticOverflow { .. }
1932 | RegexCompileError::OpenEndedReplayLimitTooSmall { .. }
1933 | RegexCompileError::TableWordCountOverflow { .. }
1934 | RegexCompileError::StorageReserveFailed { .. } => {}
1935 }
1936 }
1937
1938 for error in &variants {
1939 assert_covers_every_variant(error);
1940 let rendered = error.to_string();
1941 assert!(
1942 rendered.starts_with("regex_compile:"),
1943 "a RegexCompileError variant lacks the `regex_compile:` owner prefix: {rendered}"
1944 );
1945 assert!(
1946 rendered.contains("Fix:"),
1947 "a RegexCompileError variant lacks a `Fix:` remedy clause: {rendered}"
1948 );
1949 }
1950 }
1951}