use std::collections::{BTreeSet, HashMap};
use crate::ast::{CharVal, Node, NodeId, NumVal, Repeat};
use crate::check::CheckedGrammar;
use crate::error::MatchError;
pub const DEFAULT_MAX_DEPTH: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MatchOptions {
pub max_steps: Option<u64>,
pub max_depth: Option<usize>,
}
impl Default for MatchOptions {
fn default() -> Self {
Self {
max_steps: None,
max_depth: Some(DEFAULT_MAX_DEPTH),
}
}
}
enum Memo {
InProgress,
Done(BTreeSet<usize>),
}
pub struct Recognizer<'g, 'i> {
grammar: &'g CheckedGrammar,
src: &'i str,
input: Vec<char>,
memo: HashMap<(usize, usize), Memo>,
steps: u64,
depth: usize,
options: MatchOptions,
memoize: bool,
}
impl<'g, 'i> Recognizer<'g, 'i> {
#[must_use]
pub fn new(grammar: &'g CheckedGrammar, input: &'i str) -> Self {
Self {
grammar,
src: input,
input: input.chars().collect(),
memo: HashMap::new(),
steps: 0,
depth: 0,
options: MatchOptions::default(),
memoize: true,
}
}
#[must_use]
pub fn with_options(mut self, options: MatchOptions) -> Self {
self.options = options;
self
}
#[must_use]
pub fn input(&self) -> &'i str {
self.src
}
#[must_use]
pub const fn steps(&self) -> u64 {
self.steps
}
#[doc(hidden)]
#[must_use]
pub fn without_memoization(mut self) -> Self {
self.memoize = false;
self
}
pub fn accepts(&mut self, rule: &str) -> Result<bool, MatchError> {
let ends = self.end_positions(rule, 0)?;
Ok(ends.contains(&self.input.len()))
}
pub fn end_positions(
&mut self,
rule: &str,
start: usize,
) -> Result<BTreeSet<usize>, MatchError> {
self.grammar.can_recognize(rule)?;
let body = self
.grammar
.rule(rule)
.ok_or_else(|| MatchError::UnknownRule(rule.to_owned()))?
.body;
self.match_node(body, start)
}
fn match_node(&mut self, node: NodeId, pos: usize) -> Result<BTreeSet<usize>, MatchError> {
self.depth += 1;
if self
.options
.max_depth
.is_some_and(|limit| self.depth > limit)
{
self.depth -= 1;
return Err(MatchError::DepthLimit);
}
let ends = self.match_node_inner(node, pos);
self.depth -= 1;
ends
}
fn match_node_inner(
&mut self,
node: NodeId,
pos: usize,
) -> Result<BTreeSet<usize>, MatchError> {
let grammar = self.grammar;
match grammar.node(node) {
Node::Alt { branches } => {
let mut ends = BTreeSet::new();
for branch in branches {
ends.extend(self.match_node(*branch, pos)?);
}
Ok(ends)
}
Node::Concat { items } => {
let mut positions = BTreeSet::from([pos]);
for item in items {
positions = self.advance(*item, &positions)?;
if positions.is_empty() {
break;
}
}
Ok(positions)
}
Node::Repeat { repeat, body, .. } => self.match_repeat(*repeat, *body, pos),
Node::Optional { body } => {
let mut ends = self.match_node(*body, pos)?;
ends.insert(pos);
Ok(ends)
}
Node::RuleRef { name, .. } => self.match_rule_ref(node, name.as_str(), pos),
Node::CharVal(value) => Ok(end_set(self.match_char_val(value, pos))),
Node::NumVal { value, .. } => Ok(end_set(self.match_num_val(value, pos))),
Node::ProseVal(_) => {
debug_assert!(false, "prose reached despite the start-rule gate");
Ok(BTreeSet::new())
}
}
}
fn advance(
&mut self,
node: NodeId,
from: &BTreeSet<usize>,
) -> Result<BTreeSet<usize>, MatchError> {
let mut reached = BTreeSet::new();
for start in from {
reached.extend(self.match_node(node, *start)?);
}
Ok(reached)
}
fn match_repeat(
&mut self,
repeat: Repeat,
body: NodeId,
pos: usize,
) -> Result<BTreeSet<usize>, MatchError> {
let mut current = BTreeSet::from([pos]);
for _ in 0..repeat.min {
self.step()?;
let next = self.advance(body, ¤t)?;
if next.is_empty() {
return Ok(BTreeSet::new());
}
if next == current {
break;
}
current = next;
}
let mut result = current.clone();
let mut frontier = current;
let mut count = repeat.min;
while repeat.max.is_none_or(|max| count < max) && !frontier.is_empty() {
self.step()?;
let reached = self.advance(body, &frontier)?;
frontier = reached.difference(&result).copied().collect();
result.extend(&frontier);
count += 1;
}
Ok(result)
}
fn match_rule_ref(
&mut self,
node: NodeId,
name: &str,
pos: usize,
) -> Result<BTreeSet<usize>, MatchError> {
let grammar = self.grammar;
let Some(target) = grammar.target(node) else {
debug_assert!(false, "unresolved reference survived `check`");
return Ok(BTreeSet::new());
};
let key = (grammar.rule_index(target), pos);
if self.memoize {
match self.memo.get(&key) {
Some(Memo::Done(ends)) => return Ok(ends.clone()),
Some(Memo::InProgress) => {
return Err(MatchError::LeftRecursionDetected {
rule: name.to_owned(),
});
}
None => {
self.memo.insert(key, Memo::InProgress);
}
}
}
self.step()?;
let ends = self.match_node(grammar.rule_by_id(target).body, pos)?;
if self.memoize {
self.memo.insert(key, Memo::Done(ends.clone()));
}
Ok(ends)
}
fn match_char_val(&self, value: &CharVal, pos: usize) -> Option<usize> {
let expected: Vec<char> = value.value.chars().collect();
let end = pos.checked_add(expected.len())?;
let actual = self.input.get(pos..end)?;
let matches = actual.iter().zip(&expected).all(|(got, want)| {
if value.case_sensitive {
got == want
} else {
got.eq_ignore_ascii_case(want)
}
});
matches.then_some(end)
}
fn match_num_val(&self, value: &NumVal, pos: usize) -> Option<usize> {
match value {
NumVal::Scalar(expected) => {
let found = u64::from(*self.input.get(pos)?);
(found == *expected).then_some(pos + 1)
}
NumVal::Range { lo, hi } => {
let found = u64::from(*self.input.get(pos)?);
(*lo <= found && found <= *hi).then_some(pos + 1)
}
NumVal::Concat(values) => {
let end = pos.checked_add(values.len())?;
let actual = self.input.get(pos..end)?;
let matches = actual
.iter()
.zip(values)
.all(|(got, want)| u64::from(*got) == *want);
matches.then_some(end)
}
}
}
fn step(&mut self) -> Result<(), MatchError> {
self.steps += 1;
if self
.options
.max_steps
.is_some_and(|limit| self.steps > limit)
{
return Err(MatchError::StepLimit);
}
Ok(())
}
}
fn end_set(end: Option<usize>) -> BTreeSet<usize> {
end.into_iter().collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Grammar;
fn checked(source: &str) -> CheckedGrammar {
Grammar::parse(source)
.unwrap_or_else(|e| panic!("{source:?}: {e}"))
.check()
.unwrap_or_else(|e| panic!("{source:?}: {e:?}"))
}
fn matches(source: &str, input: &str) -> bool {
let grammar = checked(source);
let with = Recognizer::new(&grammar, input)
.accepts("start")
.expect("recognizes");
let without = Recognizer::new(&grammar, input)
.without_memoization()
.accepts("start")
.expect("recognizes");
assert_eq!(
with, without,
"memoization changed the answer for {input:?}"
);
with
}
fn body_matches(elements: &str, input: &str) -> bool {
matches(&format!("start = {elements}\r\n"), input)
}
#[test]
fn quoted_strings_fold_ascii_case_by_default() {
assert!(body_matches("\"abc\"", "abc"));
assert!(body_matches("\"abc\"", "ABC"));
assert!(body_matches("\"abc\"", "AbC"));
assert!(!body_matches("\"abc\"", "abd"));
assert!(!body_matches("\"abc\"", "ab"));
assert!(!body_matches("\"abc\"", "abcd"));
}
#[test]
fn case_sensitive_and_case_insensitive_strings_differ() {
assert!(body_matches("%s\"aBc\"", "aBc"));
assert!(!body_matches("%s\"aBc\"", "abc"), "%s is exact (RFC 7405)");
assert!(body_matches("%i\"aBc\"", "abc"));
assert!(body_matches("%i\"aBc\"", "ABC"));
}
#[test]
fn folding_never_reaches_beyond_ascii() {
assert!(body_matches("\"i\"", "I"), "ASCII letters fold");
assert!(body_matches("%xE9", "\u{e9}"));
assert!(
!body_matches("%xE9", "\u{c9}"),
"a numeric value never folds"
);
assert!(
Grammar::parse("start = \"\u{e9}\"\r\n").is_err(),
"and a non-ASCII quoted string is not even writable"
);
}
#[test]
fn the_empty_string_matches_nothing_at_all() {
assert!(body_matches("\"\"", ""));
assert!(!body_matches("\"\"", "a"));
}
#[test]
fn numeric_terminals() {
assert!(body_matches("%x41", "A"));
assert!(!body_matches("%x41", "a"), "a numeric value is exact");
assert!(body_matches("%d65", "A"));
assert!(body_matches("%b1000001", "A"));
}
#[test]
fn numeric_ranges_and_concatenations() {
assert!(body_matches("%x41-5A", "Q"));
assert!(!body_matches("%x41-5A", "q"));
assert!(body_matches("%x41.42.43", "ABC"));
assert!(!body_matches("%x41.42.43", "AB"));
}
#[test]
fn a_terminal_above_the_basic_plane_matches_its_scalar() {
assert!(body_matches("%x1D11E", "\u{1D11E}"));
assert!(body_matches("%x1D11E \"x\"", "\u{1D11E}x"));
}
#[test]
fn an_unrepresentable_terminal_simply_never_matches() {
let grammar = checked("start = \"a\" / other\r\nother = \"b\"\r\n");
let mut recognizer = Recognizer::new(&grammar, "a");
assert!(recognizer.accepts("start").expect("recognizes"));
}
#[test]
fn alternation_is_unordered() {
assert!(body_matches("\"a\" / \"ab\"", "a"));
assert!(
body_matches("\"a\" / \"ab\"", "ab"),
"a PEG would have committed to the first branch and rejected this"
);
}
#[test]
fn ambiguity_reports_every_ending() {
let grammar = checked("start = *\"a\" *\"a\"\r\n");
let mut recognizer = Recognizer::new(&grammar, "aaa");
let ends = recognizer.end_positions("start", 0).expect("recognizes");
assert_eq!(ends, BTreeSet::from([0, 1, 2, 3]));
assert!(recognizer.accepts("start").expect("recognizes"));
}
#[test]
fn concatenation_needs_every_part() {
assert!(body_matches("\"a\" \"b\"", "ab"));
assert!(!body_matches("\"a\" \"b\"", "a"));
assert!(!body_matches("\"a\" \"b\"", "ba"));
}
#[test]
fn grouping_binds_tighter_than_alternation() {
assert!(body_matches("\"a\" (\"b\" / \"c\")", "ac"));
assert!(!body_matches("\"a\" (\"b\" / \"c\")", "c"));
assert!(
body_matches("\"a\" \"b\" / \"c\"", "c"),
"without the group, the alternation wins"
);
}
#[test]
fn optionals_nest() {
assert!(body_matches("[\"a\"]", ""));
assert!(body_matches("[\"a\"]", "a"));
assert!(body_matches("[\"a\" [\"b\"]]", ""));
assert!(body_matches("[\"a\" [\"b\"]]", "a"));
assert!(body_matches("[\"a\" [\"b\"]]", "ab"));
assert!(
!body_matches("[\"a\" [\"b\"]]", "b"),
"the inner one needs the outer"
);
}
#[test]
fn prose_is_refused_before_the_input_is_looked_at() {
let grammar = checked("start = <anything>\r\n");
let mut recognizer = Recognizer::new(&grammar, "");
assert!(matches!(
recognizer.accepts("start"),
Err(MatchError::ProseValueReachable { .. })
));
}
#[test]
fn rule_references_are_followed() {
assert!(matches(
"start = middle \"!\"\r\nmiddle = \"hi\"\r\n",
"hi!"
));
assert!(!matches(
"start = middle \"!\"\r\nmiddle = \"hi\"\r\n",
"ho!"
));
}
#[test]
fn incremental_alternatives_are_matched_as_one_rule() {
let source = "start = \"a\"\r\nstart =/ \"b\"\r\nstart =/ \"c\"\r\n";
assert!(matches(source, "a"));
assert!(matches(source, "b"));
assert!(matches(source, "c"));
assert!(!matches(source, "d"));
}
#[test]
fn core_rules_need_no_definition() {
assert!(matches("start = 1*ALPHA\r\n", "abcXYZ"));
assert!(!matches("start = 1*ALPHA\r\n", "abc1"));
assert!(matches("start = 1*DIGIT\r\n", "2026"));
assert!(matches("start = CRLF\r\n", "\r\n"));
assert!(
matches("start = 1*HEXDIG\r\n", "dEaDbEeF"),
"HEXDIG folds case"
);
}
#[test]
fn a_user_rule_shadows_a_core_rule_for_user_references_only() {
let grammar = checked("start = DIGIT\r\nhex = HEXDIG\r\nDIGIT = \"x\"\r\n");
assert!(
Recognizer::new(&grammar, "x")
.accepts("start")
.expect("recognizes")
);
assert!(
!Recognizer::new(&grammar, "7")
.accepts("start")
.expect("recognizes")
);
assert!(
Recognizer::new(&grammar, "7")
.accepts("hex")
.expect("recognizes"),
"core HEXDIG still reaches core DIGIT"
);
assert!(
!Recognizer::new(&grammar, "x")
.accepts("hex")
.expect("recognizes")
);
}
#[test]
fn recursion_through_a_consuming_prefix_terminates() {
let source = "start = \"(\" start \")\" / \"x\"\r\n";
assert!(matches(source, "x"));
assert!(matches(source, "(x)"));
assert!(matches(source, "(((x)))"));
assert!(!matches(source, "((x)"));
}
#[test]
fn an_unknown_rule_is_an_error() {
let grammar = checked("start = \"a\"\r\n");
assert!(matches!(
Recognizer::new(&grammar, "a").accepts("nope"),
Err(MatchError::UnknownRule(name)) if name == "nope"
));
}
#[test]
fn end_positions_can_start_anywhere() {
let grammar = checked("start = \"b\"\r\n");
let mut recognizer = Recognizer::new(&grammar, "ab");
assert_eq!(
recognizer.end_positions("start", 1).expect("recognizes"),
BTreeSet::from([2])
);
assert!(
recognizer
.end_positions("start", 0)
.expect("recognizes")
.is_empty()
);
}
#[test]
fn ordinary_nesting_is_well_within_the_default() {
let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
let input = format!("{}x{}", "(".repeat(30), ")".repeat(30));
assert!(
Recognizer::new(&grammar, &input)
.accepts("start")
.expect("recognizes")
);
}
#[test]
fn deeper_than_the_default_is_an_error_not_a_rejection() {
let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
let input = format!("{}x{}", "(".repeat(5_000), ")".repeat(5_000));
assert!(matches!(
Recognizer::new(&grammar, &input).accepts("start"),
Err(MatchError::DepthLimit)
));
}
#[test]
fn the_limit_can_be_raised_or_removed() {
let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
let input = format!("{}x{}", "(".repeat(200), ")".repeat(200));
assert!(matches!(
Recognizer::new(&grammar, &input)
.with_options(MatchOptions {
max_depth: Some(16),
..MatchOptions::default()
})
.accepts("start"),
Err(MatchError::DepthLimit)
));
assert!(
Recognizer::new(&grammar, &input)
.with_options(MatchOptions {
max_depth: Some(2_000),
..MatchOptions::default()
})
.accepts("start")
.expect("recognizes")
);
assert!(
Recognizer::new(&grammar, &input)
.with_options(MatchOptions {
max_depth: None,
..MatchOptions::default()
})
.accepts("start")
.expect("recognizes"),
"None removes the limit"
);
}
#[test]
fn length_alone_costs_no_depth() {
let grammar = checked("start = *\"a\"\r\n");
let input = "a".repeat(100_000);
assert!(
Recognizer::new(&grammar, &input)
.accepts("start")
.expect("recognizes")
);
}
#[test]
fn the_depth_limit_does_not_leak_between_calls() {
let grammar = checked("start = \"(\" start \")\" / \"x\"\r\n");
let deep = format!("{}x{}", "(".repeat(5_000), ")".repeat(5_000));
let mut recognizer = Recognizer::new(&grammar, &deep);
assert!(matches!(
recognizer.accepts("start"),
Err(MatchError::DepthLimit)
));
let mut shallow = Recognizer::new(&grammar, "((x))");
assert!(shallow.accepts("start").expect("recognizes"));
}
#[test]
fn memoization_changes_the_work_and_not_the_answer() {
let source = "start = a a a a\r\na = [\"x\"] [\"x\"] [\"x\"]\r\n";
let grammar = checked(source);
let mut memoized = Recognizer::new(&grammar, "xxxxxx");
let with = memoized.accepts("start").expect("recognizes");
let mut plain = Recognizer::new(&grammar, "xxxxxx").without_memoization();
let without = plain.accepts("start").expect("recognizes");
assert_eq!(with, without);
assert!(
memoized.steps() < plain.steps(),
"memoized {} steps, plain {}",
memoized.steps(),
plain.steps()
);
}
}