Skip to main content

rustyfi_lang/
regexp.rs

1//! A backtracking regular-expression engine for OCaml's `Str` dialect.
2//!
3//! Upstream SATySFi's `regexp` values are `Str.regexp`s and its regexp
4//! primitives (`string-scan`, `string-match`, `split-on-regexp`) are thin
5//! wrappers over `Str.string_match` / `Str.search_forward`. This port models a
6//! `regexp` as its own pattern *string* (`regexp-of-string` is the identity),
7//! so the dialect has to be interpreted here.
8//!
9//! # Why `Str` and not a stock regex crate
10//!
11//! `Str`'s surface syntax is deliberately unlike PCRE, and the difference is
12//! not cosmetic — it inverts which characters are special:
13//!
14//! * grouping is `\(` … `\)`, and a bare `(` / `)` is a LITERAL parenthesis;
15//! * alternation is `\|`, and a bare `|` is a LITERAL bar;
16//! * `{` and `}` are always literal — `Str` has no counted repetition at all;
17//! * inside a `[…]` set, backslash is NOT an escape: `[\t]` is the two-element
18//!   set `{'\\', 't'}`.
19//!
20//! Feeding a `Str` pattern to a PCRE-syntax engine therefore does not merely
21//! fail, it silently means something else: `satysfi-code-printer`'s SATySFi
22//! identifier rule `\(\\\|\+\)?[a-zA-Z][a-zA-Z0-9-]*\|[0-9]+` would parse as a
23//! literal-parenthesis soup. Translating `Str` to PCRE is the same work as
24//! parsing `Str`, minus the control over match semantics, so the pattern is
25//! parsed directly here.
26//!
27//! # Semantics
28//!
29//! `Str` is a backtracking matcher (`strstubs.c`'s `re_match`), so quantifiers
30//! are Perl-style — greedy by default, leftmost-biased alternation, first
31//! match in backtracking order rather than leftmost-longest. This engine
32//! reproduces that, which is what makes `Str.matched_string` predictable:
33//! `a\|ab` matched against `"ab"` yields `"a"` in both.
34//!
35//! Supported: `.` (any but newline), `*`, `+`, `?` and their lazy `*?`, `+?`,
36//! `??` forms, `[…]` sets with ranges and a leading `^` complement, `^` / `$`
37//! (line anchors), `\|`, `\(`…`\)`, `\b`, `\1`–`\9` backreferences, and `\`
38//! quoting of any special character.
39
40use std::cell::{Cell, RefCell};
41use std::collections::HashMap;
42use std::rc::Rc;
43
44/// One element of a `[…]` character set.
45#[derive(Debug, Clone, PartialEq, Eq)]
46enum ClassItem {
47    Char(char),
48    Range(char, char),
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52enum Node {
53    /// Matches nothing and consumes nothing — the empty branch of `a\|`.
54    Empty,
55    Char(char),
56    /// `.` — any character except a newline, per `Str`.
57    Any,
58    Class {
59        negated: bool,
60        items: Vec<ClassItem>,
61    },
62    /// `^` — start of the text or just after a `\n`.
63    Bol,
64    /// `$` — end of the text or just before a `\n`.
65    Eol,
66    /// `\b` — a word-constituent/non-constituent boundary.
67    WordBoundary,
68    /// `\(` … `\)`; the index is 1-based, matching `Str.matched_group`.
69    Group(usize, Box<Node>),
70    /// `\1` … `\9`.
71    Backref(usize),
72    Concat(Vec<Node>),
73    /// `\|`, in source order — the matcher tries branches left to right.
74    Alt(Vec<Node>),
75    Repeat {
76        node: Box<Node>,
77        min: u32,
78        /// `None` for an unbounded `*` / `+`.
79        max: Option<u32>,
80        greedy: bool,
81    },
82}
83
84/// A parsed pattern, plus the group count needed to size the capture vector.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Regexp {
87    root: Node,
88    groups: usize,
89    /// The parser hit [`Budget::MAX_NESTING`] and read a `\(` as a literal
90    /// `(`. The tree below is therefore not this pattern, so matching it
91    /// reports [`GaveUp`] rather than answering from the degraded reading —
92    /// the parser's other degradations turn a malformed pattern into a
93    /// literal one, but this one would turn a WELL-FORMED pattern into a
94    /// different well-formed pattern, and answer confidently.
95    truncated: bool,
96}
97
98struct Parser<'p> {
99    src: &'p [char],
100    pos: usize,
101    groups: usize,
102    /// `\(` nesting still available. The grammar below is recursive descent —
103    /// `parse_atom` → `parse_alt` → `parse_concat` → `parse_repeat` →
104    /// `parse_atom` — so a pattern is free to drive the PARSER off the stack
105    /// even though nothing has matched yet. See [`Budget::MAX_NESTING`].
106    nesting: usize,
107    /// Set once the cap above has fired; see [`Regexp::truncated`].
108    truncated: bool,
109}
110
111impl<'p> Parser<'p> {
112    fn peek(&self) -> Option<char> {
113        self.src.get(self.pos).copied()
114    }
115
116    fn peek_at(&self, off: usize) -> Option<char> {
117        self.src.get(self.pos + off).copied()
118    }
119
120    fn bump(&mut self) -> Option<char> {
121        let c = self.peek();
122        if c.is_some() {
123            self.pos += 1;
124        }
125        c
126    }
127
128    /// alternation := concat (`\|` concat)*
129    fn parse_alt(&mut self) -> Node {
130        let mut branches = vec![self.parse_concat()];
131        while self.peek() == Some('\\') && self.peek_at(1) == Some('|') {
132            self.pos += 2;
133            branches.push(self.parse_concat());
134        }
135        if branches.len() == 1 {
136            branches.pop().unwrap()
137        } else {
138            Node::Alt(branches)
139        }
140    }
141
142    /// concat := repeat*, stopping at `\|` or `\)` (or end of input).
143    fn parse_concat(&mut self) -> Node {
144        let mut items: Vec<Node> = Vec::new();
145        loop {
146            match self.peek() {
147                None => break,
148                Some('\\') => match self.peek_at(1) {
149                    // Both terminate a concatenation; leave them for the caller.
150                    Some('|') | Some(')') => break,
151                    _ => {}
152                },
153                _ => {}
154            }
155            match self.parse_repeat() {
156                Some(node) => items.push(node),
157                None => break,
158            }
159        }
160        match items.len() {
161            0 => Node::Empty,
162            1 => items.pop().unwrap(),
163            _ => Node::Concat(items),
164        }
165    }
166
167    /// repeat := atom postfix*, where postfix is `*`, `+` or `?` with an
168    /// optional trailing `?` making it lazy.
169    fn parse_repeat(&mut self) -> Option<Node> {
170        let mut node = self.parse_atom()?;
171        loop {
172            let (min, max) = match self.peek() {
173                Some('*') => (0, None),
174                Some('+') => (1, None),
175                Some('?') => (0, Some(1)),
176                _ => break,
177            };
178            self.pos += 1;
179            // `Str` supports the lazy forms `*?`, `+?`, `??`. A `?` here is
180            // therefore a modifier, not another quantifier.
181            let greedy = if self.peek() == Some('?') {
182                self.pos += 1;
183                false
184            } else {
185                true
186            };
187            node = Node::Repeat {
188                node: Box::new(node),
189                min,
190                max,
191                greedy,
192            };
193        }
194        Some(node)
195    }
196
197    fn parse_atom(&mut self) -> Option<Node> {
198        let c = self.peek()?;
199        match c {
200            '.' => {
201                self.pos += 1;
202                Some(Node::Any)
203            }
204            '^' => {
205                self.pos += 1;
206                Some(Node::Bol)
207            }
208            '$' => {
209                self.pos += 1;
210                Some(Node::Eol)
211            }
212            '[' => {
213                self.pos += 1;
214                Some(self.parse_class())
215            }
216            // A quantifier with nothing to quantify is a literal in `Str`.
217            '*' | '+' | '?' => {
218                self.pos += 1;
219                Some(Node::Char(c))
220            }
221            '\\' => {
222                self.pos += 1;
223                let e = match self.bump() {
224                    Some(e) => e,
225                    // A trailing lone backslash is itself.
226                    None => return Some(Node::Char('\\')),
227                };
228                match e {
229                    // Past the nesting cap a `\(` reads as the literal `(` it
230                    // would be without the backslash — recursing further is
231                    // how the PARSER goes off the stack, before anything has
232                    // matched. `truncated` then makes every match report
233                    // `GaveUp`, so the degraded tree is never answered from.
234                    '(' if self.nesting == 0 => {
235                        self.truncated = true;
236                        Some(Node::Char('('))
237                    }
238                    '(' => {
239                        self.groups += 1;
240                        let idx = self.groups;
241                        self.nesting -= 1;
242                        let inner = self.parse_alt();
243                        self.nesting += 1;
244                        // Tolerate an unclosed `\(` rather than failing the
245                        // whole pattern: `Str` would raise, but a raised
246                        // exception here would abort a whole document over
247                        // one malformed rule.
248                        if self.peek() == Some('\\') && self.peek_at(1) == Some(')') {
249                            self.pos += 2;
250                        }
251                        Some(Node::Group(idx, Box::new(inner)))
252                    }
253                    'b' => Some(Node::WordBoundary),
254                    d if d.is_ascii_digit() && d != '0' => {
255                        Some(Node::Backref(d as usize - '0' as usize))
256                    }
257                    other => Some(Node::Char(other)),
258                }
259            }
260            other => {
261                self.pos += 1;
262                Some(Node::Char(other))
263            }
264        }
265    }
266
267    /// `[…]`. Per `Str`, a leading `^` complements, a `]` or `-` in first
268    /// position is literal, and backslash is NOT an escape inside the set.
269    fn parse_class(&mut self) -> Node {
270        let negated = if self.peek() == Some('^') {
271            self.pos += 1;
272            true
273        } else {
274            false
275        };
276        let mut items: Vec<ClassItem> = Vec::new();
277        let mut first = true;
278        loop {
279            let c = match self.peek() {
280                None => break,
281                Some(c) => c,
282            };
283            if c == ']' && !first {
284                self.pos += 1;
285                break;
286            }
287            self.pos += 1;
288            first = false;
289            // `a-z`, but a `-` in last position is a literal `-`.
290            if self.peek() == Some('-')
291                && self.peek_at(1).is_some()
292                && self.peek_at(1) != Some(']')
293            {
294                self.pos += 1;
295                let hi = self.bump().unwrap();
296                items.push(ClassItem::Range(c, hi));
297            } else {
298                items.push(ClassItem::Char(c));
299            }
300        }
301        Node::Class { negated, items }
302    }
303}
304
305impl Regexp {
306    /// Parse a `Str`-dialect pattern. Never fails: malformed input degrades to
307    /// the most literal reading, because a raised error here would take down a
308    /// whole document over a single bad highlighting rule.
309    pub fn parse(pattern: &str) -> Regexp {
310        let chars: Vec<char> = pattern.chars().collect();
311        let mut p = Parser {
312            src: &chars,
313            pos: 0,
314            groups: 0,
315            nesting: Budget::MAX_NESTING,
316            truncated: false,
317        };
318        let root = p.parse_alt();
319        Regexp {
320            root,
321            groups: p.groups,
322            truncated: p.truncated,
323        }
324    }
325
326    /// Anchored match at `start`, returning the end offset (in `char`s) of the
327    /// match — `Str.string_match` plus `Str.match_end`.
328    ///
329    /// A pattern that exhausts its step budget reports `Err(GaveUp)` rather
330    /// than "no match": see [`Budget`]. Silently answering `None` would turn a
331    /// hang into wrong output, which is worse.
332    pub fn match_at(&self, input: &[char], start: usize) -> Result<Option<usize>, GaveUp> {
333        if self.truncated {
334            return Err(GaveUp);
335        }
336        let m = Matcher::with_budget(input);
337        let mut caps = self.fresh_caps();
338        let out = self.attempt(&m, start, &mut caps);
339        if m.gave_up.get() {
340            Err(GaveUp)
341        } else {
342            Ok(out)
343        }
344    }
345
346    /// Leftmost match at or after `start` — `Str.search_forward`.
347    pub fn search_from(
348        &self,
349        input: &[char],
350        start: usize,
351    ) -> Result<Option<(usize, usize)>, GaveUp> {
352        // ONE budget for the whole search, not one per start position. A fresh
353        // `Matcher` per position — which is what calling `match_at` in the
354        // loop would build — makes the total `input.len() × budget`, i.e. the
355        // budget stops bounding anything as soon as the input is long, which
356        // is precisely when it needs to.
357        if self.truncated {
358            return Err(GaveUp);
359        }
360        let m = Matcher::with_budget(input);
361        let mut caps = self.fresh_caps();
362        for i in start..=input.len() {
363            caps.iter_mut().for_each(|c| *c = None);
364            let hit = self.attempt(&m, i, &mut caps);
365            if m.gave_up.get() {
366                return Err(GaveUp);
367            }
368            if let Some(end) = hit {
369                return Ok(Some((i, end)));
370            }
371        }
372        Ok(None)
373    }
374
375    fn fresh_caps(&self) -> Vec<Option<(usize, usize)>> {
376        vec![None; self.groups + 1]
377    }
378
379    /// One anchored attempt against an already-seeded budget.
380    fn attempt(
381        &self,
382        m: &Matcher<'_>,
383        start: usize,
384        caps: &mut Vec<Option<(usize, usize)>>,
385    ) -> Option<usize> {
386        m.node(&self.root, start, caps, &|end, _| Some(end))
387    }
388}
389
390/// The matcher ran out of steps or nesting depth before deciding.
391///
392/// This is the regexp twin of `rustyfi-syntax`'s `ParseFailureKind::GaveUp`,
393/// and it exists for the same reason: the matcher is an ordered-choice
394/// backtracker, so a pattern with a quantified group inside a quantifier
395/// (`\(a*\)*b`) costs a factor per nesting level. Unbounded, thirty
396/// characters of input take two minutes and thirty-two never finish — in a
397/// browser tab that is a freeze with no cancel, because the playground
398/// compiles on the main thread.
399#[derive(Clone, Copy, Debug, PartialEq, Eq)]
400pub struct GaveUp;
401
402struct Matcher<'i> {
403    input: &'i [char],
404    /// Work remaining. One unit per [`Matcher::node`] entry — every
405    /// alternative the backtracker tries — plus one per CHARACTER for the two
406    /// places a single entry buys a whole scan: `repeat`'s single-character
407    /// fast path and a backreference's slice compare. Charging those keeps
408    /// the budget a bound on work rather than merely on step count; without
409    /// it a step is O(input) and the wall clock is quadratic while the
410    /// counter looks linear.
411    fuel: Cell<u64>,
412    /// [`Matcher::node`] frames still available. Every recursion in the
413    /// matcher — `repeat`'s general branch, a nested `Group`, the `seq` chain
414    /// down a `Concat` — bottoms out in a `node` call whose frame stays live
415    /// while its continuation runs, so counting live `node` frames counts the
416    /// stack. Separate from `fuel` because the failure it prevents is
417    /// different: not slowness but a stack overflow, which aborts the process
418    /// natively and traps unrecoverably in wasm, where the shadow stack is
419    /// fixed at link time.
420    frames: Cell<u32>,
421    gave_up: Cell<bool>,
422}
423
424/// How much work a single [`Regexp::match_at`] may do.
425///
426/// Modelled on `rustyfi-syntax`'s `Budget`, and seeded the same way — a floor
427/// so a short input still gets a real answer, plus an allowance per input
428/// character so a long one is not cut off for being long. Only SUPERLINEAR
429/// backtracking can outrun it.
430struct Budget;
431
432impl Budget {
433    /// Work units per input character.
434    ///
435    /// Measured, not guessed. Across all 80 patterns `satysfi-code-printer`'s
436    /// `code-syntax.satyg` can produce — every `syntax-rule-line`, both halves
437    /// of every `syntax-rule-block`, and every keyword list joined the way
438    /// `strlst-to-syntax-rule` joins it — the worst honest cost against a
439    /// deliberately hostile 200,000-character input (an unterminated string
440    /// body, `"[^"]*"`, which scans to the end and then retries the closing
441    /// quote at every offset) is **2 units per character**. Everything else in
442    /// the corpus is O(1) in the input, because the single-character
443    /// quantifier takes the iterative fast path. 64 is a 32× margin on that.
444    ///
445    /// The old value here was 4,096, and the cost of that slack is paid on
446    /// the pathological side, because the give-up point is `PER_CHAR × len`
447    /// units away: `\(a*\)*b` against 200,000 characters took **21.7 s** to
448    /// give up. It now takes 0.17 s. Erring high is not free — a bound this
449    /// loose is a bound in name only, since nothing in a document is
450    /// interactive at twenty seconds.
451    const PER_CHAR: u64 = 64;
452
453    /// Floor, so a pattern against a short input is not capped below what a
454    /// long one gets. Also measured: the most work any corpus pattern does in
455    /// one anchored match is 835 units (the ~470-branch COBOL keyword
456    /// alternation), so this is a 1,197× margin, and about 25 ms of trying.
457    const FLOOR: u64 = 1_000_000;
458
459    fn for_input(len: usize) -> u64 {
460        ((len as u64).saturating_mul(Self::PER_CHAR)).max(Self::FLOOR)
461    }
462
463    /// Live [`Matcher::node`] frames allowed.
464    ///
465    /// Also measured, by bisecting the thread stack a match actually needs.
466    /// At 4,096 the worst shape found needs 1.3 MB in a release build and
467    /// 5.9 MB in a debug one — inside the wasm shadow stack 24× over, and
468    /// inside a default 8 MB thread stack even unoptimised.
469    ///
470    /// This replaced a cap on `repeat`'s recursion depth alone, which did NOT
471    /// bound the stack: the bytes a repeat LEVEL costs are a function of the
472    /// pattern nested inside it, so while `\(a\)*` needed 2.75 MB at the old
473    /// 5,000-level cap, `\(` × 60 around the same body needed **59 MB**
474    /// (227 MB in a debug build) — past the wasm shadow stack, and heading
475    /// for the CLI worker's 256 MB. Nor did it cover the other two recursions:
476    /// a 200,000-atom `Concat` took 37.6 MB and 100,000 nested `\(` took the
477    /// same, both without touching `repeat` at all. The four shapes now come
478    /// in at 1.3 MB / 0.9 MB / 0.2 MB / 0.4 MB.
479    const MAX_FRAMES: u32 = 4_096;
480
481    /// `\(` nesting the PARSER will descend into; see [`Parser::nesting`].
482    /// Four frames a level (`parse_atom` → `parse_alt` → `parse_concat` →
483    /// `parse_repeat`) at about 850 bytes, so under a megabyte at the cap.
484    /// Unbounded, `\(` × 300,000 overflowed a 256 MB stack before a single
485    /// character had been matched. The deepest pattern in the corpus nests
486    /// twice.
487    const MAX_NESTING: usize = 1_024;
488}
489
490/// Returns a [`Matcher::frames`] slot on scope exit, so a backtracked branch
491/// does not permanently consume nesting the way a bare decrement would.
492struct Frame<'a, 'i>(&'a Matcher<'i>);
493
494impl Drop for Frame<'_, '_> {
495    fn drop(&mut self) {
496        self.0.frames.set(self.0.frames.get() + 1);
497    }
498}
499
500/// The continuation a node hands its remainder to. Returning `Some(end)`
501/// means the whole match succeeded and `end` is its final offset; `None`
502/// asks the node to backtrack and try its next alternative.
503type Cont<'k> = &'k dyn Fn(usize, &mut Vec<Option<(usize, usize)>>) -> Option<usize>;
504
505fn is_word_char(c: char) -> bool {
506    c.is_alphanumeric() || c == '_'
507}
508
509impl<'i> Matcher<'i> {
510    fn with_budget(input: &'i [char]) -> Self {
511        Matcher {
512            input,
513            fuel: Cell::new(Budget::for_input(input.len())),
514            frames: Cell::new(Budget::MAX_FRAMES),
515            gave_up: Cell::new(false),
516        }
517    }
518
519    /// Spend `n` units of work. `false` means the budget is gone and the
520    /// caller must unwind — every `node` alternative checks this, so
521    /// exhaustion stops the whole match rather than being mistaken for a
522    /// failed alternative.
523    fn spend(&self, n: u64) -> bool {
524        if self.gave_up.get() {
525            return false;
526        }
527        match self.fuel.get().checked_sub(n) {
528            Some(left) => {
529                self.fuel.set(left);
530                true
531            }
532            None => {
533                self.fuel.set(0);
534                self.gave_up.set(true);
535                false
536            }
537        }
538    }
539
540    /// Take one step and one stack frame, returning the frame on scope exit.
541    /// `None` means one of the two budgets is gone.
542    fn enter(&self) -> Option<Frame<'_, 'i>> {
543        if !self.spend(1) {
544            return None;
545        }
546        match self.frames.get() {
547            0 => {
548                self.gave_up.set(true);
549                None
550            }
551            n => {
552                self.frames.set(n - 1);
553                Some(Frame(self))
554            }
555        }
556    }
557
558    fn single(&self, node: &Node, pos: usize) -> Option<usize> {
559        let c = *self.input.get(pos)?;
560        let ok = match node {
561            Node::Char(want) => c == *want,
562            Node::Any => c != '\n',
563            Node::Class { negated, items } => {
564                let hit = items.iter().any(|it| match it {
565                    ClassItem::Char(x) => c == *x,
566                    ClassItem::Range(lo, hi) => *lo <= c && c <= *hi,
567                });
568                hit != *negated
569            }
570            _ => return None,
571        };
572        if ok {
573            Some(pos + 1)
574        } else {
575            None
576        }
577    }
578
579    fn node(
580        &self,
581        node: &Node,
582        pos: usize,
583        caps: &mut Vec<Option<(usize, usize)>>,
584        k: Cont<'_>,
585    ) -> Option<usize> {
586        let _frame = self.enter()?;
587        match node {
588            Node::Empty => k(pos, caps),
589            Node::Char(_) | Node::Any | Node::Class { .. } => match self.single(node, pos) {
590                Some(next) => k(next, caps),
591                None => None,
592            },
593            Node::Bol => {
594                if pos == 0 || self.input.get(pos - 1) == Some(&'\n') {
595                    k(pos, caps)
596                } else {
597                    None
598                }
599            }
600            Node::Eol => {
601                if pos == self.input.len() || self.input.get(pos) == Some(&'\n') {
602                    k(pos, caps)
603                } else {
604                    None
605                }
606            }
607            Node::WordBoundary => {
608                let before = pos > 0 && self.input.get(pos - 1).copied().is_some_and(is_word_char);
609                let after = pos < self.input.len() && is_word_char(self.input[pos]);
610                if before != after {
611                    k(pos, caps)
612                } else {
613                    None
614                }
615            }
616            Node::Backref(n) => {
617                let (s, e) = match caps.get(*n).copied().flatten() {
618                    Some(span) => span,
619                    // An unset group matches the empty string, as in `Str`.
620                    None => return k(pos, caps),
621                };
622                let len = e - s;
623                if pos + len > self.input.len() {
624                    return None;
625                }
626                // The compare below is O(len) for what the entry step already
627                // paid one unit for, and a group can capture the whole input:
628                // `\(a*\)\1\1x` did 5e9 character compares against 200,000
629                // characters while spending 200,001 units. Charge the compare.
630                if !self.spend(len as u64) {
631                    return None;
632                }
633                if self.input[pos..pos + len] == self.input[s..e] {
634                    k(pos + len, caps)
635                } else {
636                    None
637                }
638            }
639            Node::Group(idx, inner) => {
640                let idx = *idx;
641                let saved = caps.get(idx).copied().flatten();
642                let start = pos;
643                let out = self.node(inner, pos, caps, &|end, caps| {
644                    let prev = caps[idx];
645                    caps[idx] = Some((start, end));
646                    match k(end, caps) {
647                        Some(v) => Some(v),
648                        None => {
649                            caps[idx] = prev;
650                            None
651                        }
652                    }
653                });
654                if out.is_none() {
655                    caps[idx] = saved;
656                }
657                out
658            }
659            Node::Concat(items) => self.seq(items, pos, caps, k),
660            Node::Alt(branches) => {
661                for b in branches {
662                    if let Some(v) = self.node(b, pos, caps, k) {
663                        return Some(v);
664                    }
665                }
666                None
667            }
668            Node::Repeat {
669                node,
670                min,
671                max,
672                greedy,
673            } => self.repeat(node, *min, *max, *greedy, pos, caps, k),
674        }
675    }
676
677    fn seq(
678        &self,
679        items: &[Node],
680        pos: usize,
681        caps: &mut Vec<Option<(usize, usize)>>,
682        k: Cont<'_>,
683    ) -> Option<usize> {
684        // A run of single-character nodes is DETERMINISTIC — each matches one
685        // character or fails, with nothing to backtrack into — so walk it
686        // iteratively. Going through `node` per item would cost one live
687        // frame per PATTERN character, and a literal run is the commonest
688        // long thing a pattern has: `strlst-to-syntax-rule` builds a keyword
689        // alternation whose every branch is one. The step accounting is
690        // unchanged, one unit per item either way.
691        let mut pos = pos;
692        let mut items = items;
693        while let Some((head, rest)) = items.split_first() {
694            if !matches!(head, Node::Char(_) | Node::Any | Node::Class { .. }) {
695                break;
696            }
697            if !self.spend(1) {
698                return None;
699            }
700            pos = self.single(head, pos)?;
701            items = rest;
702        }
703        match items.split_first() {
704            None => k(pos, caps),
705            Some((head, rest)) => self.node(head, pos, caps, &|p, caps| self.seq(rest, p, caps, k)),
706        }
707    }
708
709    #[allow(clippy::too_many_arguments)]
710    fn repeat(
711        &self,
712        node: &Node,
713        min: u32,
714        max: Option<u32>,
715        greedy: bool,
716        pos: usize,
717        caps: &mut Vec<Option<(usize, usize)>>,
718        k: Cont<'_>,
719    ) -> Option<usize> {
720        // Fast path for the overwhelmingly common `X*` / `X+` where `X` is a
721        // single-character matcher (`[^"]*`, `[a-z]+`, `.*`). Recursing once
722        // per repetition would make stack depth proportional to input length;
723        // a code block a few thousand characters long would then risk
724        // overflowing on a pattern as ordinary as a string literal's body.
725        if matches!(node, Node::Char(_) | Node::Any | Node::Class { .. }) {
726            // One `single` call per repetition is real work that the single
727            // step this `node` entry already paid for would otherwise buy
728            // without limit, so clamp the scan to what is left of the budget
729            // and charge what it consumes.
730            let hard = max.map_or(u64::MAX, u64::from);
731            let afford = self.fuel.get();
732            let cap = hard.min(afford);
733            let mut cur = pos;
734            let mut count = 0u64;
735            while count < cap {
736                match self.single(node, cur) {
737                    Some(next) => {
738                        cur = next;
739                        count += 1;
740                    }
741                    None => break,
742                }
743            }
744            self.fuel.set(afford - count);
745            if count == cap && cap < hard {
746                // Stopped because the budget ran out, not because the input
747                // did: a truncated repetition would be a WRONG answer.
748                self.gave_up.set(true);
749                return None;
750            }
751            // A single-character node consumes exactly one character, so the
752            // end offset for `i` repetitions is `pos + i` — the admissible
753            // ones run from `min` to `count`. (This used to be materialised as
754            // a `Vec` of offsets, which is an O(input) allocation per scan for
755            // a sequence that is just addition.)
756            let lo = u64::from(min);
757            if count < lo {
758                return None;
759            }
760            let mut try_at = |i: u64| k(pos + i as usize, caps);
761            if greedy {
762                for i in (lo..=count).rev() {
763                    if let Some(v) = try_at(i) {
764                        return Some(v);
765                    }
766                }
767            } else {
768                for i in lo..=count {
769                    if let Some(v) = try_at(i) {
770                        return Some(v);
771                    }
772                }
773            }
774            return None;
775        }
776
777        // General case: `node` can consume a variable amount, so recurse —
778        // one `node` frame per repetition, which is what `Budget::MAX_FRAMES`
779        // bounds, along with every other recursion in the matcher.
780        let more = |m: &Matcher<'i>, p: usize, caps: &mut Vec<Option<(usize, usize)>>| {
781            if max == Some(0) {
782                return None;
783            }
784            let next_min = min.saturating_sub(1);
785            let next_max = max.map(|m| m - 1);
786            m.node(node, p, caps, &|q, caps| {
787                // A zero-width body would otherwise loop forever.
788                if q == p && next_min == 0 {
789                    return None;
790                }
791                m.repeat(node, next_min, next_max, greedy, q, caps, k)
792            })
793        };
794
795        if min > 0 {
796            return more(self, pos, caps);
797        }
798        if greedy {
799            match more(self, pos, caps) {
800                Some(v) => Some(v),
801                None => k(pos, caps),
802            }
803        } else {
804            match k(pos, caps) {
805                Some(v) => Some(v),
806                None => more(self, pos, caps),
807            }
808        }
809    }
810}
811
812thread_local! {
813    /// `code-printer` calls `regexp-of-string` inside its lexer loop, and this
814    /// port's `regexp` is the pattern string itself, so the same handful of
815    /// patterns are re-parsed once per scanned character. Memoizing keeps that
816    /// linear in the input rather than in input × pattern length.
817    static CACHE: RefCell<HashMap<String, Rc<Regexp>>> = RefCell::new(HashMap::new());
818
819    /// Total pattern TEXT cached, in chars. The cap is on text size rather
820    /// than on entry count because the entries are not uniform: a parsed
821    /// pattern is
822    /// roughly thirty times its own text, so 4,096 twenty-thousand-character
823    /// patterns — cheap to generate from a document, `arabic i ^ body` will do
824    /// — reach 2.6 GB while never approaching an entry-count limit.
825    static CACHE_CHARS: Cell<usize> = const { Cell::new(0) };
826}
827
828/// Cached pattern text allowed before the table is dropped, in chars. The
829/// real corpus caches a few hundred characters in total.
830const CACHE_CHAR_LIMIT: usize = 4 * 1024 * 1024;
831
832/// Parse `pattern`, reusing a previously parsed copy when possible.
833pub fn compile(pattern: &str) -> Rc<Regexp> {
834    CACHE.with(|c| {
835        let mut c = c.borrow_mut();
836        if let Some(re) = c.get(pattern) {
837            return Rc::clone(re);
838        }
839        // The pattern set a document uses is small and fixed (one per
840        // language rule), but cap the table anyway so a program generating
841        // patterns cannot grow it without bound.
842        CACHE_CHARS.with(|n| {
843            if n.get() + pattern.chars().count() > CACHE_CHAR_LIMIT {
844                c.clear();
845                n.set(0);
846            }
847            n.set(n.get() + pattern.chars().count());
848        });
849        let re = Rc::new(Regexp::parse(pattern));
850        c.insert(pattern.to_string(), Rc::clone(&re));
851        re
852    })
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    fn scan(pat: &str, input: &str) -> Option<String> {
860        let re = Regexp::parse(pat);
861        let chars: Vec<char> = input.chars().collect();
862        re.match_at(&chars, 0)
863            .expect("this pattern must not exhaust the budget")
864            .map(|end| chars[..end].iter().collect())
865    }
866
867    /// `scan`, for a pattern that is EXPECTED to run out of budget.
868    fn scan_gives_up(pat: &str, input: &str) -> bool {
869        let re = Regexp::parse(pat);
870        let chars: Vec<char> = input.chars().collect();
871        re.match_at(&chars, 0).is_err()
872    }
873
874    #[test]
875    fn literals_and_escapes() {
876        assert_eq!(scan("abc", "abcdef").as_deref(), Some("abc"));
877        assert_eq!(scan("abc", "abdef"), None);
878        // `(` and `|` are LITERAL in `Str`, unlike PCRE.
879        assert_eq!(scan("(a)", "(a)b").as_deref(), Some("(a)"));
880        assert_eq!(scan("a|b", "a|b").as_deref(), Some("a|b"));
881        assert_eq!(scan("{2}", "{2}x").as_deref(), Some("{2}"));
882        assert_eq!(scan(r"\*", "*x").as_deref(), Some("*"));
883    }
884
885    #[test]
886    fn alternation_is_leftmost_biased_not_longest() {
887        // `Str`, like Perl and unlike POSIX, returns the FIRST branch that
888        // matches rather than the longest.
889        assert_eq!(scan(r"a\|ab", "ab").as_deref(), Some("a"));
890        assert_eq!(scan(r"ab\|a", "ab").as_deref(), Some("ab"));
891    }
892
893    #[test]
894    fn quantifiers_greedy_and_lazy() {
895        assert_eq!(scan("a*", "aaab").as_deref(), Some("aaa"));
896        assert_eq!(scan("a*?", "aaab").as_deref(), Some(""));
897        assert_eq!(scan("a+", "aaab").as_deref(), Some("aaa"));
898        assert_eq!(scan("a+?", "aaab").as_deref(), Some("a"));
899        assert_eq!(scan("ab?", "ab").as_deref(), Some("ab"));
900        assert_eq!(scan("ab?", "ac").as_deref(), Some("a"));
901        assert_eq!(scan("a+", "b"), None);
902    }
903
904    #[test]
905    fn character_classes() {
906        assert_eq!(scan("[a-z]+", "abcD").as_deref(), Some("abc"));
907        assert_eq!(scan("[^\"]*", "ab\"c").as_deref(), Some("ab"));
908        assert_eq!(scan("[]a]+", "]a]b").as_deref(), Some("]a]"));
909        assert_eq!(scan("[a-]+", "a-a!").as_deref(), Some("a-a"));
910        // Backslash is not an escape inside a set: `[\t]` is `{'\\','t'}`.
911        assert_eq!(scan(r"[\t]+", r"t\t;").as_deref(), Some(r"t\t"));
912    }
913
914    #[test]
915    fn anchors() {
916        assert_eq!(scan("$", "").as_deref(), Some(""));
917        assert_eq!(scan("$", "\nx").as_deref(), Some(""));
918        assert_eq!(scan("$", "x\n"), None);
919        assert_eq!(scan("^a", "a"). as_deref(), Some("a"));
920    }
921
922    #[test]
923    fn groups_and_backrefs() {
924        assert_eq!(scan(r"\(ab\)+", "ababc").as_deref(), Some("abab"));
925        assert_eq!(scan(r"\(a\|b\)c", "bc").as_deref(), Some("bc"));
926        assert_eq!(scan(r"\(a\)\1", "aa").as_deref(), Some("aa"));
927        assert_eq!(scan(r"\(a\)\1", "ab"), None);
928    }
929
930    #[test]
931    fn code_printer_real_patterns() {
932        // The SATySFi identifier rule from `code-syntax.satyg`.
933        let ident = r"\(\\\|\+\)?[a-zA-Z][a-zA-Z0-9-]*\|[0-9]+\|0x[0-9a-fA-F]+";
934        assert_eq!(scan(ident, "let-rec x").as_deref(), Some("let-rec"));
935        assert_eq!(scan(ident, r"\emph{a}").as_deref(), Some(r"\emph"));
936        assert_eq!(scan(ident, "+section{}").as_deref(), Some("+section"));
937        assert_eq!(scan(ident, "123abc").as_deref(), Some("123"));
938        // The rule's own third branch, `0x[0-9a-fA-F]+`, is unreachable: the
939        // second branch `[0-9]+` is tried first and always matches the
940        // leading `0`. That is what `Str` does too — the port must reproduce
941        // the quirk, not "fix" it, or highlighting would drift from upstream.
942        assert_eq!(scan(ident, "0x1F;").as_deref(), Some("0"));
943
944        // Rust identifiers, and the block-comment delimiters.
945        assert_eq!(scan("[a-zA-Z][a-zA-Z0-9_]*!?", "println!(").as_deref(), Some("println!"));
946        assert_eq!(scan(r"/\*", "/* c */").as_deref(), Some("/*"));
947        assert_eq!(scan(r"\*/", "*/ rest").as_deref(), Some("*/"));
948        // A double-quoted string body.
949        assert_eq!(scan("\"[^\"]*\"", "\"hi\" x").as_deref(), Some("\"hi\""));
950    }
951
952    #[test]
953    fn long_repeat_does_not_overflow_the_stack() {
954        // The single-character fast path must keep this iterative.
955        let long: String = std::iter::repeat('a').take(200_000).collect();
956        assert_eq!(scan("[a-z]*", &long).map(|s| s.len()), Some(200_000));
957    }
958
959    /// The case the test above does NOT cover, and which used to abort the
960    /// process: a repeated GROUP takes `repeat`'s recursive branch, one frame
961    /// per repetition. `[a-z]*` above is the single-character fast path — it
962    /// was never at risk, so it could not have caught this.
963    #[test]
964    fn a_repeated_group_over_a_long_input_gives_up_instead_of_overflowing() {
965        let long: String = std::iter::repeat('a').take(200_000).collect();
966        // Either it matches or it gives up; what it must not do is abort.
967        let re = Regexp::parse("\\(a\\)*");
968        let chars: Vec<char> = long.chars().collect();
969        let _ = re.match_at(&chars, 0);
970    }
971
972    /// Catastrophic backtracking is bounded. Unbounded, this took two minutes
973    /// at thirty characters and did not finish at thirty-two.
974    #[test]
975    fn a_quantified_group_inside_a_quantifier_gives_up_rather_than_hanging() {
976        let input: String = std::iter::repeat('a').take(64).collect::<String>() + "!";
977        assert!(
978            scan_gives_up("\\(a*\\)*b", &input),
979            "the exponential pattern should have exhausted its budget"
980        );
981        // And the budget must not fire on ordinary work: the same shape that
982        // CAN match still does, promptly.
983        assert_eq!(scan("\\(a*\\)*b", "aaab").as_deref(), Some("aaab"));
984    }
985
986    /// `match_at` is `pub` and takes an arbitrary `start`; `^` and `\b` used
987    /// to index `input[pos - 1]` unguarded and panic past the end.
988    ///
989    /// The three patterns below the fold are the ones that actually REACH the
990    /// anchor: `"a$"` does not, because the `a` fails on `input.get(pos)`
991    /// first and the `$` is never evaluated — so it was `$`, indexing
992    /// `input[pos]` forward, that was still panicking after `^` and `\b` were
993    /// fixed. A guard test has to be written against the node under test, not
994    /// against a pattern that merely contains it.
995    #[test]
996    fn match_at_past_the_end_does_not_panic() {
997        let chars: Vec<char> = "abc".chars().collect();
998        for pat in ["^a", "\\ba", "a$", "$", "a*$", "^", "\\b", "\\(a\\)*$"] {
999            let _ = Regexp::parse(pat).match_at(&chars, 7);
1000        }
1001    }
1002
1003    #[test]
1004    fn search_forward_finds_leftmost() {
1005        let re = Regexp::parse("b+");
1006        let chars: Vec<char> = "aabbbc".chars().collect();
1007        assert_eq!(re.search_from(&chars, 0), Ok(Some((2, 5))));
1008        assert_eq!(re.search_from(&chars, 5), Ok(None));
1009    }
1010
1011    /// `search_from` tries every start position, so a per-position budget
1012    /// would let the total reach `len × budget` — the bound would stop
1013    /// bounding exactly when the input got long. One shared budget means a
1014    /// pattern that is pathological at ONE position is refused for the whole
1015    /// search rather than being paid for at every one of them.
1016    #[test]
1017    fn a_leftmost_search_shares_one_budget_across_start_positions() {
1018        let input: String = "a".repeat(4_000);
1019        let chars: Vec<char> = input.chars().collect();
1020        let re = Regexp::parse("\\(a*\\)*b");
1021        let t0 = std::time::Instant::now();
1022        assert_eq!(re.search_from(&chars, 0), Err(GaveUp));
1023        // Per-position budgets would multiply this by 4,000.
1024        assert!(
1025            t0.elapsed().as_secs() < 5,
1026            "the search re-seeded its budget per start position"
1027        );
1028    }
1029
1030    /// The work a step buys must be bounded, or the counter is linear while
1031    /// the clock is quadratic: the group here captures a prefix of the input
1032    /// and the backreference compares it character by character, so one step
1033    /// used to buy an O(input) `memcmp`. 200,000 characters cost 5e9 compares
1034    /// for 200,001 steps.
1035    #[test]
1036    fn a_backreference_is_charged_for_the_characters_it_compares() {
1037        let input: String = "a".repeat(20_000);
1038        let chars: Vec<char> = input.chars().collect();
1039        let t0 = std::time::Instant::now();
1040        assert!(Regexp::parse("\\(a*\\)\\1\\1x").match_at(&chars, 0).is_err());
1041        assert!(t0.elapsed().as_secs() < 5, "the compare was not charged");
1042        // A backreference to a SHORT group — the shape real patterns use, a
1043        // matching quote or bracket — is unaffected.
1044        assert_eq!(scan(r"\(['`]\)[a-z]*\1", "'abc'!").as_deref(), Some("'abc'"));
1045    }
1046
1047    /// Same point for `repeat`'s single-character fast path, which scans the
1048    /// whole input for the one step its `node` entry paid for. Charging it is
1049    /// what keeps a give-up fast: `\(a*\)*b` against 200,000 characters took
1050    /// 21.7 s to give up before, and 0.17 s after.
1051    #[test]
1052    fn a_quantifier_scan_is_charged_for_the_characters_it_consumes() {
1053        let input: String = "a".repeat(200_000);
1054        let chars: Vec<char> = input.chars().collect();
1055        let t0 = std::time::Instant::now();
1056        assert!(Regexp::parse("\\(a*\\)*b").match_at(&chars, 0).is_err());
1057        assert!(
1058            t0.elapsed().as_secs() < 10,
1059            "giving up took longer than doing the work would have"
1060        );
1061        // The charge must not make an honest scan of the same input fail.
1062        assert_eq!(scan("[a-z]*", &input).map(|s| s.len()), Some(200_000));
1063    }
1064
1065    /// A pattern nested deeper than [`Budget::MAX_NESTING`] drives the PARSER
1066    /// off the stack, before a single character has been matched — `\(` ×
1067    /// 300,000 overflowed a 256 MB stack. The parser stops descending, and
1068    /// because the tree it then has is a DIFFERENT well-formed pattern rather
1069    /// than a malformed one read literally, matching it reports the give-up
1070    /// instead of answering.
1071    #[test]
1072    fn a_pattern_nested_past_the_parser_cap_is_refused_not_reinterpreted() {
1073        let deep = format!("{}a{}", r"\(".repeat(100_000), r"\)".repeat(100_000));
1074        let chars: Vec<char> = "a".chars().collect();
1075        assert_eq!(Regexp::parse(&deep).match_at(&chars, 0), Err(GaveUp));
1076        // Just inside the cap still works.
1077        let ok = format!("{}a{}", r"\(".repeat(1_000), r"\)".repeat(1_000));
1078        assert_eq!(Regexp::parse(&ok).match_at(&chars, 0), Ok(Some(1)));
1079    }
1080
1081    /// A long run of single-character atoms is deterministic, so `seq` walks
1082    /// it iteratively. Recursing per atom made the STACK proportional to the
1083    /// pattern's length — 200,000 literal characters needed 37.6 MB — which
1084    /// no cap on `repeat`'s depth could see.
1085    #[test]
1086    fn a_long_literal_run_does_not_recurse_per_character() {
1087        let long: String = "a".repeat(200_000);
1088        assert_eq!(scan(&long, &long).map(|s| s.len()), Some(200_000));
1089    }
1090}