Skip to main content

abnf_oracle/
recognize.rs

1//! The set-of-positions recognizer (SCOPE.md 6.2, 6.3).
2//!
3//! For an element and a start position, this computes the set of **all** end positions at which
4//! the element can finish. Unordered alternation and ambiguity are then correct by
5//! construction: nothing ever commits to the first branch that matched, because nothing ever
6//! returns a single answer to commit to. Any code here shaped like `if let Some(end) = …` would
7//! be reintroducing PEG semantics by accident.
8//!
9//! A recognizer is bound to one input for its lifetime (D2). The memo table is keyed by
10//! `(rule, position)` and means nothing against a different input, so there is no API for
11//! swapping one in.
12//!
13//! Memoization is a pure optimization: results with it disabled must be identical, and the
14//! tests run the whole §6.3 regression table both ways to keep that honest.
15
16use std::collections::{BTreeSet, HashMap};
17
18use crate::ast::{CharVal, Node, NodeId, NumVal, Repeat};
19use crate::check::CheckedGrammar;
20use crate::error::MatchError;
21
22/// How deep the recognizer may recurse before giving up: [`MatchOptions::max_depth`]'s default.
23///
24/// Chosen to be safe on a 1 MB stack, the smallest a caller is realistically on, and measured
25/// rather than guessed — see the table on [`MatchOptions::max_depth`].
26pub const DEFAULT_MAX_DEPTH: usize = 256;
27
28/// Resource limits for recognition.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct MatchOptions {
31    /// Give up after this many steps, rather than running unboundedly.
32    ///
33    /// Default `None`. Exceeding it is [`MatchError::StepLimit`], never a silent rejection:
34    /// "does not match" and "could not decide" are different answers (D16).
35    pub max_steps: Option<u64>,
36
37    /// Give up after recursing this deep. Default [`DEFAULT_MAX_DEPTH`].
38    ///
39    /// # Why this one has a default when `max_steps` does not
40    ///
41    /// Exceeding a step limit returns [`MatchError::StepLimit`]; exceeding the *stack* aborts
42    /// the process, and no caller can catch that or report it as "could not decide". An oracle
43    /// is fed adversarial input by design, so the safe default is a limit rather than a crash.
44    ///
45    /// # Depth, stack, and how they relate
46    ///
47    /// Depth counts **grammar nodes entered**, not input characters. Nesting in the input turns
48    /// into depth because a recursive rule re-enters itself, at a rate the grammar fixes: for
49    /// RFC 8259 one level of `[` costs six nodes, so the default allows JSON nested about 40
50    /// deep. A flat input costs almost nothing however long it is — length is not the problem,
51    /// nesting is.
52    ///
53    /// Each level costs roughly 2 KB of stack, measured on a debug build; a release build uses
54    /// less. That fixes the ceiling at about one level per 2 KB:
55    ///
56    /// | stack | deepest safe `max_depth` |
57    /// |---|---|
58    /// | 512 KB | 128 |
59    /// | 1 MB | 400 |
60    /// | 2 MiB — what [`std::thread`] gives a spawned thread | 900 |
61    /// | 64 MB | ~30,000 |
62    ///
63    /// The default sits under the 1 MB row, so it is safe wherever the recognizer is called
64    /// from. Raising it means giving the recognizer a bigger stack to match:
65    ///
66    /// ```no_run
67    /// # use abnf_oracle::{Grammar, MatchOptions, Recognizer};
68    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
69    /// # let src = "start = \"a\"\r\n";
70    /// # let grammar = Grammar::parse(src)?.check().map_err(|e| format!("{e:?}"))?;
71    /// std::thread::Builder::new()
72    ///     .stack_size(64 * 1024 * 1024)
73    ///     .spawn(move || {
74    ///         let mut recognizer = Recognizer::new(&grammar, "a").with_options(MatchOptions {
75    ///             max_depth: Some(40_000),
76    ///             ..MatchOptions::default()
77    ///         });
78    ///         recognizer.accepts("start")
79    ///     })?
80    ///     .join()
81    ///     .expect("no overflow")?;
82    /// # Ok(())
83    /// # }
84    /// ```
85    ///
86    /// `None` removes the limit, which is only safe if the input's nesting is already bounded
87    /// by something else.
88    pub max_depth: Option<usize>,
89}
90
91impl Default for MatchOptions {
92    fn default() -> Self {
93        Self {
94            max_steps: None,
95            max_depth: Some(DEFAULT_MAX_DEPTH),
96        }
97    }
98}
99
100/// What is known about one `(rule, position)` pair.
101enum Memo {
102    /// Being evaluated: seeing this again at the same position is left recursion.
103    InProgress,
104    /// Evaluated, with these end positions.
105    Done(BTreeSet<usize>),
106}
107
108/// Decides whether an input is in the language of a rule.
109///
110/// Bound to one input, and constructible only from a [`CheckedGrammar`] (D1), so there is no
111/// way to recognize against a grammar whose structure was never validated.
112pub struct Recognizer<'g, 'i> {
113    grammar: &'g CheckedGrammar,
114    src: &'i str,
115    /// The input as scalar values. Positions index this, not the bytes: the matching domain is
116    /// Unicode scalar values (SCOPE.md 6.1).
117    input: Vec<char>,
118    memo: HashMap<(usize, usize), Memo>,
119    steps: u64,
120    /// How deep the walk currently is, so the limit can be enforced on the way down.
121    depth: usize,
122    options: MatchOptions,
123    memoize: bool,
124}
125
126impl<'g, 'i> Recognizer<'g, 'i> {
127    /// Binds a recognizer to a grammar and one input.
128    #[must_use]
129    pub fn new(grammar: &'g CheckedGrammar, input: &'i str) -> Self {
130        Self {
131            grammar,
132            src: input,
133            input: input.chars().collect(),
134            memo: HashMap::new(),
135            steps: 0,
136            depth: 0,
137            options: MatchOptions::default(),
138            memoize: true,
139        }
140    }
141
142    /// Sets resource limits.
143    #[must_use]
144    pub fn with_options(mut self, options: MatchOptions) -> Self {
145        self.options = options;
146        self
147    }
148
149    /// The input this recognizer is bound to.
150    #[must_use]
151    pub fn input(&self) -> &'i str {
152        self.src
153    }
154
155    /// How many steps have been taken: one per repetition iteration and one per rule body
156    /// evaluated.
157    ///
158    /// Counted over the whole life of this recognizer and never reset, so
159    /// [`MatchOptions::max_steps`] is a budget for everything asked of it rather than for one
160    /// call. A recognizer is bound to one input, and its memo table is shared across calls, so
161    /// the work really is cumulative. [`crate::Generator::steps`] is the other way round.
162    ///
163    /// This is the counter [`MatchOptions::max_steps`] limits. Performance tests assert against
164    /// it rather than against wall-clock time (D31).
165    #[must_use]
166    pub const fn steps(&self) -> u64 {
167        self.steps
168    }
169
170    /// Turns the memo table off. For this crate's own tests, which assert that it changes
171    /// nothing but the work done.
172    #[doc(hidden)]
173    #[must_use]
174    pub fn without_memoization(mut self) -> Self {
175        self.memoize = false;
176        self
177    }
178
179    /// Whether the whole input is in the language of `rule`.
180    ///
181    /// # Errors
182    ///
183    /// The compatibility limits of SCOPE.md 6.6, checked before any input is examined, or
184    /// [`MatchError::StepLimit`]. Never an error for "does not match" — that is `Ok(false)`.
185    pub fn accepts(&mut self, rule: &str) -> Result<bool, MatchError> {
186        let ends = self.end_positions(rule, 0)?;
187        Ok(ends.contains(&self.input.len()))
188    }
189
190    /// Every position at which `rule` can finish, starting from `start`.
191    ///
192    /// The full answer, not the longest or the first: an ambiguous rule reports every ending it
193    /// has, which is what makes alternation unordered rather than merely untried.
194    ///
195    /// # Errors
196    ///
197    /// As [`Recognizer::accepts`].
198    pub fn end_positions(
199        &mut self,
200        rule: &str,
201        start: usize,
202    ) -> Result<BTreeSet<usize>, MatchError> {
203        // Before examining any input (SCOPE.md 6.5): a rule that can reach a prose value has no
204        // matching semantics at all, and saying "does not match" would be a lie.
205        self.grammar.can_recognize(rule)?;
206        let body = self
207            .grammar
208            .rule(rule)
209            .ok_or_else(|| MatchError::UnknownRule(rule.to_owned()))?
210            .body;
211        self.match_node(body, start)
212    }
213
214    /// The end positions of `node`, starting at `pos`.
215    ///
216    /// Every descent goes through here, so this is where the depth limit is enforced — before
217    /// the frame that would overflow is pushed rather than after.
218    fn match_node(&mut self, node: NodeId, pos: usize) -> Result<BTreeSet<usize>, MatchError> {
219        self.depth += 1;
220        if self
221            .options
222            .max_depth
223            .is_some_and(|limit| self.depth > limit)
224        {
225            self.depth -= 1;
226            return Err(MatchError::DepthLimit);
227        }
228        let ends = self.match_node_inner(node, pos);
229        self.depth -= 1;
230        ends
231    }
232
233    fn match_node_inner(
234        &mut self,
235        node: NodeId,
236        pos: usize,
237    ) -> Result<BTreeSet<usize>, MatchError> {
238        // Taken out of `self` so the grammar can be read while the memo table is written.
239        let grammar = self.grammar;
240        match grammar.node(node) {
241            Node::Alt { branches } => {
242                let mut ends = BTreeSet::new();
243                for branch in branches {
244                    ends.extend(self.match_node(*branch, pos)?);
245                }
246                Ok(ends)
247            }
248            Node::Concat { items } => {
249                let mut positions = BTreeSet::from([pos]);
250                for item in items {
251                    positions = self.advance(*item, &positions)?;
252                    if positions.is_empty() {
253                        break;
254                    }
255                }
256                Ok(positions)
257            }
258            Node::Repeat { repeat, body, .. } => self.match_repeat(*repeat, *body, pos),
259            Node::Optional { body } => {
260                // `[x]` is `x / empty`, so the start position is always an ending.
261                let mut ends = self.match_node(*body, pos)?;
262                ends.insert(pos);
263                Ok(ends)
264            }
265            Node::RuleRef { name, .. } => self.match_rule_ref(node, name.as_str(), pos),
266            Node::CharVal(value) => Ok(end_set(self.match_char_val(value, pos))),
267            Node::NumVal { value, .. } => Ok(end_set(self.match_num_val(value, pos))),
268            Node::ProseVal(_) => {
269                // Unreachable: `end_positions` refuses a start rule that can reach prose, and
270                // the reachability that decides it walks exactly the nodes this walk does.
271                debug_assert!(false, "prose reached despite the start-rule gate");
272                Ok(BTreeSet::new())
273            }
274        }
275    }
276
277    /// Every position reachable by matching `node` once from each of `from`.
278    fn advance(
279        &mut self,
280        node: NodeId,
281        from: &BTreeSet<usize>,
282    ) -> Result<BTreeSet<usize>, MatchError> {
283        let mut reached = BTreeSet::new();
284        for start in from {
285            reached.extend(self.match_node(node, *start)?);
286        }
287        Ok(reached)
288    }
289
290    /// Repetition, exactly as SCOPE.md 6.3 specifies it.
291    ///
292    /// Revision 1 of the spec got this wrong, so it is worth being explicit about the two
293    /// traps. Phase 1's early exit is *equality* of the step result, not a subset test: the
294    /// step function is deterministic, so `f(cur) == cur` means every later iteration returns
295    /// `cur` too, which is what bounds `1000000000*["a"]` to one iteration rather than a
296    /// billion. Phase 2 extends the *frontier*, not the whole result, so a position first
297    /// reached at count `c` is not re-explored at every later count.
298    fn match_repeat(
299        &mut self,
300        repeat: Repeat,
301        body: NodeId,
302        pos: usize,
303    ) -> Result<BTreeSet<usize>, MatchError> {
304        // Phase 1: positions reachable after exactly `min` repetitions. The count matters here
305        // even when the positions stop changing, so emptiness and an exact fixpoint are the
306        // only ways out early.
307        let mut current = BTreeSet::from([pos]);
308        for _ in 0..repeat.min {
309            self.step()?;
310            let next = self.advance(body, &current)?;
311            if next.is_empty() {
312                return Ok(BTreeSet::new());
313            }
314            if next == current {
315                break;
316            }
317            current = next;
318        }
319
320        // Phase 2: from exactly-min, extend by up to `max - min` more. Breadth-first by count,
321        // subtracting what is already known, so this terminates even for an unbounded maximum.
322        //
323        // The budget counts *down* from how many repetitions are still allowed rather than up
324        // from `min`. Counting up overflows on `18446744073709551615*["a"]`, which is a legal
325        // bound (D17) and reaches here with `min` at `u64::MAX`: the loop is entered once, the
326        // frontier empties, and the increment wraps — panicking in a debug build, which is the
327        // build this crate mostly runs in. Counting down also makes the shape plain: when the
328        // maximum is unbounded there is no counter, because nothing but an empty frontier can
329        // stop the loop.
330        debug_assert!(
331            repeat.max.is_none_or(|max| max >= repeat.min),
332            "`check` rejects an inverted repeat range (D18), so this cannot underflow"
333        );
334        let mut remaining = repeat.max.map(|max| max - repeat.min);
335
336        let mut result = current.clone();
337        let mut frontier = current;
338        while remaining.is_none_or(|left| left > 0) && !frontier.is_empty() {
339            self.step()?;
340            let reached = self.advance(body, &frontier)?;
341            frontier = reached.difference(&result).copied().collect();
342            result.extend(&frontier);
343            if let Some(left) = remaining.as_mut() {
344                *left -= 1;
345            }
346        }
347        Ok(result)
348    }
349
350    /// A rule reference, through the memo table.
351    fn match_rule_ref(
352        &mut self,
353        node: NodeId,
354        name: &str,
355        pos: usize,
356    ) -> Result<BTreeSet<usize>, MatchError> {
357        let grammar = self.grammar;
358        let Some(target) = grammar.target(node) else {
359            debug_assert!(false, "unresolved reference survived `check`");
360            return Ok(BTreeSet::new());
361        };
362        let key = (grammar.rule_index(target), pos);
363
364        if self.memoize {
365            match self.memo.get(&key) {
366                Some(Memo::Done(ends)) => return Ok(ends.clone()),
367                // Belt and braces: `check` rejects left recursion outright, so this cannot
368                // fire on a grammar that got this far (SCOPE.md 6.2).
369                Some(Memo::InProgress) => {
370                    return Err(MatchError::LeftRecursionDetected {
371                        rule: name.to_owned(),
372                    });
373                }
374                None => {
375                    self.memo.insert(key, Memo::InProgress);
376                }
377            }
378        }
379
380        let outcome = self
381            .step()
382            .and_then(|()| self.match_node(grammar.rule_by_id(target).body, pos));
383
384        let ends = match outcome {
385            Ok(ends) => ends,
386            Err(error) => {
387                // `InProgress` must not outlive the call that wrote it. A resource limit
388                // unwinds past the `Done` insertion below, and a later call on this same
389                // recognizer would then read the leftover marker as left recursion -- which
390                // `check` has already ruled out, making it a wrong answer rather than a
391                // limit. Every frame cleans up as the error passes through, so the table is
392                // whole again by the time the caller sees the error.
393                if self.memoize {
394                    self.memo.remove(&key);
395                }
396                return Err(error);
397            }
398        };
399
400        if self.memoize {
401            self.memo.insert(key, Memo::Done(ends.clone()));
402        }
403        Ok(ends)
404    }
405
406    /// Where a quoted string ends, if it matches at `pos`.
407    fn match_char_val(&self, value: &CharVal, pos: usize) -> Option<usize> {
408        let expected: Vec<char> = value.value.chars().collect();
409        let end = pos.checked_add(expected.len())?;
410        let actual = self.input.get(pos..end)?;
411        let matches = actual.iter().zip(&expected).all(|(got, want)| {
412            if value.case_sensitive {
413                got == want
414            } else {
415                // ASCII folding only: RFC 5234 defines no other, and guessing at Unicode case
416                // rules would make the oracle disagree with every other ABNF tool (SCOPE.md 6.1).
417                got.eq_ignore_ascii_case(want)
418            }
419        });
420        matches.then_some(end)
421    }
422
423    /// Where a numeric terminal ends, if it matches at `pos`.
424    fn match_num_val(&self, value: &NumVal, pos: usize) -> Option<usize> {
425        match value {
426            NumVal::Scalar(expected) => {
427                let found = u64::from(*self.input.get(pos)?);
428                (found == *expected).then_some(pos + 1)
429            }
430            NumVal::Range { lo, hi } => {
431                let found = u64::from(*self.input.get(pos)?);
432                (*lo <= found && found <= *hi).then_some(pos + 1)
433            }
434            NumVal::Concat(values) => {
435                let end = pos.checked_add(values.len())?;
436                let actual = self.input.get(pos..end)?;
437                let matches = actual
438                    .iter()
439                    .zip(values)
440                    .all(|(got, want)| u64::from(*got) == *want);
441                matches.then_some(end)
442            }
443        }
444    }
445
446    /// Counts one step, and stops if that exceeds the limit.
447    fn step(&mut self) -> Result<(), MatchError> {
448        self.steps += 1;
449        if self
450            .options
451            .max_steps
452            .is_some_and(|limit| self.steps > limit)
453        {
454            return Err(MatchError::StepLimit);
455        }
456        Ok(())
457    }
458}
459
460/// A terminal's result: the one end position it has, or none at all.
461fn end_set(end: Option<usize>) -> BTreeSet<usize> {
462    end.into_iter().collect()
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    use crate::ast::Grammar;
469
470    fn checked(source: &str) -> CheckedGrammar {
471        Grammar::parse(source)
472            .unwrap_or_else(|e| panic!("{source:?}: {e}"))
473            .check()
474            .unwrap_or_else(|e| panic!("{source:?}: {e:?}"))
475    }
476
477    /// Whether `input` matches `start` in the given grammar, asserted to be independent of the
478    /// memo table (invariant 4).
479    fn matches(source: &str, input: &str) -> bool {
480        let grammar = checked(source);
481        let with = Recognizer::new(&grammar, input)
482            .accepts("start")
483            .expect("recognizes");
484        let without = Recognizer::new(&grammar, input)
485            .without_memoization()
486            .accepts("start")
487            .expect("recognizes");
488        assert_eq!(
489            with, without,
490            "memoization changed the answer for {input:?}"
491        );
492        with
493    }
494
495    /// Whether `input` matches a one-rule grammar whose body is `elements`.
496    fn body_matches(elements: &str, input: &str) -> bool {
497        matches(&format!("start = {elements}\r\n"), input)
498    }
499
500    // -- one test per construct of SCOPE.md 4 ------------------------------------------------
501
502    #[test]
503    fn quoted_strings_fold_ascii_case_by_default() {
504        assert!(body_matches("\"abc\"", "abc"));
505        assert!(body_matches("\"abc\"", "ABC"));
506        assert!(body_matches("\"abc\"", "AbC"));
507        assert!(!body_matches("\"abc\"", "abd"));
508        assert!(!body_matches("\"abc\"", "ab"));
509        assert!(!body_matches("\"abc\"", "abcd"));
510    }
511
512    #[test]
513    fn case_sensitive_and_case_insensitive_strings_differ() {
514        assert!(body_matches("%s\"aBc\"", "aBc"));
515        assert!(!body_matches("%s\"aBc\"", "abc"), "%s is exact (RFC 7405)");
516        assert!(body_matches("%i\"aBc\"", "abc"));
517        assert!(body_matches("%i\"aBc\"", "ABC"));
518    }
519
520    #[test]
521    fn folding_never_reaches_beyond_ascii() {
522        // RFC 5234 defines no other folding, and inventing one would put this crate at odds
523        // with every other ABNF tool (SCOPE.md 6.1, 12 item 1). Two things enforce it: a
524        // char-val cannot contain a non-ASCII character in the first place (D35), and a
525        // numeric terminal is exact.
526        assert!(body_matches("\"i\"", "I"), "ASCII letters fold");
527        assert!(body_matches("%xE9", "\u{e9}"));
528        assert!(
529            !body_matches("%xE9", "\u{c9}"),
530            "a numeric value never folds"
531        );
532        assert!(
533            Grammar::parse("start = \"\u{e9}\"\r\n").is_err(),
534            "and a non-ASCII quoted string is not even writable"
535        );
536    }
537
538    #[test]
539    fn the_empty_string_matches_nothing_at_all() {
540        assert!(body_matches("\"\"", ""));
541        assert!(!body_matches("\"\"", "a"));
542    }
543
544    #[test]
545    fn numeric_terminals() {
546        assert!(body_matches("%x41", "A"));
547        assert!(!body_matches("%x41", "a"), "a numeric value is exact");
548        assert!(body_matches("%d65", "A"));
549        assert!(body_matches("%b1000001", "A"));
550    }
551
552    #[test]
553    fn numeric_ranges_and_concatenations() {
554        assert!(body_matches("%x41-5A", "Q"));
555        assert!(!body_matches("%x41-5A", "q"));
556        assert!(body_matches("%x41.42.43", "ABC"));
557        assert!(!body_matches("%x41.42.43", "AB"));
558    }
559
560    #[test]
561    fn a_terminal_above_the_basic_plane_matches_its_scalar() {
562        // Positions are scalar values, not bytes, so one astral character is one position.
563        assert!(body_matches("%x1D11E", "\u{1D11E}"));
564        assert!(body_matches("%x1D11E \"x\"", "\u{1D11E}x"));
565    }
566
567    #[test]
568    fn an_unrepresentable_terminal_simply_never_matches() {
569        // Not an error here: `can_recognize` refuses it as a start rule, and a rule that merely
570        // contains one is refused too. Reached through a rule that is allowed, it matches
571        // nothing, which is the honest answer (SCOPE.md 6.1).
572        let grammar = checked("start = \"a\" / other\r\nother = \"b\"\r\n");
573        let mut recognizer = Recognizer::new(&grammar, "a");
574        assert!(recognizer.accepts("start").expect("recognizes"));
575    }
576
577    #[test]
578    fn alternation_is_unordered() {
579        assert!(body_matches("\"a\" / \"ab\"", "a"));
580        assert!(
581            body_matches("\"a\" / \"ab\"", "ab"),
582            "a PEG would have committed to the first branch and rejected this"
583        );
584    }
585
586    #[test]
587    fn ambiguity_reports_every_ending() {
588        // The set-of-positions model in one assertion: `*"a" *"a"` can split three characters
589        // four ways, and all of them end where the input does.
590        let grammar = checked("start = *\"a\" *\"a\"\r\n");
591        let mut recognizer = Recognizer::new(&grammar, "aaa");
592        let ends = recognizer.end_positions("start", 0).expect("recognizes");
593        assert_eq!(ends, BTreeSet::from([0, 1, 2, 3]));
594        assert!(recognizer.accepts("start").expect("recognizes"));
595    }
596
597    #[test]
598    fn concatenation_needs_every_part() {
599        assert!(body_matches("\"a\" \"b\"", "ab"));
600        assert!(!body_matches("\"a\" \"b\"", "a"));
601        assert!(!body_matches("\"a\" \"b\"", "ba"));
602    }
603
604    #[test]
605    fn grouping_binds_tighter_than_alternation() {
606        assert!(body_matches("\"a\" (\"b\" / \"c\")", "ac"));
607        assert!(!body_matches("\"a\" (\"b\" / \"c\")", "c"));
608        assert!(
609            body_matches("\"a\" \"b\" / \"c\"", "c"),
610            "without the group, the alternation wins"
611        );
612    }
613
614    #[test]
615    fn optionals_nest() {
616        assert!(body_matches("[\"a\"]", ""));
617        assert!(body_matches("[\"a\"]", "a"));
618        assert!(body_matches("[\"a\" [\"b\"]]", ""));
619        assert!(body_matches("[\"a\" [\"b\"]]", "a"));
620        assert!(body_matches("[\"a\" [\"b\"]]", "ab"));
621        assert!(
622            !body_matches("[\"a\" [\"b\"]]", "b"),
623            "the inner one needs the outer"
624        );
625    }
626
627    #[test]
628    fn prose_is_refused_before_the_input_is_looked_at() {
629        let grammar = checked("start = <anything>\r\n");
630        let mut recognizer = Recognizer::new(&grammar, "");
631        assert!(matches!(
632            recognizer.accepts("start"),
633            Err(MatchError::ProseValueReachable { .. })
634        ));
635    }
636
637    // -- rules ---------------------------------------------------------------------------------
638
639    #[test]
640    fn rule_references_are_followed() {
641        assert!(matches(
642            "start = middle \"!\"\r\nmiddle = \"hi\"\r\n",
643            "hi!"
644        ));
645        assert!(!matches(
646            "start = middle \"!\"\r\nmiddle = \"hi\"\r\n",
647            "ho!"
648        ));
649    }
650
651    #[test]
652    fn incremental_alternatives_are_matched_as_one_rule() {
653        let source = "start = \"a\"\r\nstart =/ \"b\"\r\nstart =/ \"c\"\r\n";
654        assert!(matches(source, "a"));
655        assert!(matches(source, "b"));
656        assert!(matches(source, "c"));
657        assert!(!matches(source, "d"));
658    }
659
660    #[test]
661    fn core_rules_need_no_definition() {
662        assert!(matches("start = 1*ALPHA\r\n", "abcXYZ"));
663        assert!(!matches("start = 1*ALPHA\r\n", "abc1"));
664        assert!(matches("start = 1*DIGIT\r\n", "2026"));
665        assert!(matches("start = CRLF\r\n", "\r\n"));
666        assert!(
667            matches("start = 1*HEXDIG\r\n", "dEaDbEeF"),
668            "HEXDIG folds case"
669        );
670    }
671
672    #[test]
673    fn a_user_rule_shadows_a_core_rule_for_user_references_only() {
674        // D33, as behaviour rather than as a resolution table: the user's DIGIT changes what
675        // their reference means, and core HEXDIG carries on reaching the core DIGIT.
676        let grammar = checked("start = DIGIT\r\nhex = HEXDIG\r\nDIGIT = \"x\"\r\n");
677
678        assert!(
679            Recognizer::new(&grammar, "x")
680                .accepts("start")
681                .expect("recognizes")
682        );
683        assert!(
684            !Recognizer::new(&grammar, "7")
685                .accepts("start")
686                .expect("recognizes")
687        );
688        assert!(
689            Recognizer::new(&grammar, "7")
690                .accepts("hex")
691                .expect("recognizes"),
692            "core HEXDIG still reaches core DIGIT"
693        );
694        assert!(
695            !Recognizer::new(&grammar, "x")
696                .accepts("hex")
697                .expect("recognizes")
698        );
699    }
700
701    #[test]
702    fn recursion_through_a_consuming_prefix_terminates() {
703        let source = "start = \"(\" start \")\" / \"x\"\r\n";
704        assert!(matches(source, "x"));
705        assert!(matches(source, "(x)"));
706        assert!(matches(source, "(((x)))"));
707        assert!(!matches(source, "((x)"));
708    }
709
710    #[test]
711    fn an_unknown_rule_is_an_error() {
712        let grammar = checked("start = \"a\"\r\n");
713        assert!(matches!(
714            Recognizer::new(&grammar, "a").accepts("nope"),
715            Err(MatchError::UnknownRule(name)) if name == "nope"
716        ));
717    }
718
719    #[test]
720    fn end_positions_can_start_anywhere() {
721        let grammar = checked("start = \"b\"\r\n");
722        let mut recognizer = Recognizer::new(&grammar, "ab");
723        assert_eq!(
724            recognizer.end_positions("start", 1).expect("recognizes"),
725            BTreeSet::from([2])
726        );
727        assert!(
728            recognizer
729                .end_positions("start", 0)
730                .expect("recognizes")
731                .is_empty()
732        );
733    }
734
735    // -- the depth limit ---------------------------------------------------------------------
736
737    #[test]
738    fn ordinary_nesting_is_well_within_the_default() {
739        // The default has to be generous enough that normal input never meets it.
740        let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
741        let input = format!("{}x{}", "(".repeat(30), ")".repeat(30));
742        assert!(
743            Recognizer::new(&grammar, &input)
744                .accepts("start")
745                .expect("recognizes")
746        );
747    }
748
749    #[test]
750    fn deeper_than_the_default_is_an_error_not_a_rejection() {
751        // The whole point of the limit: an answer the caller can act on, rather than a stack
752        // overflow, which aborts the process and cannot be caught.
753        let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
754        let input = format!("{}x{}", "(".repeat(5_000), ")".repeat(5_000));
755        assert!(matches!(
756            Recognizer::new(&grammar, &input).accepts("start"),
757            Err(MatchError::DepthLimit)
758        ));
759    }
760
761    #[test]
762    fn the_limit_can_be_raised_or_removed() {
763        let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
764        let input = format!("{}x{}", "(".repeat(200), ")".repeat(200));
765
766        assert!(matches!(
767            Recognizer::new(&grammar, &input)
768                .with_options(MatchOptions {
769                    max_depth: Some(16),
770                    ..MatchOptions::default()
771                })
772                .accepts("start"),
773            Err(MatchError::DepthLimit)
774        ));
775        assert!(
776            Recognizer::new(&grammar, &input)
777                .with_options(MatchOptions {
778                    max_depth: Some(2_000),
779                    ..MatchOptions::default()
780                })
781                .accepts("start")
782                .expect("recognizes")
783        );
784        assert!(
785            Recognizer::new(&grammar, &input)
786                .with_options(MatchOptions {
787                    max_depth: None,
788                    ..MatchOptions::default()
789                })
790                .accepts("start")
791                .expect("recognizes"),
792            "None removes the limit"
793        );
794    }
795
796    #[test]
797    fn length_alone_costs_no_depth() {
798        // Depth tracks nesting, not size: a flat input of any length stays shallow, which is
799        // what makes a low default workable.
800        let grammar = checked("start = *\"a\"\r\n");
801        let input = "a".repeat(100_000);
802        assert!(
803            Recognizer::new(&grammar, &input)
804                .accepts("start")
805                .expect("recognizes")
806        );
807    }
808
809    #[test]
810    fn the_depth_limit_does_not_leak_between_calls() {
811        // The counter is decremented on the way back up, including on the error path, so a
812        // recognizer that hit the limit once still works afterwards.
813        //
814        // Asserted on the *same* recognizer, which is the point: an earlier version of this
815        // test built a second one on different input, and so checked nothing about reuse.
816        let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
817        // Four is enough to decide "x))" and not enough for "((x))": alternation is unordered,
818        // so the deep branch is explored even when the shallow one would succeed.
819        let mut recognizer = Recognizer::new(&grammar, "((x))").with_options(MatchOptions {
820            max_depth: Some(4),
821            ..MatchOptions::default()
822        });
823        assert!(matches!(
824            recognizer.accepts("start"),
825            Err(MatchError::DepthLimit)
826        ));
827
828        // Asking the same question again must give the same honest answer. Anything else --
829        // `LeftRecursionDetected` in particular -- would be a verdict invented by leftover
830        // state, on a grammar `check` has already proved is not left-recursive.
831        match recognizer.accepts("start") {
832            Err(MatchError::DepthLimit) => {}
833            other => panic!("a repeated question should get a repeated answer, got {other:?}"),
834        }
835
836        // And the recognizer is still usable for work it *can* do: from position 2 the input
837        // is "x))", which needs almost no depth. This is the key the failed descent left
838        // half-written, so it only decides if the table was repaired on the way out.
839        let ends = recognizer
840            .end_positions("start", 2)
841            .expect("usable after a depth limit");
842        assert!(
843            ends.contains(&3),
844            "expected to match \"x\" at 2, got {ends:?}"
845        );
846    }
847
848    #[test]
849    fn a_step_limit_does_not_poison_the_memo() {
850        // `match_rule_ref` writes `Memo::InProgress` before descending. If a limit unwinds past
851        // the `Done` insertion, that marker outlives the call, and the next one reads it as
852        // left recursion -- a wrong answer rather than a limit.
853        let grammar = checked("start = a a a\r\na = b b\r\nb = \"x\" / \"y\"\r\n");
854        let mut recognizer = Recognizer::new(&grammar, "xxxxxx").with_options(MatchOptions {
855            max_steps: Some(3),
856            ..MatchOptions::default()
857        });
858
859        assert!(matches!(
860            recognizer.accepts("start"),
861            Err(MatchError::StepLimit)
862        ));
863
864        // The step budget is spent and is not reset per call, so refusing again is correct.
865        // Refusing *for the wrong reason* is the bug.
866        match recognizer.accepts("start") {
867            Err(MatchError::StepLimit) => {}
868            other => panic!("expected the spent budget to persist, got {other:?}"),
869        }
870    }
871
872    #[test]
873    fn a_limit_deep_in_the_grammar_still_leaves_the_memo_whole() {
874        // Not only the outermost rule: the limit can trip several frames down, and every frame
875        // has to clean up as the error passes through it. Sweeping the budget walks the failure
876        // point through each level of the chain in turn.
877        let grammar = checked("start = a\r\na = b\r\nb = c\r\nc = d\r\nd = \"x\" / \"y\"\r\n");
878
879        for budget in 1..=8 {
880            let mut recognizer = Recognizer::new(&grammar, "x").with_options(MatchOptions {
881                max_steps: Some(budget),
882                ..MatchOptions::default()
883            });
884            let first = recognizer.accepts("start");
885            let second = recognizer.accepts("start");
886
887            // Whatever the first call decided, the second must agree with it. The two can only
888            // differ if the first left something behind.
889            assert_eq!(
890                first, second,
891                "budget {budget}: the answer changed when the question was repeated"
892            );
893            assert!(
894                !matches!(second, Err(MatchError::LeftRecursionDetected { .. })),
895                "budget {budget}: invented left recursion in a grammar that has none"
896            );
897        }
898    }
899
900    #[test]
901    fn memoization_changes_the_work_and_not_the_answer() {
902        // A grammar that revisits the same rule at the same position many times.
903        let source = "start = a a a a\r\na = [\"x\"] [\"x\"] [\"x\"]\r\n";
904        let grammar = checked(source);
905
906        let mut memoized = Recognizer::new(&grammar, "xxxxxx");
907        let with = memoized.accepts("start").expect("recognizes");
908
909        let mut plain = Recognizer::new(&grammar, "xxxxxx").without_memoization();
910        let without = plain.accepts("start").expect("recognizes");
911
912        assert_eq!(with, without);
913        assert!(
914            memoized.steps() < plain.steps(),
915            "memoized {} steps, plain {}",
916            memoized.steps(),
917            plain.steps()
918        );
919    }
920}