#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as HashMap;
use alloc::string::{String, ToString};
use super::ast::{Grammar, Term};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchResult {
Match {
end_pos: usize,
},
NoMatch,
}
#[derive(Debug)]
pub struct Interpreter<'g, 'i> {
grammar: &'g Grammar,
input: &'i str,
cache: HashMap<(String, usize), MatchResult>,
depth: usize,
}
const MAX_RECURSION_DEPTH: usize = 256;
impl<'g, 'i> Interpreter<'g, 'i> {
#[must_use]
pub fn new(grammar: &'g Grammar, input: &'i str) -> Self {
Self {
grammar,
input,
cache: HashMap::new(),
depth: 0,
}
}
#[must_use]
pub fn input(&self) -> &'i str {
self.input
}
pub fn match_production(&mut self, name: &str, pos: usize) -> MatchResult {
let key = (name.to_string(), pos);
if let Some(cached) = self.cache.get(&key) {
return *cached;
}
self.cache.insert(key.clone(), MatchResult::NoMatch);
let rhs = match self.grammar.lookup(name) {
Some(p) => p.rhs.clone(),
None => return MatchResult::NoMatch,
};
let result = self.match_term(&rhs, pos);
self.cache.insert(key, result);
result
}
pub fn match_term(&mut self, term: &Term, pos: usize) -> MatchResult {
if self.depth >= MAX_RECURSION_DEPTH {
return MatchResult::NoMatch;
}
self.depth += 1;
let result = self.match_term_inner(term, pos);
self.depth -= 1;
result
}
fn match_term_inner(&mut self, term: &Term, pos: usize) -> MatchResult {
match term {
Term::Literal(s) => {
if pos <= self.input.len() && self.input[pos..].starts_with(s.as_str()) {
MatchResult::Match {
end_pos: pos + s.len(),
}
} else {
MatchResult::NoMatch
}
}
Term::NonTerminal(name) => self.match_production(name, pos),
Term::CharClass(ranges) => match_char_class(self.input, pos, ranges),
Term::Sequence(items) => {
let mut current = pos;
for item in items {
match self.match_term(item, current) {
MatchResult::Match { end_pos } => current = end_pos,
MatchResult::NoMatch => return MatchResult::NoMatch,
}
}
MatchResult::Match { end_pos: current }
}
Term::Alternation(branches) => {
let mut best: Option<usize> = None;
for branch in branches {
if let MatchResult::Match { end_pos } = self.match_term(branch, pos)
&& best.is_none_or(|b| end_pos > b)
{
best = Some(end_pos);
}
}
match best {
Some(end_pos) => MatchResult::Match { end_pos },
None => MatchResult::NoMatch,
}
}
Term::Optional(inner) => match self.match_term(inner, pos) {
MatchResult::Match { end_pos } => MatchResult::Match { end_pos },
MatchResult::NoMatch => MatchResult::Match { end_pos: pos },
},
Term::ZeroOrMore(inner) => {
let mut current = pos;
loop {
match self.match_term(inner, current) {
MatchResult::Match { end_pos } if end_pos > current => current = end_pos,
_ => break,
}
}
MatchResult::Match { end_pos: current }
}
Term::OneOrMore(inner) => match self.match_term(inner, pos) {
MatchResult::Match { end_pos: first_end } => {
let mut current = first_end;
loop {
match self.match_term(inner, current) {
MatchResult::Match { end_pos } if end_pos > current => {
current = end_pos
}
_ => break,
}
}
MatchResult::Match { end_pos: current }
}
MatchResult::NoMatch => MatchResult::NoMatch,
},
Term::Subtraction(a, b) => {
let a_result = self.match_term(a, pos);
if let MatchResult::Match { end_pos: a_end } = a_result {
if let MatchResult::Match { end_pos: b_end } = self.match_term(b, pos)
&& b_end == a_end
{
return MatchResult::NoMatch;
}
MatchResult::Match { end_pos: a_end }
} else {
MatchResult::NoMatch
}
}
}
}
}
fn match_char_class(input: &str, pos: usize, ranges: &[super::ast::CodePointRange]) -> MatchResult {
if pos > input.len() {
return MatchResult::NoMatch;
}
if let Some(c) = input[pos..].chars().next() {
let cp = c as u32;
for r in ranges {
if r.contains(cp) {
return MatchResult::Match {
end_pos: pos + c.len_utf8(),
};
}
}
}
MatchResult::NoMatch
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml_grammar::load_grammar;
fn matches_completely(result: MatchResult, expected_end: usize) -> bool {
matches!(result, MatchResult::Match { end_pos } if end_pos == expected_end)
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_literal() {
let grammar = Grammar::new();
let t = Term::Literal("<!ELEMENT".to_string());
let mut ok = Interpreter::new(&grammar, "<!ELEMENT doc");
assert!(matches_completely(ok.match_term(&t, 0), "<!ELEMENT".len()));
let mut not_ok = Interpreter::new(&grammar, "<doc/>");
assert!(matches!(not_ok.match_term(&t, 0), MatchResult::NoMatch));
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_char_class_from_loaded_char_production() {
let spec = "<prod id=\"NT-Char\" num=\"2\">\
<lhs>Char</lhs>\
<rhs>#x9 | #xA | #xD | [#x20-#xD7FF]</rhs>\
</prod>";
let grammar = load_grammar(spec).unwrap();
let mut a = Interpreter::new(&grammar, "A");
assert!(matches_completely(a.match_production("Char", 0), 1));
let mut tab = Interpreter::new(&grammar, "\t");
assert!(matches_completely(tab.match_production("Char", 0), 1));
let mut nul = Interpreter::new(&grammar, "\0");
assert!(matches!(
nul.match_production("Char", 0),
MatchResult::NoMatch
));
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_sequence_via_real_name_production() {
let spec = "
<prod id=\"NT-NameStartChar\" num=\"4\">
<lhs>NameStartChar</lhs>
<rhs>[A-Z] | \"_\" | [a-z]</rhs>
</prod>
<prod id=\"NT-NameChar\" num=\"4a\">
<lhs>NameChar</lhs>
<rhs><nt def=\"NT-NameStartChar\">NameStartChar</nt> | \"-\" | [0-9]</rhs>
</prod>
<prod id=\"NT-Name\" num=\"5\">
<lhs>Name</lhs>
<rhs><nt def=\"NT-NameStartChar\">NameStartChar</nt> (<nt def=\"NT-NameChar\">NameChar</nt>)*</rhs>
</prod>
";
let grammar = load_grammar(spec).unwrap();
let mut foo = Interpreter::new(&grammar, "foo-bar123");
assert!(matches_completely(
foo.match_production("Name", 0),
"foo-bar123".len()
));
let mut underscore = Interpreter::new(&grammar, "_");
assert!(matches_completely(
underscore.match_production("Name", 0),
1
));
let mut digit = Interpreter::new(&grammar, "1abc");
assert!(matches!(
digit.match_production("Name", 0),
MatchResult::NoMatch
));
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_alternation_longest_branch_wins() {
let spec = "
<prod id=\"NT-X\" num=\"99\">
<lhs>X</lhs>
<rhs>\"abc\" | \"abcdef\"</rhs>
</prod>
";
let grammar = load_grammar(spec).unwrap();
let mut interp = Interpreter::new(&grammar, "abcdef");
assert!(matches_completely(interp.match_production("X", 0), 6));
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_alternation_leftmost_wins_on_tie() {
let spec = "
<prod id=\"NT-X\" num=\"99\">
<lhs>X</lhs>
<rhs>\"ab\" | \"ab\"</rhs>
</prod>
";
let grammar = load_grammar(spec).unwrap();
let mut interp = Interpreter::new(&grammar, "ab");
assert!(matches_completely(interp.match_production("X", 0), 2));
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_optional_and_kleene() {
let spec = "
<prod id=\"NT-A\" num=\"1\">
<lhs>A</lhs>
<rhs>\"a\"? \"b\"*</rhs>
</prod>
";
let grammar = load_grammar(spec).unwrap();
let mut empty = Interpreter::new(&grammar, "");
assert!(matches_completely(empty.match_production("A", 0), 0));
let mut single_a = Interpreter::new(&grammar, "a");
assert!(matches_completely(single_a.match_production("A", 0), 1));
let mut abbb = Interpreter::new(&grammar, "abbb");
assert!(matches_completely(abbb.match_production("A", 0), 4));
let mut bbb = Interpreter::new(&grammar, "bbb");
assert!(matches_completely(bbb.match_production("A", 0), 3));
}
#[crate::praxis_value(Verifiable)]
#[test]
fn matches_subtraction_for_comment_body_char() {
let spec = "
<prod id=\"NT-Char\" num=\"2\">
<lhs>Char</lhs>
<rhs>[#x20-#xD7FF]</rhs>
</prod>
<prod id=\"NT-CharNotHyphen\" num=\"99\">
<lhs>CharNotHyphen</lhs>
<rhs><nt def=\"NT-Char\">Char</nt> - \"-\"</rhs>
</prod>
";
let grammar = load_grammar(spec).unwrap();
let mut a = Interpreter::new(&grammar, "a");
assert!(matches_completely(
a.match_production("CharNotHyphen", 0),
1
));
let mut hyphen = Interpreter::new(&grammar, "-");
assert!(matches!(
hyphen.match_production("CharNotHyphen", 0),
MatchResult::NoMatch
));
}
#[crate::praxis_value(Deterministic)]
#[test]
fn match_production_caches_result_across_calls() {
let spec = "
<prod id=\"NT-X\" num=\"1\">
<lhs>X</lhs>
<rhs>\"hello\"</rhs>
</prod>
";
let grammar = load_grammar(spec).unwrap();
let mut interp = Interpreter::new(&grammar, "hello world");
let r1 = interp.match_production("X", 0);
let r2 = interp.match_production("X", 0);
assert_eq!(r1, r2);
assert!(interp.cache.contains_key(&("X".to_string(), 0)));
}
}