use alloc::{
boxed::Box,
format,
string::{String, ToString},
};
use super::ast::{Grammar, Production};
use super::rhs_parser::{ParseRhsError, parse_rhs};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadGrammarError {
Malformed {
production: Option<String>,
position: usize,
what: String,
},
RhsParse {
production: String,
position: usize,
cause: Box<ParseRhsError>,
},
}
impl core::fmt::Display for LoadGrammarError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Malformed {
production,
position,
what,
} => match production {
Some(name) => write!(
f,
"malformed production `{name}` at byte {position}: {what}"
),
None => write!(f, "malformed spec at byte {position}: {what}"),
},
Self::RhsParse {
production,
position,
cause,
} => write!(
f,
"RHS parse error in production `{production}` near byte {position}: {cause}"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for LoadGrammarError {}
pub fn load_grammar(spec_bytes: &str) -> Result<Grammar, LoadGrammarError> {
let mut grammar = Grammar::new();
let stripped = strip_xml_comments(spec_bytes);
let spec_bytes: &str = &stripped;
let mut cursor = 0;
while let Some(rel) = spec_bytes[cursor..].find("<prod ") {
let prod_start = cursor + rel;
let close_open =
spec_bytes[prod_start..]
.find('>')
.ok_or_else(|| LoadGrammarError::Malformed {
production: None,
position: prod_start,
what: "unterminated <prod> open tag".to_string(),
})?;
let open_attrs = &spec_bytes[prod_start + 6..prod_start + close_open];
let num = extract_attr(open_attrs, "num").unwrap_or_default();
let prod_close = spec_bytes[prod_start..].find("</prod>").ok_or_else(|| {
LoadGrammarError::Malformed {
production: None,
position: prod_start,
what: "missing </prod>".to_string(),
}
})?;
let prod_body = &spec_bytes[prod_start + close_open + 1..prod_start + prod_close];
let (lhs_name, rhs_content) = split_lhs_rhs(prod_body, prod_start)?;
let rhs_term = parse_rhs(&rhs_content).map_err(|e| LoadGrammarError::RhsParse {
production: lhs_name.clone(),
position: prod_start,
cause: Box::new(e),
})?;
grammar.add(Production {
name: lhs_name,
number: num,
rhs: rhs_term,
});
cursor = prod_start + prod_close + "</prod>".len();
}
Ok(grammar)
}
fn strip_xml_comments(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(start) = rest.find("<!--") {
out.push_str(&rest[..start]);
let after_open = &rest[start + "<!--".len()..];
if let Some(end) = after_open.find("-->") {
rest = &after_open[end + "-->".len()..];
} else {
out.push_str(&rest[start..]);
return out;
}
}
out.push_str(rest);
out
}
fn extract_attr(open_attrs: &str, key: &str) -> Option<String> {
let needle = format!("{key}=");
let idx = open_attrs.find(&needle)?;
let after = &open_attrs[idx + needle.len()..];
let quote = after.chars().next()?;
if quote != '"' && quote != '\'' {
return None;
}
let rest = &after[1..];
let end = rest.find(quote)?;
Some(rest[..end].to_string())
}
fn split_lhs_rhs(body: &str, prod_pos: usize) -> Result<(String, String), LoadGrammarError> {
let lhs_open = body
.find("<lhs")
.ok_or_else(|| LoadGrammarError::Malformed {
production: None,
position: prod_pos,
what: "missing <lhs>".to_string(),
})?;
let after_lhs_open_tag =
body[lhs_open + 4..]
.find('>')
.ok_or_else(|| LoadGrammarError::Malformed {
production: None,
position: prod_pos,
what: "unterminated <lhs> open tag".to_string(),
})?;
let lhs_content_start = lhs_open + 4 + after_lhs_open_tag + 1;
let lhs_content_end =
body[lhs_content_start..]
.find("</lhs>")
.ok_or_else(|| LoadGrammarError::Malformed {
production: None,
position: prod_pos,
what: "missing </lhs>".to_string(),
})?;
let lhs_name = body[lhs_content_start..lhs_content_start + lhs_content_end]
.trim()
.to_string();
let mut rhs_parts: Vec<String> = Vec::new();
let mut cursor = lhs_content_start + lhs_content_end + "</lhs>".len();
while let Some(rel) = body[cursor..].find("<rhs") {
let rhs_open_abs = cursor + rel;
let after_rhs_open_tag =
body[rhs_open_abs + 4..]
.find('>')
.ok_or_else(|| LoadGrammarError::Malformed {
production: Some(lhs_name.clone()),
position: prod_pos,
what: "unterminated <rhs> open tag".to_string(),
})?;
let rhs_content_start = rhs_open_abs + 4 + after_rhs_open_tag + 1;
let rhs_content_end = body[rhs_content_start..].find("</rhs>").ok_or_else(|| {
LoadGrammarError::Malformed {
production: Some(lhs_name.clone()),
position: prod_pos,
what: "missing </rhs>".to_string(),
}
})?;
rhs_parts.push(body[rhs_content_start..rhs_content_start + rhs_content_end].to_string());
cursor = rhs_content_start + rhs_content_end + "</rhs>".len();
}
if rhs_parts.is_empty() {
return Err(LoadGrammarError::Malformed {
production: Some(lhs_name),
position: prod_pos,
what: "no <rhs> blocks".to_string(),
});
}
Ok((lhs_name, rhs_parts.join(" ")))
}
#[cfg(test)]
mod tests {
use super::*;
fn pick_attrs(s: &str) -> &str {
&s[6..s.find('>').unwrap()]
}
#[crate::praxis_value(Verifiable)]
#[test]
fn extracts_double_quoted_attribute() {
let open = "<prod id=\"NT-Char\" num=\"2\">";
assert_eq!(extract_attr(pick_attrs(open), "num").as_deref(), Some("2"));
assert_eq!(
extract_attr(pick_attrs(open), "id").as_deref(),
Some("NT-Char")
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn extracts_single_quoted_attribute() {
let open = "<prod id='NT-ExternalDef'>";
assert_eq!(
extract_attr(pick_attrs(open), "id").as_deref(),
Some("NT-ExternalDef")
);
}
#[crate::praxis_value(Verifiable)]
#[test]
fn loads_two_production_fixture() {
let spec = "
<prod id=\"NT-document\" num=\"1\">
<lhs>document</lhs>
<rhs>#x9 | #xA</rhs>
</prod>
<prod id=\"NT-Char\" num=\"2\">
<lhs diff=\"chg\">Char</lhs>
<rhs>#x9 | #xA | #xD | [#x20-#xD7FF]</rhs>
</prod>
";
let g = load_grammar(spec).expect("load");
assert_eq!(g.len(), 2);
let p1 = g.lookup("document").expect("document");
assert_eq!(p1.number, "1");
let p2 = g.lookup("Char").expect("Char");
assert_eq!(p2.number, "2");
}
#[crate::praxis_value(Honest)]
#[test]
fn reports_rhs_parse_errors() {
let spec = "<prod id=\"NT-Bad\" num=\"99\">
<lhs>Bad</lhs>
<rhs>?</rhs>
</prod>";
let err = load_grammar(spec).unwrap_err();
match err {
LoadGrammarError::RhsParse { production, .. } => assert_eq!(production, "Bad"),
_ => panic!("expected RhsParse"),
}
}
}