1use std::collections::{BTreeSet, HashMap};
17
18use crate::ast::{CharVal, Node, NodeId, NumVal, Repeat};
19use crate::check::CheckedGrammar;
20use crate::error::MatchError;
21
22pub const DEFAULT_MAX_DEPTH: usize = 256;
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct MatchOptions {
31 pub max_steps: Option<u64>,
36
37 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
100enum Memo {
102 InProgress,
104 Done(BTreeSet<usize>),
106}
107
108pub struct Recognizer<'g, 'i> {
113 grammar: &'g CheckedGrammar,
114 src: &'i str,
115 input: Vec<char>,
118 memo: HashMap<(usize, usize), Memo>,
119 steps: u64,
120 depth: usize,
122 options: MatchOptions,
123 memoize: bool,
124}
125
126impl<'g, 'i> Recognizer<'g, 'i> {
127 #[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 #[must_use]
144 pub fn with_options(mut self, options: MatchOptions) -> Self {
145 self.options = options;
146 self
147 }
148
149 #[must_use]
151 pub fn input(&self) -> &'i str {
152 self.src
153 }
154
155 #[must_use]
166 pub const fn steps(&self) -> u64 {
167 self.steps
168 }
169
170 #[doc(hidden)]
173 #[must_use]
174 pub fn without_memoization(mut self) -> Self {
175 self.memoize = false;
176 self
177 }
178
179 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 pub fn end_positions(
199 &mut self,
200 rule: &str,
201 start: usize,
202 ) -> Result<BTreeSet<usize>, MatchError> {
203 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 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 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 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 debug_assert!(false, "prose reached despite the start-rule gate");
272 Ok(BTreeSet::new())
273 }
274 }
275 }
276
277 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 fn match_repeat(
299 &mut self,
300 repeat: Repeat,
301 body: NodeId,
302 pos: usize,
303 ) -> Result<BTreeSet<usize>, MatchError> {
304 let mut current = BTreeSet::from([pos]);
308 for _ in 0..repeat.min {
309 self.step()?;
310 let next = self.advance(body, ¤t)?;
311 if next.is_empty() {
312 return Ok(BTreeSet::new());
313 }
314 if next == current {
315 break;
316 }
317 current = next;
318 }
319
320 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 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 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 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 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 got.eq_ignore_ascii_case(want)
418 }
419 });
420 matches.then_some(end)
421 }
422
423 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 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
460fn 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 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 fn body_matches(elements: &str, input: &str) -> bool {
497 matches(&format!("start = {elements}\r\n"), input)
498 }
499
500 #[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 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 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 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 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 #[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 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 #[test]
738 fn ordinary_nesting_is_well_within_the_default() {
739 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 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 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 let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
817 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 match recognizer.accepts("start") {
832 Err(MatchError::DepthLimit) => {}
833 other => panic!("a repeated question should get a repeated answer, got {other:?}"),
834 }
835
836 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 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 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 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 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 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}