Skip to main content

interscript_rs/
grammar.rs

1//! The embedded ISC grammar: `grammar/isc.parg` (vendored verbatim
2//! from interscript-ruby, see README provenance) compiled once through
3//! parsanol's portable backend, plus the parse entry point that
4//! resolves the engine's arena-backed tree into the owned
5//! parslet-shaped value tree (`tree::TNode`).
6
7use crate::tree::{resolve, TNode};
8use parsanol::parg::frontend::compile::{compile_entry_mode, Tables};
9use parsanol::parg::frontend::parser::parse_grammar_with_mode;
10use parsanol::portable::parslet_transform::to_parslet_compatible;
11use parsanol::portable::Grammar;
12use std::sync::OnceLock;
13
14/// Vendored verbatim from interscript-ruby@34bd8829 `grammar/isc.parg`
15/// (introduced in WO13 commit 873caf2; blob c9d4ffc8).
16pub const ISC_PARG: &str = include_str!("../grammar/isc.parg");
17
18fn compiled() -> &'static Grammar {
19    static GRAMMAR: OnceLock<Grammar> = OnceLock::new();
20    GRAMMAR.get_or_init(|| {
21        let (parsed, mode) = parse_grammar_with_mode(ISC_PARG)
22            .expect("embedded grammar/isc.parg must parse");
23        compile_entry_mode(
24            &parsed.document,
25            "isc",
26            &Tables::new(),
27            &parsed.atomic_rules,
28            mode.as_deref(),
29        )
30        .expect("embedded grammar/isc.parg must compile")
31    })
32}
33
34/// Parse an ISC map source into the parslet-shaped value tree the
35/// Ruby-side `Interscript::Isc::DocumentBuilder` consumes.
36pub fn parse_isc(source: &str) -> Result<TNode, (usize, Vec<String>)> {
37    let grammar = compiled();
38    let mut arena = parsanol::portable::AstArena::for_input(source.len().max(1 << 16));
39    let mut parser = parsanol::portable::PortableParser::new(grammar, source, &mut arena);
40    let ast = match parser.parse() {
41        Ok(ast) => ast,
42        Err(_) => {
43            let wire = match parser.failure_wire() {
44                Some((offset, expected)) => (offset, expected),
45                None => (source.len(), Vec::new()),
46            };
47            return Err(wire);
48        }
49    };
50    let shaped = to_parslet_compatible(&ast, &mut arena, source);
51    Ok(resolve(&shaped, &arena, source))
52}
53
54/// Run the accept/reject suite embedded in `grammar/isc.parg`
55/// (`test isc { accept ... / reject ... }`).
56pub fn run_grammar_suite() -> (usize, usize, Vec<String>) {
57    let (parsed, mode) = parse_grammar_with_mode(ISC_PARG).expect("grammar parses");
58    let grammar = compile_entry_mode(
59        &parsed.document,
60        "isc",
61        &Tables::new(),
62        &parsed.atomic_rules,
63        mode.as_deref(),
64    )
65    .expect("grammar compiles");
66
67    let mut passed = 0usize;
68    let mut failed = 0usize;
69    let mut failures = Vec::new();
70    for test in &parsed.document.tests {
71        let mut arena = parsanol::portable::AstArena::for_input(test.input.len().max(1 << 12));
72        let mut parser = parsanol::portable::PortableParser::new(&grammar, &test.input, &mut arena);
73        let ok = parser.parse().is_ok();
74        let wanted = test.kind == "accept";
75        if ok == wanted {
76            passed += 1;
77        } else {
78            failed += 1;
79            failures.push(format!("{} {:?} (parsed={ok})", test.kind, test.input));
80        }
81    }
82    (passed, failed, failures)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn embedded_suite_passes() {
91        let (passed, failed, failures) = run_grammar_suite();
92        assert_eq!(failed, 0, "failures: {failures:?}");
93        assert!(passed >= 8, "expected the parg suite, got {passed} passes");
94    }
95
96    #[test]
97    fn minimal_map_parses() {
98        let src = r#"system "T:a-b:c-d:1" { stage main { parallel { sub "a" "b" } } }"#;
99        assert!(parse_isc(src).is_ok());
100    }
101
102    #[test]
103    fn truncated_map_fails_with_wire() {
104        let src = r#"system "T:a-b:c-d:1" {"#;
105        let err = parse_isc(src).unwrap_err();
106        assert!(err.1.iter().any(|e| e.contains("metadata") || e.contains("stage") || e.contains('"')), "{err:?}");
107    }
108}