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/// `grammar/isc.parg` — vendored verbatim from interscript-ruby@34bd8829
15/// (introduced in WO13 commit 873caf2; blob c9d4ffc8), then extended in
16/// place with the ISC-WF-1 workflow constructs (ruleset / use /
17/// override / languages / when, spec docs/isc-wf-1.md §2). The new
18/// keywords join the keyword table (`ruleset` included: it is a block
19/// head, and the ident-list guards need every sibling block head
20/// keyworded); no corpus map uses them at bare-identifier positions,
21/// so the extension is parse-additive.
22pub const ISC_PARG: &str = include_str!("../grammar/isc.parg");
23
24fn compiled() -> &'static Grammar {
25    static GRAMMAR: OnceLock<Grammar> = OnceLock::new();
26    GRAMMAR.get_or_init(|| {
27        let (parsed, mode) = parse_grammar_with_mode(ISC_PARG)
28            .expect("embedded grammar/isc.parg must parse");
29        compile_entry_mode(
30            &parsed.document,
31            "isc",
32            &Tables::new(),
33            &parsed.atomic_rules,
34            mode.as_deref(),
35        )
36        .expect("embedded grammar/isc.parg must compile")
37    })
38}
39
40/// Parse an ISC map source into the parslet-shaped value tree the
41/// Ruby-side `Interscript::Isc::DocumentBuilder` consumes.
42pub fn parse_isc(source: &str) -> Result<TNode, (usize, Vec<String>)> {
43    let grammar = compiled();
44    let mut arena = parsanol::portable::AstArena::for_input(source.len().max(1 << 16));
45    let mut parser = parsanol::portable::PortableParser::new(grammar, source, &mut arena);
46    let ast = match parser.parse() {
47        Ok(ast) => ast,
48        Err(_) => {
49            let wire = match parser.failure_wire() {
50                Some((offset, expected)) => (offset, expected),
51                None => (source.len(), Vec::new()),
52            };
53            return Err(wire);
54        }
55    };
56    let shaped = to_parslet_compatible(&ast, &mut arena, source);
57    Ok(resolve(&shaped, &arena, source))
58}
59
60/// Run the accept/reject suite embedded in `grammar/isc.parg`
61/// (`test isc { accept ... / reject ... }`).
62pub fn run_grammar_suite() -> (usize, usize, Vec<String>) {
63    let (parsed, mode) = parse_grammar_with_mode(ISC_PARG).expect("grammar parses");
64    let grammar = compile_entry_mode(
65        &parsed.document,
66        "isc",
67        &Tables::new(),
68        &parsed.atomic_rules,
69        mode.as_deref(),
70    )
71    .expect("grammar compiles");
72
73    let mut passed = 0usize;
74    let mut failed = 0usize;
75    let mut failures = Vec::new();
76    for test in &parsed.document.tests {
77        let mut arena = parsanol::portable::AstArena::for_input(test.input.len().max(1 << 12));
78        let mut parser = parsanol::portable::PortableParser::new(&grammar, &test.input, &mut arena);
79        let ok = parser.parse().is_ok();
80        let wanted = test.kind == "accept";
81        if ok == wanted {
82            passed += 1;
83        } else {
84            failed += 1;
85            failures.push(format!("{} {:?} (parsed={ok})", test.kind, test.input));
86        }
87    }
88    (passed, failed, failures)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn embedded_suite_passes() {
97        let (passed, failed, failures) = run_grammar_suite();
98        assert_eq!(failed, 0, "failures: {failures:?}");
99        assert!(passed >= 8, "expected the parg suite, got {passed} passes");
100    }
101
102    #[test]
103    fn minimal_map_parses() {
104        let src = r#"system "T:a-b:c-d:1" { stage main { parallel { sub "a" "b" } } }"#;
105        assert!(parse_isc(src).is_ok());
106    }
107
108    #[test]
109    fn truncated_map_fails_with_wire() {
110        let src = r#"system "T:a-b:c-d:1" {"#;
111        let err = parse_isc(src).unwrap_err();
112        assert!(err.1.iter().any(|e| e.contains("metadata") || e.contains("stage") || e.contains('"')), "{err:?}");
113    }
114}