Skip to main content

voxgig_struct/
re.rs

1// Copyright (c) 2025-2026 Voxgig Ltd. MIT LICENSE.
2//
3// Voxgig Struct — RE2-subset regex engine, pure Rust, no runtime deps.
4//
5// Direct port of c/src/regex.c (Thompson NFA via two state sets).
6// Provides an API surface compatible with the subset of the `regex`
7// crate that the Voxgig Struct port uses:
8//
9//   - Regex::new(pattern)               -> Result<Regex, RegexError>
10//   - re.is_match(input)                -> bool
11//   - re.captures(input)                -> Option<Captures<'_>>
12//   - re.captures_iter(input)           -> CapturesIter<'_>
13//   - re.replace_all(input, &str)       -> Cow<'_, str>     ($&, $0..$9)
14//   - re.replace_all(input, |c| String) -> Cow<'_, str>     (closure)
15//   - Captures::get(i) -> Option<Match<'_>>; iter(); index by usize
16//
17// Dialect mirrors c/src/regex.h:
18//   . anchors ^ $ . groups (...) (?:...) (?P<name>...) (names ignored)
19//   . classes [abc] [^abc] [a-z]    predefined \d \D \s \S \w \W
20//   . quantifiers * + ? {n} {n,} {n,m} and lazy *? +? ?? {..}?
21//   . word boundary \b \B   . alternation a|b
22//   . NOT supported: backref, lookaround, possessive, atomic.
23
24use std::borrow::Cow;
25
26const MAX_GROUPS: usize = 16;
27
28// ---------- instructions ----------
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31enum Op {
32    Char(u8),
33    Any,
34    Class,
35    Match,
36    Jmp(i32),
37    Split(i32, i32),
38    Save(usize),
39    Bol,
40    Eol,
41    Wb,
42    Nwb,
43}
44
45#[derive(Clone, Copy)]
46struct CharClass {
47    bits: [u8; 32],
48}
49
50impl CharClass {
51    fn zero() -> Self {
52        Self { bits: [0; 32] }
53    }
54    fn set(&mut self, c: u8) {
55        self.bits[(c >> 3) as usize] |= 1u8 << (c & 7);
56    }
57    fn set_range(&mut self, lo: u8, hi: u8) {
58        let (lo, hi) = if lo > hi { (hi, lo) } else { (lo, hi) };
59        for c in lo..=hi {
60            self.set(c);
61        }
62    }
63    fn has(&self, c: u8) -> bool {
64        (self.bits[(c >> 3) as usize] >> (c & 7)) & 1 == 1
65    }
66    fn negate(&mut self) {
67        for b in self.bits.iter_mut() {
68            *b = !*b;
69        }
70    }
71    fn predef(&mut self, c: u8) {
72        match c {
73            b'd' => self.set_range(b'0', b'9'),
74            b'D' => {
75                self.set_range(0, 255);
76                for x in b'0'..=b'9' {
77                    self.bits[(x >> 3) as usize] &= !(1u8 << (x & 7));
78                }
79            }
80            b's' => {
81                for c in [b' ', b'\t', b'\n', b'\r', 0x0C, 0x0B].iter() {
82                    self.set(*c);
83                }
84            }
85            b'S' => {
86                self.set_range(0, 255);
87                for c in [b' ', b'\t', b'\n', b'\r', 0x0C, 0x0B].iter() {
88                    self.bits[(*c >> 3) as usize] &= !(1u8 << (c & 7));
89                }
90            }
91            b'w' => {
92                self.set_range(b'0', b'9');
93                self.set_range(b'A', b'Z');
94                self.set_range(b'a', b'z');
95                self.set(b'_');
96            }
97            b'W' => {
98                self.set_range(0, 255);
99                for x in b'0'..=b'9' {
100                    self.bits[(x >> 3) as usize] &= !(1u8 << (x & 7));
101                }
102                for x in b'A'..=b'Z' {
103                    self.bits[(x >> 3) as usize] &= !(1u8 << (x & 7));
104                }
105                for x in b'a'..=b'z' {
106                    self.bits[(x >> 3) as usize] &= !(1u8 << (x & 7));
107                }
108                self.bits[(b'_' >> 3) as usize] &= !(1u8 << (b'_' & 7));
109            }
110            _ => {}
111        }
112    }
113}
114
115#[derive(Clone, Copy)]
116struct Insn {
117    op: Op,
118    cc: CharClass, // only used for Op::Class
119}
120
121impl Insn {
122    fn new(op: Op) -> Self {
123        Self {
124            op,
125            cc: CharClass::zero(),
126        }
127    }
128}
129
130// ---------- compiled regex ----------
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct RegexError(pub String);
134
135impl std::fmt::Display for RegexError {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(f, "regex: {}", self.0)
138    }
139}
140
141impl std::error::Error for RegexError {}
142
143pub struct Regex {
144    code: Vec<Insn>,
145    ngroups: usize, // including group 0
146    anchored_start: bool,
147}
148
149// ---------- parser ----------
150
151struct Parser<'a> {
152    src: &'a [u8],
153    pos: usize,
154    next_group: usize,
155    err: Option<String>,
156    code: Vec<Insn>,
157}
158
159fn hexval(c: u8) -> i32 {
160    match c {
161        b'0'..=b'9' => (c - b'0') as i32,
162        b'a'..=b'f' => (c - b'a') as i32 + 10,
163        b'A'..=b'F' => (c - b'A') as i32 + 10,
164        _ => -1,
165    }
166}
167
168impl<'a> Parser<'a> {
169    fn perr(&mut self, msg: &str) {
170        if self.err.is_none() {
171            self.err = Some(format!("regex parse error at {}: {}", self.pos, msg));
172        }
173    }
174
175    // Returns (byte, predef_letter, kind):
176    //   kind 0 = byte, kind 1 = predef class (predef_letter set), kind 2 = \b/\B (predef_letter set)
177    fn parse_escape(&mut self) -> (u8, u8, u8) {
178        if self.pos >= self.src.len() {
179            self.perr("trailing backslash");
180            return (0, 0, 0);
181        }
182        let c = self.src[self.pos];
183        self.pos += 1;
184        match c {
185            b'n' => (b'\n', 0, 0),
186            b't' => (b'\t', 0, 0),
187            b'r' => (b'\r', 0, 0),
188            b'f' => (0x0C, 0, 0),
189            b'v' => (0x0B, 0, 0),
190            b'0' => (0, 0, 0),
191            b'a' => (0x07, 0, 0),
192            b'e' => (27, 0, 0),
193            b'x' => {
194                if self.pos + 1 >= self.src.len() {
195                    self.perr("bad \\xNN");
196                    return (0, 0, 0);
197                }
198                let h1 = hexval(self.src[self.pos]);
199                let h2 = hexval(self.src[self.pos + 1]);
200                if h1 < 0 || h2 < 0 {
201                    self.perr("bad \\xNN");
202                    return (0, 0, 0);
203                }
204                self.pos += 2;
205                (((h1 << 4) | h2) as u8, 0, 0)
206            }
207            b'd' | b'D' | b's' | b'S' | b'w' | b'W' => (0, c, 1),
208            b'b' | b'B' => (0, c, 2),
209            _ => (c, 0, 0),
210        }
211    }
212
213    fn parse_class(&mut self) -> CharClass {
214        let mut out = CharClass::zero();
215        let mut neg = false;
216        if self.pos < self.src.len() && self.src[self.pos] == b'^' {
217            neg = true;
218            self.pos += 1;
219        }
220        let mut first = true;
221        while self.pos < self.src.len() && (first || self.src[self.pos] != b']') {
222            first = false;
223            let c;
224            if self.src[self.pos] == b'\\' {
225                self.pos += 1;
226                let (b, p, k) = self.parse_escape();
227                if k == 1 {
228                    let mut sub = CharClass::zero();
229                    sub.predef(p);
230                    for i in 0..32 {
231                        out.bits[i] |= sub.bits[i];
232                    }
233                    continue;
234                }
235                c = if k == 2 { 8 } else { b };
236            } else {
237                c = self.src[self.pos];
238                self.pos += 1;
239            }
240            if self.pos + 1 < self.src.len()
241                && self.src[self.pos] == b'-'
242                && self.src[self.pos + 1] != b']'
243            {
244                self.pos += 1;
245                let hi;
246                if self.src[self.pos] == b'\\' {
247                    self.pos += 1;
248                    let (b, _p, k) = self.parse_escape();
249                    hi = if k == 0 { b } else { b'-' };
250                } else {
251                    hi = self.src[self.pos];
252                    self.pos += 1;
253                }
254                out.set_range(c, hi);
255            } else {
256                out.set(c);
257            }
258        }
259        if self.pos >= self.src.len() || self.src[self.pos] != b']' {
260            self.perr("unclosed [");
261            return out;
262        }
263        self.pos += 1;
264        if neg {
265            out.negate();
266        }
267        out
268    }
269
270    fn emit(&mut self, op: Op) -> usize {
271        let ix = self.code.len();
272        self.code.push(Insn::new(op));
273        ix
274    }
275
276    // Returns the index of the first emitted instruction for this atom.
277    fn parse_atom(&mut self) -> usize {
278        if self.pos >= self.src.len() {
279            return self.code.len();
280        }
281        let start = self.code.len();
282        let c = self.src[self.pos];
283        if c == b'(' {
284            self.pos += 1;
285            let mut capture = true;
286            if self.pos + 1 < self.src.len()
287                && self.src[self.pos] == b'?'
288                && self.src[self.pos + 1] == b':'
289            {
290                capture = false;
291                self.pos += 2;
292            } else if self.pos + 2 < self.src.len()
293                && self.src[self.pos] == b'?'
294                && self.src[self.pos + 1] == b'P'
295                && self.src[self.pos + 2] == b'<'
296            {
297                // Named group — consume name; we don't expose names but still capture.
298                self.pos += 3;
299                while self.pos < self.src.len() && self.src[self.pos] != b'>' {
300                    self.pos += 1;
301                }
302                if self.pos < self.src.len() {
303                    self.pos += 1;
304                }
305            }
306            let group = if capture {
307                let g = self.next_group;
308                self.next_group += 1;
309                self.emit(Op::Save(g * 2));
310                g
311            } else {
312                0
313            };
314            self.parse_alt();
315            if self.pos >= self.src.len() || self.src[self.pos] != b')' {
316                self.perr("unclosed (");
317                return start;
318            }
319            self.pos += 1;
320            if capture {
321                self.emit(Op::Save(group * 2 + 1));
322            }
323        } else if c == b'[' {
324            self.pos += 1;
325            let cc = self.parse_class();
326            let ix = self.emit(Op::Class);
327            self.code[ix].cc = cc;
328        } else if c == b'.' {
329            self.pos += 1;
330            self.emit(Op::Any);
331        } else if c == b'^' {
332            self.pos += 1;
333            self.emit(Op::Bol);
334        } else if c == b'$' {
335            self.pos += 1;
336            self.emit(Op::Eol);
337        } else if c == b'\\' {
338            self.pos += 1;
339            let (b, p, k) = self.parse_escape();
340            match k {
341                1 => {
342                    let mut cc = CharClass::zero();
343                    cc.predef(p);
344                    let ix = self.emit(Op::Class);
345                    self.code[ix].cc = cc;
346                }
347                2 => {
348                    self.emit(if p == b'b' { Op::Wb } else { Op::Nwb });
349                }
350                _ => {
351                    self.emit(Op::Char(b));
352                }
353            }
354        } else if c == b')' || c == b'|' {
355            return start;
356        } else {
357            self.pos += 1;
358            self.emit(Op::Char(c));
359        }
360        start
361    }
362
363    fn code_clone(&mut self, from: usize, to: usize) -> usize {
364        let delta = self.code.len() as i32 - from as i32;
365        let start = self.code.len();
366        for i in from..to {
367            let mut insn = self.code[i];
368            match insn.op {
369                Op::Jmp(t) if (t as usize) >= from && (t as usize) < to => {
370                    insn.op = Op::Jmp(t + delta);
371                }
372                Op::Split(x, y) => {
373                    let nx = if (x as usize) >= from && (x as usize) < to {
374                        x + delta
375                    } else {
376                        x
377                    };
378                    let ny = if (y as usize) >= from && (y as usize) < to {
379                        y + delta
380                    } else {
381                        y
382                    };
383                    insn.op = Op::Split(nx, ny);
384                }
385                _ => {}
386            }
387            self.code.push(insn);
388        }
389        start
390    }
391
392    fn shift_targets_after(&mut self, from: usize, by: i32) {
393        for i in (from + 1)..self.code.len() {
394            match &mut self.code[i].op {
395                Op::Jmp(t) if *t as usize >= from => {
396                    *t += by;
397                }
398                Op::Split(x, y) => {
399                    if *x as usize >= from {
400                        *x += by;
401                    }
402                    if *y as usize >= from {
403                        *y += by;
404                    }
405                }
406                _ => {}
407            }
408        }
409    }
410
411    fn apply_quant(&mut self, start: usize, q: u8, n_lo: i32, n_hi: i32, lazy: bool) {
412        let end = self.code.len();
413        let alen = end - start;
414        if alen == 0 {
415            return;
416        }
417
418        match q {
419            b'?' => {
420                // SPLIT before atom, falling through after.
421                self.code.insert(start, Insn::new(Op::Split(0, 0)));
422                let after = self.code.len() as i32;
423                let to_atom = (start + 1) as i32;
424                self.code[start].op = Op::Split(
425                    if lazy { after } else { to_atom },
426                    if lazy { to_atom } else { after },
427                );
428                self.shift_targets_after(start, 1);
429            }
430            b'*' => {
431                // L0: SPLIT L1 L2; atom; JMP L0; L2:
432                self.code.insert(start, Insn::new(Op::Split(0, 0)));
433                self.shift_targets_after(start, 1);
434                // We've inserted one before start; compute after the atom (now end+1).
435                let after_atom = self.code.len(); // before JMP emit
436                self.emit(Op::Jmp(start as i32));
437                let exit = self.code.len();
438                let to_atom = (start + 1) as i32;
439                self.code[start].op = Op::Split(
440                    if lazy { exit as i32 } else { to_atom },
441                    if lazy { to_atom } else { exit as i32 },
442                );
443                let _ = after_atom;
444            }
445            b'+' => {
446                let _ix = self.emit(Op::Split(0, 0));
447                let after = self.code.len() as i32;
448                let s_ix = self.code.len() - 1;
449                self.code[s_ix].op = Op::Split(
450                    if lazy { after } else { start as i32 },
451                    if lazy { start as i32 } else { after },
452                );
453            }
454            b'{' => {
455                // Emit n_lo mandatory copies (we already have one — the original atom).
456                for _ in 1..n_lo {
457                    self.code_clone(start, end);
458                }
459                if n_hi == -1 {
460                    // {n,}: Kleene-star of the atom appended.
461                    let split_ix = self.emit(Op::Split(0, 0));
462                    let atom_start = self.code.len();
463                    self.code_clone(start, end);
464                    let jmp_ix = self.emit(Op::Jmp(split_ix as i32));
465                    let exit = self.code.len() as i32;
466                    self.code[split_ix].op = Op::Split(
467                        if lazy { exit } else { atom_start as i32 },
468                        if lazy { atom_start as i32 } else { exit },
469                    );
470                    let _ = jmp_ix;
471                } else if n_hi > n_lo {
472                    for _ in 0..(n_hi - n_lo) {
473                        let sp = self.emit(Op::Split(0, 0));
474                        let clone_start = self.code.len();
475                        self.code_clone(start, end);
476                        let after = self.code.len() as i32;
477                        self.code[sp].op = Op::Split(
478                            if lazy { after } else { clone_start as i32 },
479                            if lazy { clone_start as i32 } else { after },
480                        );
481                    }
482                }
483            }
484            _ => {}
485        }
486    }
487
488    fn parse_concat(&mut self) -> usize {
489        let start = self.code.len();
490        while self.pos < self.src.len() && self.src[self.pos] != b')' && self.src[self.pos] != b'|'
491        {
492            let atom_start = self.parse_atom();
493            if self.err.is_some() {
494                return start;
495            }
496            if self.pos < self.src.len() {
497                let q = self.src[self.pos];
498                if q == b'*' || q == b'+' || q == b'?' {
499                    self.pos += 1;
500                    let mut lazy = false;
501                    if self.pos < self.src.len() && self.src[self.pos] == b'?' {
502                        lazy = true;
503                        self.pos += 1;
504                    }
505                    self.apply_quant(atom_start, q, 0, 0, lazy);
506                } else if q == b'{' {
507                    let save = self.pos;
508                    self.pos += 1;
509                    let mut n_lo: i32 = 0;
510                    let mut got_lo = false;
511                    while self.pos < self.src.len() && self.src[self.pos].is_ascii_digit() {
512                        n_lo = n_lo * 10 + (self.src[self.pos] - b'0') as i32;
513                        got_lo = true;
514                        self.pos += 1;
515                    }
516                    let mut n_hi: i32 = n_lo;
517                    let mut open = false;
518                    if !got_lo {
519                        self.pos = save;
520                    } else {
521                        if self.pos < self.src.len() && self.src[self.pos] == b',' {
522                            self.pos += 1;
523                            n_hi = -1;
524                            let mut hi: i32 = 0;
525                            let mut got_hi = false;
526                            while self.pos < self.src.len() && self.src[self.pos].is_ascii_digit() {
527                                hi = hi * 10 + (self.src[self.pos] - b'0') as i32;
528                                got_hi = true;
529                                self.pos += 1;
530                            }
531                            if got_hi {
532                                n_hi = hi;
533                            } else {
534                                open = true;
535                            }
536                        }
537                        if self.pos < self.src.len() && self.src[self.pos] == b'}' {
538                            self.pos += 1;
539                            let mut lazy = false;
540                            if self.pos < self.src.len() && self.src[self.pos] == b'?' {
541                                lazy = true;
542                                self.pos += 1;
543                            }
544                            self.apply_quant(
545                                atom_start,
546                                b'{',
547                                n_lo,
548                                if open { -1 } else { n_hi },
549                                lazy,
550                            );
551                        } else {
552                            self.perr("bad {n,m}");
553                        }
554                    }
555                }
556            }
557        }
558        start
559    }
560
561    fn parse_alt(&mut self) -> usize {
562        let start = self.parse_concat();
563        if self.err.is_some() {
564            return start;
565        }
566        while self.pos < self.src.len() && self.src[self.pos] == b'|' {
567            let branch1_end = self.code.len();
568            let jmp_ix = self.emit(Op::Jmp(0)); // placeholder
569            let branch2_start = self.code.len();
570            // Insert SPLIT at `start`.
571            self.code.insert(start, Insn::new(Op::Split(0, 0)));
572            // Patch jumps inside the moved block (everything from start+1 to end).
573            for i in (start + 1)..self.code.len() {
574                match &mut self.code[i].op {
575                    Op::Jmp(t) if *t as usize >= start => {
576                        *t += 1;
577                    }
578                    Op::Split(x, y) => {
579                        if *x as usize >= start {
580                            *x += 1;
581                        }
582                        if *y as usize >= start {
583                            *y += 1;
584                        }
585                    }
586                    _ => {}
587                }
588            }
589            self.code[start].op = Op::Split((start + 1) as i32, (branch2_start + 1) as i32);
590            let _ = branch1_end;
591            let jmp_ix = jmp_ix + 1;
592            self.pos += 1;
593            self.parse_concat();
594            self.code[jmp_ix].op = Op::Jmp(self.code.len() as i32);
595        }
596        start
597    }
598}
599
600// ---------- compile ----------
601
602impl Regex {
603    pub fn new(pattern: &str) -> Result<Self, RegexError> {
604        let bytes = pattern.as_bytes();
605        let mut p = Parser {
606            src: bytes,
607            pos: 0,
608            next_group: 1, // 0 reserved for whole match
609            err: None,
610            code: Vec::new(),
611        };
612        // Wrap whole pattern in implicit group 0.
613        p.code.push(Insn::new(Op::Save(0)));
614        let anchored_start = !bytes.is_empty() && bytes[0] == b'^';
615        p.parse_alt();
616        if let Some(e) = p.err {
617            return Err(RegexError(e));
618        }
619        if p.pos < bytes.len() {
620            return Err(RegexError(format!(
621                "regex parse error at {}: unexpected )",
622                p.pos
623            )));
624        }
625        p.code.push(Insn::new(Op::Save(1)));
626        p.code.push(Insn::new(Op::Match));
627        Ok(Self {
628            code: p.code,
629            ngroups: p.next_group,
630            anchored_start,
631        })
632    }
633
634    pub fn is_match(&self, input: &str) -> bool {
635        self.find_first(input.as_bytes()).is_some()
636    }
637
638    pub fn captures<'h>(&self, input: &'h str) -> Option<Captures<'h>> {
639        let slots = self.find_first(input.as_bytes())?;
640        Some(Captures {
641            input,
642            slots,
643            ngroups: self.ngroups,
644        })
645    }
646
647    pub fn captures_iter<'r, 'h>(&'r self, input: &'h str) -> CapturesIter<'r, 'h> {
648        CapturesIter {
649            re: self,
650            input,
651            pos: 0,
652            done: false,
653        }
654    }
655
656    /// Replace the FIRST match only. Mirrors `regex` crate `replace`.
657    pub fn replace<'h, R: Replacer>(&self, input: &'h str, mut rep: R) -> Cow<'h, str> {
658        let bytes = input.as_bytes();
659        let mut start = 0usize;
660        let slots = loop {
661            if start > bytes.len() {
662                return Cow::Borrowed(input);
663            }
664            if let Some(s) = self.match_at(bytes, start) {
665                break s;
666            }
667            if self.anchored_start {
668                return Cow::Borrowed(input);
669            }
670            start += 1;
671        };
672        let mut out = String::with_capacity(input.len());
673        out.push_str(&input[..start]);
674        let caps = Captures {
675            input,
676            slots: slots.clone(),
677            ngroups: self.ngroups,
678        };
679        rep.replace_into(&caps, &mut out);
680        let mend = slots[1] as usize;
681        out.push_str(&input[mend..]);
682        Cow::Owned(out)
683    }
684
685    pub fn replace_all<'h, R: Replacer>(&self, input: &'h str, mut rep: R) -> Cow<'h, str> {
686        let bytes = input.as_bytes();
687        let mut out = String::new();
688        let mut pos: usize = 0;
689        let mut any = false;
690        while pos <= bytes.len() {
691            let mut start = pos;
692            let mut found_slots: Option<Vec<i32>> = None;
693            while start <= bytes.len() {
694                if let Some(s) = self.match_at(bytes, start) {
695                    found_slots = Some(s);
696                    break;
697                }
698                if self.anchored_start && start > pos {
699                    break;
700                }
701                start += 1;
702                if start > bytes.len() {
703                    break;
704                }
705            }
706            let slots = match found_slots {
707                Some(s) => s,
708                None => {
709                    out.push_str(&input[pos..]);
710                    break;
711                }
712            };
713            any = true;
714            // Copy pre-match.
715            out.push_str(&input[pos..start]);
716            let caps = Captures {
717                input,
718                slots: slots.clone(),
719                ngroups: self.ngroups,
720            };
721            rep.replace_into(&caps, &mut out);
722            let mend = slots[1] as usize;
723            if slots[1] == slots[0] {
724                // Empty match: emit one char and advance.
725                if mend < bytes.len() {
726                    let ch = input[mend..].chars().next().unwrap();
727                    out.push(ch);
728                    pos = mend + ch.len_utf8();
729                } else {
730                    pos = mend + 1;
731                }
732            } else {
733                pos = mend;
734            }
735        }
736        if any {
737            Cow::Owned(out)
738        } else {
739            Cow::Borrowed(input)
740        }
741    }
742
743    // ---- internal matching ----
744
745    fn find_first(&self, bytes: &[u8]) -> Option<Vec<i32>> {
746        let mut start: usize = 0;
747        loop {
748            if let Some(s) = self.match_at(bytes, start) {
749                return Some(s);
750            }
751            if self.anchored_start {
752                return None;
753            }
754            if start > bytes.len() {
755                return None;
756            }
757            start += 1;
758        }
759    }
760
761    fn match_at(&self, input: &[u8], start: usize) -> Option<Vec<i32>> {
762        let nslots = self.ngroups * 2;
763        let mut cur = ThreadList::new(self.code.len());
764        let mut nxt = ThreadList::new(self.code.len());
765        let init: Vec<i32> = vec![-1; nslots];
766        cur.gen = 1; // bump past initial visited[] = 0
767        cur.add(self, input, 0, &init, start);
768        let mut best: Option<Vec<i32>> = None;
769        let mut sp = start;
770        loop {
771            if cur.threads.is_empty() {
772                break;
773            }
774            nxt.reset();
775            let c: i32 = if sp < input.len() {
776                input[sp] as i32
777            } else {
778                -1
779            };
780            for th in &cur.threads {
781                let insn = &self.code[th.pc];
782                match insn.op {
783                    Op::Char(b) if c == b as i32 => {
784                        nxt.add(self, input, th.pc + 1, &th.slots, sp + 1);
785                    }
786                    Op::Any if c >= 0 && c != b'\n' as i32 => {
787                        nxt.add(self, input, th.pc + 1, &th.slots, sp + 1);
788                    }
789                    Op::Class if c >= 0 && insn.cc.has(c as u8) => {
790                        nxt.add(self, input, th.pc + 1, &th.slots, sp + 1);
791                    }
792                    Op::Match => {
793                        // Always overwrite: descendants of higher-priority
794                        // threads (those iterated before this Match thread)
795                        // are still alive in nxt and any later Match they
796                        // produce is in a strictly-higher-priority lineage.
797                        best = Some(th.slots.clone());
798                        break;
799                    }
800                    _ => {}
801                }
802            }
803            std::mem::swap(&mut cur, &mut nxt);
804            sp += 1;
805            if cur.threads.is_empty() {
806                break;
807            }
808        }
809        // Drain remaining current threads at EOI (some may have advanced past
810        // last char and now point at Match via epsilons). Always overwrite —
811        // same priority rule as the main loop.
812        for th in &cur.threads {
813            if matches!(self.code[th.pc].op, Op::Match) {
814                best = Some(th.slots.clone());
815                break;
816            }
817        }
818        best
819    }
820}
821
822// ---------- thread list (Thompson NFA driver) ----------
823
824#[derive(Clone)]
825struct Thread {
826    pc: usize,
827    slots: Vec<i32>,
828}
829
830struct ThreadList {
831    threads: Vec<Thread>,
832    visited: Vec<u32>,
833    gen: u32,
834}
835
836impl ThreadList {
837    fn new(code_len: usize) -> Self {
838        Self {
839            threads: Vec::new(),
840            visited: vec![0; code_len],
841            gen: 0,
842        }
843    }
844
845    fn reset(&mut self) {
846        self.threads.clear();
847        self.gen = self.gen.wrapping_add(1);
848        if self.gen == 0 {
849            self.visited.iter_mut().for_each(|v| *v = 0);
850            self.gen = 1;
851        }
852    }
853
854    fn add(&mut self, re: &Regex, input: &[u8], pc: usize, slots: &[i32], sp: usize) {
855        // Iterative epsilon-closure: we walk Jmp/Split/Save/Bol/Eol/Wb/Nwb
856        // until we hit a char-consuming op or Match. A recursive version
857        // overflows the stack on long Thompson chains (e.g. `a{0,10000}`
858        // unrolls into 10000 chained Splits — `cargo test` aborted with
859        // SIGABRT on the pathological-regex panel before this loop landed).
860        //
861        // The stack mirrors the recursive order: Split pushes y first then
862        // x, so x is processed first (priority preserved).
863        let mut stack: Vec<(usize, Vec<i32>)> = vec![(pc, slots.to_vec())];
864        while let Some((cur_pc, cur_slots)) = stack.pop() {
865            if cur_pc >= re.code.len() {
866                continue;
867            }
868            if self.visited[cur_pc] == self.gen {
869                continue;
870            }
871            self.visited[cur_pc] = self.gen;
872            match re.code[cur_pc].op {
873                Op::Jmp(t) => {
874                    stack.push((t as usize, cur_slots));
875                }
876                Op::Split(x, y) => {
877                    // Push y first so x (higher priority) is popped first.
878                    stack.push((y as usize, cur_slots.clone()));
879                    stack.push((x as usize, cur_slots));
880                }
881                Op::Save(slot) => {
882                    let mut ns = cur_slots;
883                    ns[slot] = sp as i32;
884                    stack.push((cur_pc + 1, ns));
885                }
886                Op::Bol => {
887                    if sp == 0 || (sp - 1 < input.len() && input[sp - 1] == b'\n') {
888                        stack.push((cur_pc + 1, cur_slots));
889                    }
890                }
891                Op::Eol => {
892                    if sp >= input.len() || input[sp] == b'\n' {
893                        stack.push((cur_pc + 1, cur_slots));
894                    }
895                }
896                Op::Wb | Op::Nwb => {
897                    let left = sp > 0
898                        && sp - 1 < input.len()
899                        && (input[sp - 1].is_ascii_alphanumeric() || input[sp - 1] == b'_');
900                    let right = sp < input.len()
901                        && (input[sp].is_ascii_alphanumeric() || input[sp] == b'_');
902                    let at_boundary = left != right;
903                    let want = matches!(re.code[cur_pc].op, Op::Wb);
904                    if at_boundary == want {
905                        stack.push((cur_pc + 1, cur_slots));
906                    }
907                }
908                _ => {
909                    // Char-consuming op (or Match): queue thread.
910                    self.threads.push(Thread {
911                        pc: cur_pc,
912                        slots: cur_slots,
913                    });
914                }
915            }
916        }
917    }
918}
919
920impl ThreadList {
921    // Convenience: re-init visited with the right size when first used.
922}
923
924// Initial gen tracking: bump before first use to avoid 0 match.
925impl ThreadList {
926    #[allow(dead_code)] // kept for the regex engine's reset hooks.
927    #[inline]
928    fn ensure_first_gen(&mut self) {
929        if self.gen == 0 {
930            self.gen = 1;
931        }
932    }
933}
934
935// ---------- Captures ----------
936
937#[derive(Debug, Clone, Copy)]
938pub struct Match<'h> {
939    text: &'h str,
940    start: usize,
941    end: usize,
942}
943
944impl<'h> Match<'h> {
945    pub fn as_str(&self) -> &'h str {
946        &self.text[self.start..self.end]
947    }
948    pub fn start(&self) -> usize {
949        self.start
950    }
951    pub fn end(&self) -> usize {
952        self.end
953    }
954}
955
956pub struct Captures<'h> {
957    input: &'h str,
958    slots: Vec<i32>,
959    ngroups: usize,
960}
961
962impl<'h> Captures<'h> {
963    pub fn get(&self, i: usize) -> Option<Match<'h>> {
964        if i >= self.ngroups {
965            return None;
966        }
967        let s = self.slots[2 * i];
968        let e = self.slots[2 * i + 1];
969        if s < 0 || e < 0 || e < s {
970            return None;
971        }
972        Some(Match {
973            text: self.input,
974            start: s as usize,
975            end: e as usize,
976        })
977    }
978    pub fn len(&self) -> usize {
979        self.ngroups
980    }
981    pub fn is_empty(&self) -> bool {
982        self.ngroups == 0
983    }
984    pub fn iter(&self) -> impl Iterator<Item = Option<Match<'h>>> + '_ {
985        (0..self.ngroups).map(move |i| self.get(i))
986    }
987}
988
989impl<'h> std::ops::Index<usize> for Captures<'h> {
990    type Output = str;
991    fn index(&self, i: usize) -> &str {
992        self.get(i).map(|m| m.as_str()).unwrap_or("")
993    }
994}
995
996// ---------- CapturesIter ----------
997
998pub struct CapturesIter<'r, 'h> {
999    re: &'r Regex,
1000    input: &'h str,
1001    pos: usize,
1002    done: bool,
1003}
1004
1005impl<'r, 'h> Iterator for CapturesIter<'r, 'h> {
1006    type Item = Captures<'h>;
1007    fn next(&mut self) -> Option<Self::Item> {
1008        if self.done {
1009            return None;
1010        }
1011        let bytes = self.input.as_bytes();
1012        let mut start = self.pos;
1013        let mut found: Option<Vec<i32>> = None;
1014        loop {
1015            if start > bytes.len() {
1016                break;
1017            }
1018            if let Some(s) = self.re.match_at(bytes, start) {
1019                found = Some(s);
1020                break;
1021            }
1022            if self.re.anchored_start {
1023                break;
1024            }
1025            start += 1;
1026        }
1027        let slots = found?;
1028        // Advance: if match was empty, step by 1 to avoid infinite loop.
1029        let mend = slots[1] as usize;
1030        self.pos = if slots[1] == slots[0] { mend + 1 } else { mend };
1031        if mend > bytes.len() {
1032            self.done = true;
1033        }
1034        Some(Captures {
1035            input: self.input,
1036            slots,
1037            ngroups: self.re.ngroups,
1038        })
1039    }
1040}
1041
1042// ---------- Replacer trait ----------
1043
1044pub trait Replacer {
1045    fn replace_into(&mut self, caps: &Captures<'_>, dst: &mut String);
1046}
1047
1048impl Replacer for &str {
1049    fn replace_into(&mut self, caps: &Captures<'_>, dst: &mut String) {
1050        let bytes = self.as_bytes();
1051        let mut i = 0;
1052        while i < bytes.len() {
1053            if bytes[i] == b'$' && i + 1 < bytes.len() {
1054                let nc = bytes[i + 1];
1055                if nc == b'&' {
1056                    if let Some(m) = caps.get(0) {
1057                        dst.push_str(m.as_str());
1058                    }
1059                    i += 2;
1060                    continue;
1061                }
1062                if nc.is_ascii_digit() {
1063                    let g = (nc - b'0') as usize;
1064                    if let Some(m) = caps.get(g) {
1065                        dst.push_str(m.as_str());
1066                    }
1067                    i += 2;
1068                    continue;
1069                }
1070                if nc == b'{' {
1071                    // ${N} backref — read digits until '}'.
1072                    let mut j = i + 2;
1073                    let mut g: usize = 0;
1074                    let mut any = false;
1075                    while j < bytes.len() && bytes[j].is_ascii_digit() {
1076                        g = g * 10 + (bytes[j] - b'0') as usize;
1077                        any = true;
1078                        j += 1;
1079                    }
1080                    if any && j < bytes.len() && bytes[j] == b'}' {
1081                        if let Some(m) = caps.get(g) {
1082                            dst.push_str(m.as_str());
1083                        }
1084                        i = j + 1;
1085                        continue;
1086                    }
1087                }
1088                if nc == b'$' {
1089                    dst.push('$');
1090                    i += 2;
1091                    continue;
1092                }
1093            }
1094            dst.push(bytes[i] as char);
1095            i += 1;
1096        }
1097    }
1098}
1099
1100impl Replacer for String {
1101    fn replace_into(&mut self, caps: &Captures<'_>, dst: &mut String) {
1102        let mut s = self.as_str();
1103        s.replace_into(caps, dst);
1104    }
1105}
1106
1107impl<F: FnMut(&Captures<'_>) -> String> Replacer for F {
1108    fn replace_into(&mut self, caps: &Captures<'_>, dst: &mut String) {
1109        dst.push_str(&self(caps));
1110    }
1111}
1112
1113// ---------- compile-time MAX_GROUPS sanity ----------
1114#[allow(dead_code)]
1115const _: () = {
1116    let _ = MAX_GROUPS;
1117};
1118
1119// ============================================================================
1120// Tests
1121// ============================================================================
1122
1123#[cfg(test)]
1124mod tests {
1125    use super::*;
1126
1127    #[test]
1128    fn test_simple_match() {
1129        let r = Regex::new(r"^hello$").unwrap();
1130        assert!(r.is_match("hello"));
1131        assert!(!r.is_match("hello world"));
1132    }
1133
1134    #[test]
1135    fn test_captures() {
1136        let r = Regex::new(r"(\w+)\s(\w+)").unwrap();
1137        let c = r.captures("foo bar").unwrap();
1138        assert_eq!(&c[0], "foo bar");
1139        assert_eq!(&c[1], "foo");
1140        assert_eq!(&c[2], "bar");
1141    }
1142
1143    #[test]
1144    fn test_alternation() {
1145        let r = Regex::new(r"cat|dog").unwrap();
1146        assert!(r.is_match("cat"));
1147        assert!(r.is_match("dog"));
1148        assert!(!r.is_match("fish"));
1149    }
1150
1151    #[test]
1152    fn test_quant() {
1153        let r = Regex::new(r"a{2,4}").unwrap();
1154        assert!(r.is_match("aa"));
1155        assert!(r.is_match("aaaa"));
1156        assert!(!r.is_match("a"));
1157    }
1158
1159    #[test]
1160    fn test_replace_simple() {
1161        let r = Regex::new(r"\d+").unwrap();
1162        let out = r.replace_all("a1b22c333", "X");
1163        assert_eq!(out, "aXbXcX");
1164    }
1165
1166    #[test]
1167    fn test_replace_backref() {
1168        let r = Regex::new(r"(\w+)").unwrap();
1169        let out = r.replace_all("hi", "<$1>");
1170        assert_eq!(out, "<hi>");
1171    }
1172
1173    #[test]
1174    fn test_classes() {
1175        let r = Regex::new(r"^[a-z]+$").unwrap();
1176        assert!(r.is_match("hello"));
1177        assert!(!r.is_match("Hello"));
1178    }
1179
1180    #[test]
1181    fn test_predef_class() {
1182        let r = Regex::new(r"^\d+$").unwrap();
1183        assert!(r.is_match("123"));
1184        assert!(!r.is_match("12a"));
1185    }
1186
1187    #[test]
1188    fn test_word_boundary() {
1189        let r = Regex::new(r"\bcat\b").unwrap();
1190        assert!(r.is_match("the cat sat"));
1191        assert!(!r.is_match("category"));
1192    }
1193
1194    #[test]
1195    fn test_meta_path_pattern() {
1196        // Pattern used by the struct port: ^([^$]+)\$([=~])(.+)$
1197        let r = Regex::new(r"^([^$]+)\$([=~])(.+)$").unwrap();
1198        let c = r.captures("name$=value").unwrap();
1199        assert_eq!(&c[1], "name");
1200        assert_eq!(&c[2], "=");
1201        assert_eq!(&c[3], "value");
1202    }
1203
1204    #[test]
1205    fn test_injection_full_pattern() {
1206        let r = Regex::new(r"^`(\$[A-Z]+|[^`]*)[0-9]*`$").unwrap();
1207        let c = r.captures("`$NAME`").unwrap();
1208        assert_eq!(&c[1], "$NAME");
1209        let c = r.captures("`foo.bar`").unwrap();
1210        assert_eq!(&c[1], "foo.bar");
1211    }
1212
1213    #[test]
1214    fn test_captures_iter() {
1215        let r = Regex::new(r"\d+").unwrap();
1216        let nums: Vec<String> = r
1217            .captures_iter("a1 b22 c333")
1218            .map(|c| c[0].to_string())
1219            .collect();
1220        assert_eq!(nums, vec!["1", "22", "333"]);
1221    }
1222
1223    #[test]
1224    fn test_replace_callback() {
1225        let r = Regex::new(r"(\w+)").unwrap();
1226        let out = r.replace_all("foo bar", |c: &Captures<'_>| c[1].to_uppercase());
1227        assert_eq!(out, "FOO BAR");
1228    }
1229}