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