use alloc::{
boxed::Box,
format,
string::{String, ToString},
vec,
vec::Vec,
};
use super::ast::{CodePointRange, Term};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseRhsError {
Tokenize { position: usize, what: String },
EmptyRhs,
DanglingQuantifier { position: usize },
DanglingOperator { position: usize, op: char },
UnbalancedParen { position: usize },
}
impl core::fmt::Display for ParseRhsError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Tokenize { position, what } => {
write!(f, "tokenize error at {position}: {what}")
}
Self::EmptyRhs => f.write_str("empty RHS — productions must have at least one term"),
Self::DanglingQuantifier { position } => {
write!(f, "dangling postfix quantifier at {position}")
}
Self::DanglingOperator { position, op } => {
write!(f, "dangling operator '{op}' at {position}")
}
Self::UnbalancedParen { position } => write!(f, "unbalanced parenthesis at {position}"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseRhsError {}
pub fn parse_rhs(rhs_content: &str) -> Result<Term, ParseRhsError> {
let preprocessed = collapse_nbsp(rhs_content);
let tokens = tokenize(&preprocessed)?;
if tokens.is_empty() {
return Err(ParseRhsError::EmptyRhs);
}
let mut tp = TokenParser::new(&tokens);
let term = tp.parse_alternation()?;
if tp.peek().is_some() {
return Err(ParseRhsError::UnbalancedParen {
position: tp.pos_byte(),
});
}
Ok(term)
}
fn collapse_nbsp(raw: &str) -> String {
raw.replace(" ", " ")
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Tok {
Nt(String), Literal(String), Hex(u32), CharClass(Vec<CodePointRange>), NegatedChars(Vec<CodePointRange>), OpenParen, CloseParen, Pipe, Minus, Question, Star, Plus, }
#[derive(Debug, Clone)]
struct Token {
tok: Tok,
pos: usize,
}
fn tokenize(s: &str) -> Result<Vec<Token>, ParseRhsError> {
let mut out = Vec::new();
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_whitespace() {
i += 1;
continue;
}
if s[i..].starts_with("<nt") {
let (consumed, name) = read_nt_element(s, i)?;
out.push(Token {
tok: Tok::Nt(name),
pos: i,
});
i += consumed;
continue;
}
if bytes[i] == b'\'' || bytes[i] == b'"' {
let quote = bytes[i] as char;
let (consumed, lit) = read_literal(s, i, quote)?;
out.push(Token {
tok: Tok::Literal(lit),
pos: i,
});
i += consumed;
continue;
}
if bytes[i] == b'#' && bytes.get(i + 1) == Some(&b'x') {
let (consumed, code) = read_hex(s, i + 2)?;
out.push(Token {
tok: Tok::Hex(code),
pos: i,
});
i += 2 + consumed;
continue;
}
if bytes[i] == b'[' {
let (consumed, ranges, negated) = read_char_class(s, i)?;
let tok = if negated {
Tok::NegatedChars(ranges)
} else {
Tok::CharClass(ranges)
};
out.push(Token { tok, pos: i });
i += consumed;
continue;
}
let single = match bytes[i] {
b'(' => Some(Tok::OpenParen),
b')' => Some(Tok::CloseParen),
b'|' => Some(Tok::Pipe),
b'-' => Some(Tok::Minus),
b'?' => Some(Tok::Question),
b'*' => Some(Tok::Star),
b'+' => Some(Tok::Plus),
_ => None,
};
if let Some(t) = single {
out.push(Token { tok: t, pos: i });
i += 1;
continue;
}
return Err(ParseRhsError::Tokenize {
position: i,
what: format!("unexpected byte {:?}", bytes[i] as char),
});
}
Ok(out)
}
fn read_nt_element(s: &str, i: usize) -> Result<(usize, String), ParseRhsError> {
let rest = &s[i..];
let close_open = rest.find('>').ok_or_else(|| ParseRhsError::Tokenize {
position: i,
what: "unterminated <nt> open tag".to_string(),
})?;
let after_open = close_open + 1;
let inner_start = after_open;
let close_tag = rest[inner_start..]
.find("</nt>")
.ok_or_else(|| ParseRhsError::Tokenize {
position: i,
what: "missing </nt>".to_string(),
})?;
let name = rest[inner_start..inner_start + close_tag]
.trim()
.to_string();
let consumed = inner_start + close_tag + "</nt>".len();
Ok((consumed, name))
}
fn read_literal(s: &str, i: usize, quote: char) -> Result<(usize, String), ParseRhsError> {
let rest = &s[i + 1..];
let end = rest.find(quote).ok_or_else(|| ParseRhsError::Tokenize {
position: i,
what: format!("unterminated {quote} literal"),
})?;
let raw = &rest[..end];
Ok((1 + end + 1, decode_entities(raw)))
}
fn decode_entities(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
let mut rest = raw;
while let Some(amp) = rest.find('&') {
out.push_str(&rest[..amp]);
rest = &rest[amp..];
let replacement = if rest.starts_with("<") {
("<", "<".len())
} else if rest.starts_with(">") {
(">", ">".len())
} else if rest.starts_with("&") {
("&", "&".len())
} else if rest.starts_with(""") {
("\"", """.len())
} else if rest.starts_with("'") {
("'", "'".len())
} else if rest.starts_with(" ") {
(" ", " ".len())
} else {
("&", 1)
};
out.push_str(replacement.0);
rest = &rest[replacement.1..];
}
out.push_str(rest);
out
}
fn read_hex(s: &str, i: usize) -> Result<(usize, u32), ParseRhsError> {
let bytes = s.as_bytes();
let mut j = i;
while j < bytes.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if j == i {
return Err(ParseRhsError::Tokenize {
position: i,
what: "expected hex digits after #x".to_string(),
});
}
let code = u32::from_str_radix(&s[i..j], 16).map_err(|_| ParseRhsError::Tokenize {
position: i,
what: format!("invalid hex code point {:?}", &s[i..j]),
})?;
Ok((j - i, code))
}
fn read_char_class(s: &str, i: usize) -> Result<(usize, Vec<CodePointRange>, bool), ParseRhsError> {
let rest = &s[i..];
let close = rest.find(']').ok_or_else(|| ParseRhsError::Tokenize {
position: i,
what: "unterminated [char-class]".to_string(),
})?;
let raw_inner = &rest[1..close];
let consumed = close + 1;
let decoded = decode_entities(raw_inner);
let mut inner: &str = &decoded;
let negated = if let Some(stripped) = inner.strip_prefix('^') {
inner = stripped;
true
} else {
false
};
let mut atoms = Vec::new();
while !inner.is_empty() {
let (atom, rest_inner) = parse_class_atom(inner, i)?;
atoms.push(atom);
inner = rest_inner;
}
if atoms.is_empty() {
return Err(ParseRhsError::Tokenize {
position: i,
what: "empty character class".to_string(),
});
}
Ok((consumed, atoms, negated))
}
fn parse_class_atom(inner: &str, byte_pos: usize) -> Result<(CodePointRange, &str), ParseRhsError> {
if let Some(after_hash) = inner.strip_prefix("#x") {
let (lo, after_lo) = split_off_hex(after_hash, byte_pos)?;
if let Some(after_dash_hash) = after_lo.strip_prefix("-#x") {
let (hi, tail) = split_off_hex(after_dash_hash, byte_pos)?;
return Ok((CodePointRange { lo, hi }, tail));
}
return Ok((CodePointRange { lo, hi: lo }, after_lo));
}
let first = inner
.chars()
.next()
.ok_or_else(|| ParseRhsError::Tokenize {
position: byte_pos,
what: "expected a character-class atom".to_string(),
})?;
let after_first = &inner[first.len_utf8()..];
if let Some(rest_after_dash) = after_first.strip_prefix('-')
&& let Some(second) = rest_after_dash.chars().next()
{
let lo = first as u32;
let hi = second as u32;
let consumed = first.len_utf8() + 1 + second.len_utf8();
return Ok((CodePointRange { lo, hi }, &inner[consumed..]));
}
let cp = first as u32;
Ok((CodePointRange { lo: cp, hi: cp }, after_first))
}
fn split_off_hex(s: &str, byte_pos: usize) -> Result<(u32, &str), ParseRhsError> {
let end = s
.bytes()
.position(|b| !b.is_ascii_hexdigit())
.unwrap_or(s.len());
if end == 0 {
return Err(ParseRhsError::Tokenize {
position: byte_pos,
what: "expected hex digits".to_string(),
});
}
let value = u32::from_str_radix(&s[..end], 16).map_err(|_| ParseRhsError::Tokenize {
position: byte_pos,
what: format!("invalid hex in range: {:?}", &s[..end]),
})?;
Ok((value, &s[end..]))
}
struct TokenParser<'a> {
tokens: &'a [Token],
idx: usize,
depth: usize,
}
impl<'a> TokenParser<'a> {
fn new(tokens: &'a [Token]) -> Self {
Self {
tokens,
idx: 0,
depth: 0,
}
}
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.idx)
}
fn bump(&mut self) -> Option<&'a Token> {
let t = self.tokens.get(self.idx);
if t.is_some() {
self.idx += 1;
}
t
}
fn pos_byte(&self) -> usize {
self.tokens
.get(self.idx)
.or_else(|| self.tokens.last())
.map(|t| t.pos)
.unwrap_or(0)
}
fn parse_alternation(&mut self) -> Result<Term, ParseRhsError> {
const MAX_RHS_DEPTH: usize = 256;
self.depth += 1;
if self.depth > MAX_RHS_DEPTH {
self.depth -= 1;
return Err(ParseRhsError::UnbalancedParen {
position: self.pos_byte(),
});
}
let result = self.parse_alternation_inner();
self.depth -= 1;
result
}
fn parse_alternation_inner(&mut self) -> Result<Term, ParseRhsError> {
let first = self.parse_sequence()?;
if !matches!(self.peek().map(|t| &t.tok), Some(Tok::Pipe)) {
return Ok(first);
}
let mut branches = vec![first];
while matches!(self.peek().map(|t| &t.tok), Some(Tok::Pipe)) {
let pipe_pos = self.bump().unwrap().pos;
if self
.peek()
.map(|t| matches!(&t.tok, Tok::CloseParen | Tok::Pipe))
.unwrap_or(true)
{
return Err(ParseRhsError::DanglingOperator {
position: pipe_pos,
op: '|',
});
}
branches.push(self.parse_sequence()?);
}
Ok(fold_char_class(branches))
}
fn parse_sequence(&mut self) -> Result<Term, ParseRhsError> {
let mut items = Vec::new();
while let Some(t) = self.peek() {
if matches!(t.tok, Tok::Pipe | Tok::CloseParen) {
break;
}
items.push(self.parse_subtraction()?);
}
if items.is_empty() {
return Err(ParseRhsError::DanglingOperator {
position: self.pos_byte(),
op: '|',
});
}
Ok(if items.len() == 1 {
items.pop().unwrap()
} else {
Term::Sequence(items)
})
}
fn parse_subtraction(&mut self) -> Result<Term, ParseRhsError> {
let lhs = self.parse_quantified()?;
if let Some(Token {
tok: Tok::Minus,
pos,
}) = self.peek()
{
let minus_pos = *pos;
self.bump();
if self
.peek()
.map(|t| matches!(t.tok, Tok::Pipe | Tok::CloseParen))
.unwrap_or(true)
{
return Err(ParseRhsError::DanglingOperator {
position: minus_pos,
op: '-',
});
}
let rhs = self.parse_quantified()?;
Ok(Term::Subtraction(Box::new(lhs), Box::new(rhs)))
} else {
Ok(lhs)
}
}
fn parse_quantified(&mut self) -> Result<Term, ParseRhsError> {
let atom = self.parse_atom()?;
if let Some(t) = self.peek() {
match t.tok {
Tok::Question => {
self.bump();
return Ok(Term::Optional(Box::new(atom)));
}
Tok::Star => {
self.bump();
return Ok(Term::ZeroOrMore(Box::new(atom)));
}
Tok::Plus => {
self.bump();
return Ok(Term::OneOrMore(Box::new(atom)));
}
_ => {}
}
}
Ok(atom)
}
fn parse_atom(&mut self) -> Result<Term, ParseRhsError> {
let t = self.bump().ok_or(ParseRhsError::EmptyRhs)?.clone();
match t.tok {
Tok::Nt(name) => Ok(Term::NonTerminal(name)),
Tok::Literal(s) => Ok(Term::Literal(s)),
Tok::Hex(code) => Ok(Term::CharClass(vec![CodePointRange { lo: code, hi: code }])),
Tok::CharClass(ranges) => Ok(Term::CharClass(ranges)),
Tok::NegatedChars(ranges) => Ok(Term::Subtraction(
Box::new(Term::NonTerminal("Char".to_string())),
Box::new(Term::CharClass(ranges)),
)),
Tok::OpenParen => {
let inner = self.parse_alternation()?;
let close = self.bump();
match close.map(|t| &t.tok) {
Some(Tok::CloseParen) => Ok(inner),
_ => Err(ParseRhsError::UnbalancedParen { position: t.pos }),
}
}
Tok::CloseParen => Err(ParseRhsError::UnbalancedParen { position: t.pos }),
Tok::Question | Tok::Star | Tok::Plus => {
Err(ParseRhsError::DanglingQuantifier { position: t.pos })
}
Tok::Pipe => Err(ParseRhsError::DanglingOperator {
position: t.pos,
op: '|',
}),
Tok::Minus => Err(ParseRhsError::DanglingOperator {
position: t.pos,
op: '-',
}),
}
}
}
fn fold_char_class(branches: Vec<Term>) -> Term {
let mut ranges = Vec::new();
let mut other_branches: Vec<Term> = Vec::new();
for b in branches {
match b {
Term::CharClass(rs) => ranges.extend(rs),
other => other_branches.push(other),
}
}
if other_branches.is_empty() {
return Term::CharClass(ranges);
}
if ranges.is_empty() {
return if other_branches.len() == 1 {
other_branches.pop().unwrap()
} else {
Term::Alternation(other_branches)
};
}
let mut all = Vec::with_capacity(other_branches.len() + 1);
all.push(Term::CharClass(ranges));
all.extend(other_branches);
Term::Alternation(all)
}
#[cfg(test)]
mod tests {
use super::*;
fn nt(name: &str) -> Term {
Term::NonTerminal(name.to_string())
}
fn lit(s: &str) -> Term {
Term::Literal(s.to_string())
}
fn range(lo: u32, hi: u32) -> CodePointRange {
CodePointRange { lo, hi }
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_default_decl_rhs_with_nested_optional_group() {
let rhs = r#"'#REQUIRED' | '#IMPLIED' | (('#FIXED' <nt def="NT-S">S</nt>)? <nt def="NT-AttValue">AttValue</nt>)"#;
let t = parse_rhs(rhs).unwrap();
assert_eq!(
t,
Term::Alternation(vec![
lit("#REQUIRED"),
lit("#IMPLIED"),
Term::Sequence(vec![
Term::Optional(Box::new(Term::Sequence(vec![lit("#FIXED"), nt("S")]))),
nt("AttValue"),
]),
])
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_char_production_rhs() {
let rhs = "#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]";
let t = parse_rhs(rhs).unwrap();
assert_eq!(
t,
Term::CharClass(vec![
range(9, 9),
range(0xA, 0xA),
range(0xD, 0xD),
range(0x20, 0xD7FF),
range(0xE000, 0xFFFD),
range(0x10000, 0x10FFFF),
])
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_name_start_char_with_ascii_literals() {
let rhs = "\":\" | [A-Z] | \"_\" | [a-z]";
let t = parse_rhs(rhs).unwrap();
match t {
Term::Alternation(branches) => {
assert!(branches.iter().any(|b| matches!(b, Term::CharClass(_))));
assert!(
branches
.iter()
.any(|b| matches!(b, Term::Literal(s) if s == ":"))
);
assert!(
branches
.iter()
.any(|b| matches!(b, Term::Literal(s) if s == "_"))
);
}
_ => panic!("expected Alternation, got {t:?}"),
}
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_elementdecl_rhs_with_nt_refs_and_literal() {
let rhs = "'<!ELEMENT' <nt def=\"NT-S\">S</nt> <nt def=\"NT-Name\">Name</nt> \
<nt def=\"NT-S\">S</nt> <nt def=\"NT-contentspec\">contentspec</nt> \
<nt def=\"NT-S\">S</nt>? '>'";
let t = parse_rhs(rhs).unwrap();
match t {
Term::Sequence(items) => {
assert_eq!(items[0], lit("<!ELEMENT"));
assert_eq!(items[1], nt("S"));
assert_eq!(items[2], nt("Name"));
assert_eq!(items[3], nt("S"));
assert_eq!(items[4], nt("contentspec"));
assert_eq!(items[5], Term::Optional(Box::new(nt("S"))));
assert_eq!(items[6], lit(">"));
}
_ => panic!("expected Sequence, got {t:?}"),
}
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_name_char_with_nt_reference_then_alternation() {
let rhs = "<nt def=\"NT-NameStartChar\">NameStartChar</nt> | \"-\" | \".\" | [0-9] | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]";
let t = parse_rhs(rhs).unwrap();
match t {
Term::Alternation(branches) => {
assert!(
branches
.iter()
.any(|b| matches!(b, Term::NonTerminal(s) if s == "NameStartChar"))
);
let cc_count = branches
.iter()
.filter(|b| matches!(b, Term::CharClass(_)))
.count();
assert_eq!(cc_count, 1, "char-class atoms must fold to one branch");
let cc = branches
.iter()
.find_map(|b| match b {
Term::CharClass(r) => Some(r),
_ => None,
})
.unwrap();
assert_eq!(cc.len(), 4);
assert!(
branches
.iter()
.any(|b| matches!(b, Term::Literal(s) if s == "-"))
);
assert!(
branches
.iter()
.any(|b| matches!(b, Term::Literal(s) if s == "."))
);
}
_ => panic!("expected Alternation, got {t:?}"),
}
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_subtraction_for_comment_char() {
let rhs = "<nt def=\"NT-Char\">Char</nt> - \"-\"";
let t = parse_rhs(rhs).unwrap();
assert_eq!(
t,
Term::Subtraction(Box::new(nt("Char")), Box::new(lit("-")))
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_kleene_star_and_plus() {
let rhs = "<nt def=\"NT-A\">A</nt>* <nt def=\"NT-B\">B</nt>+";
let t = parse_rhs(rhs).unwrap();
assert_eq!(
t,
Term::Sequence(vec![
Term::ZeroOrMore(Box::new(nt("A"))),
Term::OneOrMore(Box::new(nt("B"))),
])
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn parses_grouping_for_sequence_inside_alternation() {
let rhs = "(<nt def=\"NT-A\">A</nt> <nt def=\"NT-B\">B</nt>) | <nt def=\"NT-C\">C</nt>";
let t = parse_rhs(rhs).unwrap();
assert_eq!(
t,
Term::Alternation(vec![Term::Sequence(vec![nt("A"), nt("B")]), nt("C"),])
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn decodes_xml_predefined_entities_in_literals() {
let rhs = "'<' | '>' | '&' | '"' | '''";
let t = parse_rhs(rhs).unwrap();
match t {
Term::Alternation(branches) => {
let lits: Vec<&str> = branches
.iter()
.filter_map(|b| {
if let Term::Literal(s) = b {
Some(s.as_str())
} else {
None
}
})
.collect();
assert!(lits.contains(&"<"));
assert!(lits.contains(&">"));
assert!(lits.contains(&"&"));
assert!(lits.contains(&"\""));
assert!(lits.contains(&"'"));
}
_ => panic!("expected Alternation"),
}
}
#[crate::praxis_value(Honest)]
#[test]
fn rejects_empty_rhs() {
assert!(matches!(parse_rhs(""), Err(ParseRhsError::EmptyRhs)));
assert!(matches!(parse_rhs(" "), Err(ParseRhsError::EmptyRhs)));
}
#[crate::praxis_value(Honest)]
#[test]
fn rejects_dangling_quantifier() {
assert!(matches!(
parse_rhs("?"),
Err(ParseRhsError::DanglingQuantifier { .. })
));
}
#[crate::praxis_value(Honest)]
#[test]
fn rejects_unbalanced_paren() {
let r = parse_rhs("( #x20");
assert!(matches!(r, Err(ParseRhsError::UnbalancedParen { .. })));
}
}