//! rust-langrpg — RPG IV free-format parser //! //! Loads the BNF grammar embedded at compile time, builds a [`bnf::GrammarParser`], //! and demonstrates parsing a handful of RPG IV free-format snippets. use bnf::{Grammar, Term}; /// The RPG IV BNF grammar, embedded at compile time from `src/rpg.bnf`. const RPG_BNF: &str = include_str!("rpg.bnf"); // ───────────────────────────────────────────────────────────────────────────── // Public API // ───────────────────────────────────────────────────────────────────────────── /// Load and validate the RPG IV grammar. /// /// Returns `Err` if the BNF source is malformed. pub fn load_grammar() -> Result { RPG_BNF.parse::() } /// Parse `source` as the given `rule` (a non-terminal name such as `"statement"`). /// /// Returns `Some(parse_tree_string)` for the first successful parse, or `None` /// when the grammar cannot match the input. pub fn parse_as<'a>( parser: &'a bnf::GrammarParser, source: &str, rule: &str, ) -> Option { let target = Term::Nonterminal(rule.to_string()); parser .parse_input_starting_with(source, &target) .next() .map(|tree| tree.to_string()) } // ───────────────────────────────────────────────────────────────────────────── // Demo helpers // ───────────────────────────────────────────────────────────────────────────── struct Example { label: &'static str, rule: &'static str, src: &'static str, } fn run_example(parser: &bnf::GrammarParser, ex: &Example) { println!(" ┌─ {} ({}) ─────────────────────", ex.label, ex.rule); println!(" │ source : {:?}", ex.src); match parse_as(parser, ex.src, ex.rule) { Some(tree) => { // Only print the first line of a potentially huge tree. let preview: String = tree.lines().take(6).collect::>().join("\n │ "); println!(" │ result : OK"); println!(" │ tree : {}", preview); } None => println!(" │ result : NO PARSE"), } println!(" └──────────────────────────────────────────────"); println!(); } // ───────────────────────────────────────────────────────────────────────────── // main // ───────────────────────────────────────────────────────────────────────────── fn main() { // ── 1. Load grammar ──────────────────────────────────────────────────── println!("=== RPG IV Free-Format Parser ==="); println!(); let grammar = match load_grammar() { Ok(g) => { println!("[grammar] Loaded successfully."); g } Err(e) => { eprintln!("[grammar] Failed to parse BNF: {}", e); std::process::exit(1); } }; // ── 2. Build parser ──────────────────────────────────────────────────── let parser = match grammar.build_parser() { Ok(p) => { println!("[parser] Built successfully (all non-terminals resolved)."); p } Err(e) => { eprintln!("[parser] Failed to build: {}", e); std::process::exit(1); } }; println!(); // ── 3. Example snippets ──────────────────────────────────────────────── // // Each snippet is a *token stream* that matches the grammar rules. // Because the `bnf` crate performs character-level / terminal matching // the snippets are written exactly as the grammar expects them — // individual keyword tokens separated by spaces aren't needed; the BNF // uses quoted terminal strings so the parser works on the raw text. // // We showcase a range of rules from simple primitives all the way up to // compound statements so you can see the grammar in action. println!("=== Parsing Examples ==="); println!(); let examples: &[Example] = &[ // ── Identifiers ───────────────────────────────────────────────────── Example { label: "simple identifier", rule: "identifier", src: "myVar", }, Example { label: "identifier with digits and underscore", rule: "identifier", src: "calc_Total2", }, // ── Literals ──────────────────────────────────────────────────────── Example { label: "integer literal", rule: "integer-literal", src: "42", }, Example { label: "numeric literal (decimal)", rule: "numeric-literal", src: "3.14", }, // ── Named constants ────────────────────────────────────────────────── Example { label: "named constant *ON", rule: "named-constant", src: "*ON", }, Example { label: "named constant *BLANKS", rule: "named-constant", src: "*BLANKS", }, // ── Type specifications ────────────────────────────────────────────── Example { label: "CHAR type spec", rule: "type-spec", src: "CHAR(10)", }, Example { label: "PACKED type spec", rule: "type-spec", src: "PACKED(7:2)", }, Example { label: "INT type spec", rule: "type-spec", src: "INT(10)", }, Example { label: "DATE type spec", rule: "type-spec", src: "DATE", }, Example { label: "IND (indicator) type spec", rule: "type-spec", src: "IND", }, // ── Declarations ───────────────────────────────────────────────────── Example { label: "standalone character variable", rule: "standalone-decl", src: "DCL-S myName CHAR(25);", }, Example { label: "standalone integer with initialiser", rule: "standalone-decl", src: "DCL-S counter INT(10) INZ(0);", }, Example { label: "constant declaration", rule: "constant-decl", src: "DCL-C MAX_SIZE CONST(100);", }, // ── Control-option specification ───────────────────────────────────── Example { label: "CTL-OPT NOMAIN", rule: "control-spec", src: "CTL-OPT NOMAIN;", }, Example { label: "CTL-OPT DFTACTGRP(*NO)", rule: "control-spec", src: "CTL-OPT DFTACTGRP(*NO);", }, // ── Statements ─────────────────────────────────────────────────────── Example { label: "assignment statement", rule: "assign-stmt", src: "result=a+b;", }, Example { label: "EVAL assignment", rule: "assign-stmt", src: "EVAL result=42;", }, Example { label: "RETURN with value", rule: "return-stmt", src: "RETURN result;", }, Example { label: "bare RETURN", rule: "return-stmt", src: "RETURN;", }, Example { label: "LEAVE statement", rule: "leave-stmt", src: "LEAVE;", }, Example { label: "ITER statement", rule: "iter-stmt", src: "ITER;", }, Example { label: "EXSR call", rule: "exsr-stmt", src: "EXSR calcTotals;", }, // ── I/O statements ─────────────────────────────────────────────────── Example { label: "READ file", rule: "read-stmt", src: "READ myFile;", }, Example { label: "WRITE record", rule: "write-stmt", src: "WRITE outputRec;", }, Example { label: "CHAIN key into file", rule: "chain-stmt", src: "CHAIN keyFld myFile;", }, Example { label: "SETLL to beginning", rule: "setll-stmt", src: "SETLL *START myFile;", }, Example { label: "OPEN file", rule: "open-stmt", src: "OPEN myFile;", }, Example { label: "CLOSE file", rule: "close-stmt", src: "CLOSE myFile;", }, Example { label: "CLOSE *ALL", rule: "close-stmt", src: "CLOSE *ALL;", }, // ── Expressions ────────────────────────────────────────────────────── Example { label: "simple addition expression", rule: "expression", src: "a+b", }, Example { label: "comparison expression", rule: "expression", src: "x>=10", }, Example { label: "NOT expression", rule: "expression", src: "NOT flag", }, Example { label: "combined AND / OR", rule: "expression", src: "a=1ANDb=2", }, Example { label: "parenthesised expression", rule: "expression", src: "(x+y)", }, // ── Built-in functions ─────────────────────────────────────────────── Example { label: "%LEN built-in", rule: "built-in-function", src: "%LEN(myField)", }, Example { label: "%TRIM built-in", rule: "built-in-function", src: "%TRIM(name)", }, Example { label: "%EOF built-in (no arg)", rule: "built-in-function", src: "%EOF()", }, Example { label: "%SUBST built-in (3-arg)", rule: "built-in-function", src: "%SUBST(str:1:5)", }, Example { label: "%DEC built-in", rule: "built-in-function", src: "%DEC(value:9:2)", }, // ── Duration codes ─────────────────────────────────────────────────── Example { label: "duration code *DAYS", rule: "duration-code", src: "*DAYS", }, // ── Date / time formats ────────────────────────────────────────────── Example { label: "date format *ISO", rule: "date-format", src: "*ISO", }, Example { label: "time format *HMS", rule: "time-format", src: "*HMS", }, // ── Qualified names ────────────────────────────────────────────────── Example { label: "simple qualified name", rule: "qualified-name", src: "myDs.field", }, Example { label: "deeply qualified name", rule: "qualified-name", src: "ds.subDs.leaf", }, ]; let total = examples.len(); let mut ok = 0usize; let mut bad = 0usize; for ex in examples { let target = Term::Nonterminal(ex.rule.to_string()); let matched = parser .parse_input_starting_with(ex.src, &target) .next() .is_some(); if matched { ok += 1; } else { bad += 1; } run_example(&parser, ex); } // ── 4. Summary ───────────────────────────────────────────────────────── println!("=== Summary ==="); println!(" total : {}", total); println!(" matched : {}", ok); println!(" failed : {}", bad); println!(); if bad == 0 { println!("All examples parsed successfully."); } else { println!("{} example(s) did not parse — check the BNF or the snippet.", bad); std::process::exit(1); } } // ───────────────────────────────────────────────────────────────────────────── // Unit tests // ───────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; /// Leak a `Grammar` onto the heap so the returned `GrammarParser` can hold /// a `'static` reference to it. The tiny allocation is intentional: this /// is test-only code and the process exits shortly after. fn make_parser() -> bnf::GrammarParser<'static> { let grammar: &'static bnf::Grammar = Box::leak(Box::new(load_grammar().expect("BNF must be valid"))); grammar .build_parser() .expect("all non-terminals must resolve") } // ── Grammar loading ────────────────────────────────────────────────────── #[test] fn grammar_loads() { load_grammar().expect("grammar should parse without error"); } #[test] fn parser_builds() { make_parser(); } // ── Identifiers ────────────────────────────────────────────────────────── #[test] fn identifier_simple() { let p = make_parser(); assert!(parse_as(&p, "abc", "identifier").is_some()); } #[test] fn identifier_with_underscore() { let p = make_parser(); assert!(parse_as(&p, "my_var", "identifier").is_some()); } #[test] fn identifier_single_letter() { let p = make_parser(); assert!(parse_as(&p, "x", "identifier").is_some()); } // ── Literals ───────────────────────────────────────────────────────────── #[test] fn integer_literal() { let p = make_parser(); assert!(parse_as(&p, "0", "integer-literal").is_some()); assert!(parse_as(&p, "999", "integer-literal").is_some()); } #[test] fn numeric_literal_decimal() { let p = make_parser(); assert!(parse_as(&p, "3.14", "numeric-literal").is_some()); } // ── Named constants ─────────────────────────────────────────────────────── #[test] fn named_constants() { let p = make_parser(); for s in &["*ON", "*OFF", "*BLANK", "*BLANKS", "*ZEROS", "*ZERO", "*HIVAL", "*LOVAL", "*NULL"] { assert!(parse_as(&p, s, "named-constant").is_some(), "{} failed", s); } } // ── Type specs ──────────────────────────────────────────────────────────── #[test] fn type_spec_char() { let p = make_parser(); assert!(parse_as(&p, "CHAR(50)", "type-spec").is_some()); } #[test] fn type_spec_packed() { let p = make_parser(); assert!(parse_as(&p, "PACKED(9:2)", "type-spec").is_some()); } #[test] fn type_spec_date() { let p = make_parser(); assert!(parse_as(&p, "DATE", "type-spec").is_some()); } #[test] fn type_spec_ind() { let p = make_parser(); assert!(parse_as(&p, "IND", "type-spec").is_some()); } #[test] fn type_spec_pointer() { let p = make_parser(); assert!(parse_as(&p, "POINTER", "type-spec").is_some()); } // ── Declarations ───────────────────────────────────────────────────────── #[test] fn standalone_decl_no_inz() { let p = make_parser(); assert!(parse_as(&p, "DCL-S total PACKED(9:2);", "standalone-decl").is_some()); } #[test] fn standalone_decl_with_inz() { let p = make_parser(); assert!(parse_as(&p, "DCL-S counter INT(10) INZ(0);", "standalone-decl").is_some()); } #[test] fn constant_decl() { let p = make_parser(); assert!(parse_as(&p, "DCL-C MAX CONST(100);", "constant-decl").is_some()); } // ── Control spec ────────────────────────────────────────────────────────── #[test] fn ctl_opt_nomain() { let p = make_parser(); assert!(parse_as(&p, "CTL-OPT NOMAIN;", "control-spec").is_some()); } #[test] fn ctl_opt_dftactgrp_no() { let p = make_parser(); assert!(parse_as(&p, "CTL-OPT DFTACTGRP(*NO);", "control-spec").is_some()); } // ── Statements ─────────────────────────────────────────────────────────── #[test] fn assign_simple() { let p = make_parser(); assert!(parse_as(&p, "x=1;", "assign-stmt").is_some()); } #[test] fn return_with_expr() { let p = make_parser(); assert!(parse_as(&p, "RETURN result;", "return-stmt").is_some()); } #[test] fn return_bare() { let p = make_parser(); assert!(parse_as(&p, "RETURN;", "return-stmt").is_some()); } #[test] fn leave_stmt() { let p = make_parser(); assert!(parse_as(&p, "LEAVE;", "leave-stmt").is_some()); } #[test] fn iter_stmt() { let p = make_parser(); assert!(parse_as(&p, "ITER;", "iter-stmt").is_some()); } #[test] fn exsr_stmt() { let p = make_parser(); assert!(parse_as(&p, "EXSR myRoutine;", "exsr-stmt").is_some()); } #[test] fn read_stmt() { let p = make_parser(); assert!(parse_as(&p, "READ myFile;", "read-stmt").is_some()); } #[test] fn write_stmt() { let p = make_parser(); assert!(parse_as(&p, "WRITE outRec;", "write-stmt").is_some()); } #[test] fn chain_stmt() { let p = make_parser(); assert!(parse_as(&p, "CHAIN key myFile;", "chain-stmt").is_some()); } #[test] fn setll_start() { let p = make_parser(); assert!(parse_as(&p, "SETLL *START myFile;", "setll-stmt").is_some()); } #[test] fn open_close() { let p = make_parser(); assert!(parse_as(&p, "OPEN myFile;", "open-stmt").is_some()); assert!(parse_as(&p, "CLOSE myFile;", "close-stmt").is_some()); } #[test] fn close_all() { let p = make_parser(); assert!(parse_as(&p, "CLOSE *ALL;", "close-stmt").is_some()); } // ── Expressions ────────────────────────────────────────────────────────── #[test] fn expression_addition() { let p = make_parser(); assert!(parse_as(&p, "a+b", "expression").is_some()); } #[test] fn expression_comparison() { let p = make_parser(); assert!(parse_as(&p, "x=1", "expression").is_some()); assert!(parse_as(&p, "x<>0", "expression").is_some()); } #[test] fn expression_not() { let p = make_parser(); assert!(parse_as(&p, "NOT flag", "expression").is_some()); } #[test] fn expression_parenthesised() { let p = make_parser(); assert!(parse_as(&p, "(x+1)", "expression").is_some()); } // ── Built-in functions ─────────────────────────────────────────────────── #[test] fn builtin_len() { let p = make_parser(); assert!(parse_as(&p, "%LEN(myField)", "built-in-function").is_some()); } #[test] fn builtin_trim() { let p = make_parser(); assert!(parse_as(&p, "%TRIM(s)", "built-in-function").is_some()); } #[test] fn builtin_eof_no_arg() { let p = make_parser(); assert!(parse_as(&p, "%EOF()", "built-in-function").is_some()); } #[test] fn builtin_subst_3arg() { let p = make_parser(); assert!(parse_as(&p, "%SUBST(s:1:5)", "built-in-function").is_some()); } // ── Date / time formats ────────────────────────────────────────────────── #[test] fn date_formats() { let p = make_parser(); for fmt in &["*MDY", "*DMY", "*YMD", "*JUL", "*ISO", "*USA", "*EUR", "*JIS"] { assert!(parse_as(&p, fmt, "date-format").is_some(), "{} failed", fmt); } } #[test] fn time_formats() { let p = make_parser(); for fmt in &["*HMS", "*ISO", "*USA", "*EUR", "*JIS"] { assert!(parse_as(&p, fmt, "time-format").is_some(), "{} failed", fmt); } } // ── Qualified names ─────────────────────────────────────────────────────── #[test] fn qualified_name_simple() { let p = make_parser(); assert!(parse_as(&p, "myVar", "qualified-name").is_some()); } #[test] fn qualified_name_dotted() { let p = make_parser(); assert!(parse_as(&p, "ds.field", "qualified-name").is_some()); } // ── Duration codes ─────────────────────────────────────────────────────── #[test] fn duration_codes() { let p = make_parser(); for code in &["*YEARS", "*MONTHS", "*DAYS", "*HOURS", "*MINUTES", "*SECONDS", "*MSECONDS"] { assert!(parse_as(&p, code, "duration-code").is_some(), "{} failed", code); } } }