From 26b2e2f0f7e6937163e101871b2a6577faf42aed Mon Sep 17 00:00:00 2001 From: charles Date: Thu, 12 Mar 2026 20:55:01 -0700 Subject: [PATCH] add: bnf --- Cargo.lock | 329 ++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 702 +++++++++++++++++++++++++++++++++++++++++++++++++++- src/rpg.bnf | 693 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1723 insertions(+), 2 deletions(-) create mode 100644 src/rpg.bnf diff --git a/Cargo.lock b/Cargo.lock index 6b2839d..06d08dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,335 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "bnf" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b77b055f8cb1d566fa4ef55bc699f60eefb17927dc25fa454a05b6fabf7aa4" +dependencies = [ + "getrandom", + "hashbrown", + "nom", + "rand", + "serde", + "serde_json", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + [[package]] name = "rust-langrpg" version = "0.1.0" +dependencies = [ + "bnf", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index bb2f2ba..cff2a34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] +bnf = "0.6" diff --git a/src/main.rs b/src/main.rs index 623ab5f..62369d2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,701 @@ -fn main() { - println!("Hello world!") +//! 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); + } + } } diff --git a/src/rpg.bnf b/src/rpg.bnf new file mode 100644 index 0000000..60fc018 --- /dev/null +++ b/src/rpg.bnf @@ -0,0 +1,693 @@ + ::= ' ' | '\t' | '\n' | '\r' + ::= | + ::= | '' + + ::= + + ::= + | + | + + ::= + | + + ::= + | + | + | + | + | + | + + ::= + | + + ::= 'DCL-PROC' ';' 'END-PROC' ';' + | 'DCL-PROC' ';' 'END-PROC' ';' + + ::= + | + + ::= 'EXPORT' + | 'EXTPROC' '(' ')' + | 'EXTPROC' '(' '*CL' ':' ')' + | 'EXTPROC' '(' '*DCLCASE' ')' + + ::= + | + | + | + | + | + | '' + + ::= 'DCL-PI' ';' 'END-PI' ';' + | 'DCL-PI' ';' 'END-PI' ';' + | 'DCL-PI' ';' 'END-PI' ';' + | 'DCL-PI' ';' 'END-PI' ';' + | 'DCL-PI' '*N' ';' 'END-PI' ';' + | 'DCL-PI' '*N' ';' 'END-PI' ';' + | 'DCL-PI' '*N' ';' 'END-PI' ';' + | 'DCL-PI' '*N' ';' 'END-PI' ';' + + ::= + | + + ::= ';' + | ';' + + ::= + | + + ::= 'VALUE' + | 'CONST' + | 'OPTIONS' '(' '*NOPASS' ')' + | 'OPTIONS' '(' '*OMIT' ')' + | 'OPTIONS' '(' '*VARSIZE' ')' + + ::= 'BEG-SR' ';' 'END-SR' ';' + | 'BEG-SR' ';' 'END-SR' ';' + + ::= 'CTL-OPT' ';' + + ::= + | + + ::= 'DFTACTGRP' '(' '*YES' ')' + | 'DFTACTGRP' '(' '*NO' ')' + | 'ACTGRP' '(' '*CALLER' ')' + | 'ACTGRP' '(' '*NEW' ')' + | 'ACTGRP' '(' ')' + | 'BNDDIR' '(' ')' + | 'OPTION' '(' ')' + | 'DATFMT' '(' ')' + | 'TIMFMT' '(' ')' + | 'DECEDIT' '(' ')' + | 'ALWNULL' '(' '*USRCTL' ')' + | 'ALWNULL' '(' '*INPUTONLY' ')' + | 'ALWNULL' '(' '*NO' ')' + | 'DEBUG' '(' '*YES' ')' + | 'DEBUG' '(' '*NO' ')' + | 'EXPROPTS' '(' '*RESDECPOS' ')' + | 'MAIN' '(' ')' + | 'NOMAIN' + | 'COPYRIGHT' '(' ')' + | 'STGMDL' '(' '*SNGLVL' ')' + | 'STGMDL' '(' '*TERASPACE' ')' + | 'STGMDL' '(' '*INHERIT' ')' + | 'TEXT' '(' ')' + | 'TRUNCNBR' '(' '*YES' ')' + | 'TRUNCNBR' '(' '*NO' ')' + + ::= ':' + | + + ::= '*SRCSTMT' + | '*NODEBUGIO' + | '*NOUNREF' + | '*NOSHOWCPY' + + ::= 'DCL-F' ';' + | 'DCL-F' ';' + + ::= + | + + ::= 'KEYED' + | 'USAGE' '(' '*INPUT' ')' + | 'USAGE' '(' '*OUTPUT' ')' + | 'USAGE' '(' '*UPDATE' ':' '*DELETE' ')' + | 'USAGE' '(' '*UPDATE' ')' + | 'USAGE' '(' '*DELETE' ')' + | 'DISK' + | 'PRINTER' + | 'SEQ' + | 'RENAME' '(' ':' ')' + | 'QUALIFIED' + | 'TEMPLATE' + | 'EXTFILE' '(' ')' + | 'EXTFILE' '(' ')' + | 'EXTMBR' '(' ')' + | 'EXTMBR' '(' ')' + | 'EXTDESC' '(' ')' + | 'STATIC' + | 'INFDS' '(' ')' + | 'BLOCK' '(' '*YES' ')' + | 'BLOCK' '(' '*NO' ')' + + ::= 'DCL-S' ';' + | 'DCL-S' ';' + + ::= 'DCL-C' ';' + | 'DCL-C' 'CONST' '(' ')' ';' + + ::= 'DCL-C' ';' + + ::= '*ON' + | '*OFF' + | '*BLANKS' + | '*BLANK' + | '*ZEROS' + | '*ZERO' + | '*HIVAL' + | '*LOVAL' + | '*NULL' + + ::= 'DCL-DS' ';' 'END-DS' ';' + | 'DCL-DS' ';' 'END-DS' ';' + | 'DCL-DS' ';' 'END-DS' ';' + | 'DCL-DS' ';' 'END-DS' ';' + + ::= + | + + ::= 'QUALIFIED' + | 'TEMPLATE' + | 'LIKEDS' '(' ')' + | 'LIKEREC' '(' ':' ')' + | 'EXTNAME' '(' ':' ')' + | 'EXTNAME' '(' ')' + | 'BASED' '(' ')' + | 'INZ' + | 'INZ' '(' '*EXTDFT' ')' + | 'DIM' '(' ')' + | 'STATIC' + + ::= '*INPUT' + | '*OUTPUT' + | '*ALL' + | '*KEY' + | + + ::= + | + + ::= ';' + | ';' + | + + ::= + | + + ::= 'INZ' + | 'INZ' '(' ')' + | 'INZ' '(' ')' + | 'STATIC' + | 'BASED' '(' ')' + | 'DIM' '(' ')' + | 'LIKE' '(' ')' + | 'LIKEDS' '(' ')' + | 'ASCEND' + | 'DESCEND' + | 'ALTSEQ' + | 'OPDESC' + | 'NOOPT' + | 'VOLATILE' + + ::= 'CHAR' '(' ')' + | 'VARCHAR' '(' ')' + | 'GRAPH' '(' ')' + | 'VARGRAPH' '(' ')' + | 'UCS2' '(' ')' + | 'VARUCS2' '(' ')' + | 'PACKED' '(' ':' ')' + | 'ZONED' '(' ':' ')' + | 'BINDEC' '(' ':' ')' + | 'INT' '(' ')' + | 'UNS' '(' ')' + | 'FLOAT' '(' ')' + | 'DATE' + | 'DATE' '(' ')' + | 'TIME' + | 'TIME' '(' ')' + | 'TIMESTAMP' + | 'TIMESTAMP' '(' ')' + | 'IND' + | 'POINTER' + | 'POINTER' 'PROCPTR' + | 'OBJECT' '(' '*JAVA' ':' ')' + | 'LIKE' '(' ')' + | 'LIKEDS' '(' ')' + | 'LIKEREC' '(' ':' ')' + + ::= '*MDY' + | '*DMY' + | '*YMD' + | '*JUL' + | '*ISO' + | '*USA' + | '*EUR' + | '*JIS' + + ::= '*HMS' + | '*ISO' + | '*USA' + | '*EUR' + | '*JIS' + + ::= + | + + ::= + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + + ::= '=' ';' + | 'EVAL' '=' ';' + | 'EVAL' '(' ')' '=' ';' + | 'EVALR' '=' ';' + | 'EVAL-CORR' '=' ';' + + ::= 'H' + | 'T' + | 'E' + + ::= + | '(' ')' + + ::= 'IF' ';' 'ENDIF' ';' + | 'IF' ';' 'ENDIF' ';' + | 'IF' ';' 'ENDIF' ';' + | 'IF' ';' 'ENDIF' ';' + | 'IF' ';' 'ENDIF' ';' + | 'IF' ';' 'ENDIF' ';' + | 'IF' ';' 'ENDIF' ';' + + ::= + | + + ::= 'ELSEIF' ';' + | 'ELSEIF' ';' + + ::= 'ELSE' ';' + | 'ELSE' ';' + + ::= 'DOW' ';' 'ENDDO' ';' + | 'DOW' ';' 'ENDDO' ';' + + ::= 'DOU' ';' 'ENDDO' ';' + | 'DOU' ';' 'ENDDO' ';' + + ::= 'FOR' '=' 'TO' ';' 'ENDFOR' ';' + | 'FOR' '=' 'TO' 'BY' ';' 'ENDFOR' ';' + | 'FOR' '=' 'DOWNTO' ';' 'ENDFOR' ';' + | 'FOR' '=' 'DOWNTO' 'BY' ';' 'ENDFOR' ';' + | 'FOR' '=' 'TO' ';' 'ENDFOR' ';' + | 'FOR' '=' 'DOWNTO' ';' 'ENDFOR' ';' + + ::= 'SELECT' ';' 'ENDSL' ';' + | 'SELECT' ';' 'ENDSL' ';' + | 'SELECT' ';' 'ENDSL' ';' + + ::= + | + + ::= 'WHEN' ';' + | 'WHEN' ';' + + ::= 'OTHER' ';' + | 'OTHER' ';' + + ::= 'MONITOR' ';' 'ENDMON' ';' + | 'MONITOR' ';' 'ENDMON' ';' + + ::= + | + + ::= 'ON-ERROR' ';' + | 'ON-ERROR' ';' + | 'ON-ERROR' ';' + | 'ON-ERROR' ';' + + ::= ':' + | + + ::= + | '*PROGRAM' + | '*FILE' + | '*ALL' + + ::= 'CALLP' '(' ')' ';' + | 'CALLP' '(' ')' ';' + | 'CALLP' ';' + | '(' ')' ';' + | '(' ')' ';' + + ::= 'RETURN' ';' + | 'RETURN' ';' + + ::= 'LEAVE' ';' + + ::= 'ITER' ';' + + ::= 'LEAVESR' ';' + + ::= 'EXSR' ';' + + ::= 'READ' ';' + | 'READ' ';' + | 'READ' '(' 'N' ')' ';' + + ::= 'READP' ';' + | 'READP' ';' + + ::= 'READE' ';' + | 'READE' ';' + | 'READE' '(' 'N' ')' ';' + + ::= 'READPE' ';' + | 'READPE' ';' + + ::= 'WRITE' ';' + | 'WRITE' ';' + | 'WRITE' '(' 'E' ')' ';' + + ::= 'UPDATE' ';' + | 'UPDATE' ';' + | 'UPDATE' '(' 'E' ')' ';' + + ::= 'DELETE' ';' + | 'DELETE' '(' 'E' ')' ';' + | 'DELETE' ';' + + ::= 'CHAIN' ';' + | 'CHAIN' '(' 'N' ')' ';' + | 'CHAIN' '(' 'E' ')' ';' + + ::= 'SETLL' ';' + | 'SETLL' '*START' ';' + | 'SETLL' '*END' ';' + + ::= 'SETGT' ';' + | 'SETGT' '*START' ';' + | 'SETGT' '*END' ';' + + ::= 'OPEN' ';' + | 'OPEN' '(' 'E' ')' ';' + + ::= 'CLOSE' ';' + | 'CLOSE' '(' 'E' ')' ';' + | 'CLOSE' '*ALL' ';' + + ::= 'EXCEPT' ';' + | 'EXCEPT' ';' + + ::= 'EXFMT' ';' + | 'EXFMT' '(' 'E' ')' ';' + + ::= 'DSPLY' ';' + | 'DSPLY' '(' ':' ':' ')' ';' + | 'DSPLY' '(' ')' ';' + + ::= 'RESET' ';' + | 'RESET' '*ALL' ';' + + ::= 'CLEAR' ';' + + ::= 'SORTA' ';' + | 'SORTA' '(' 'A' ')' ';' + | 'SORTA' '(' 'D' ')' ';' + + ::= 'DEALLOC' ';' + | 'DEALLOC' '(' 'N' ')' ';' + + ::= 'DUMP' ';' + | 'DUMP' '(' 'A' ')' ';' + + ::= 'FORCE' ';' + + ::= 'POST' ';' + | 'POST' '(' 'E' ')' ';' + + ::= 'FEOD' ';' + + ::= 'UNLOCK' ';' + + ::= + + ::= + + ::= 'OR' + | + + ::= 'AND' + | + + ::= 'NOT' + | + + ::= '=' + | '<>' + | '>=' + | '<=' + | '>' + | '<' + | + + ::= '+' + | '-' + | + + ::= '**' + | '*' + | '/' + | + + ::= '-' + | '+' + | + + ::= + | + | + | + | '(' ')' + | '(' ')' + | '(' ')' + | + | '(' ')' + + ::= '*IN' '(' ')' + | '*IN' + | '*ON' + | '*OFF' + | '*BLANK' + | '*BLANKS' + | '*ZERO' + | '*ZEROS' + | '*HIVAL' + | '*LOVAL' + | '*NULL' + | '*ALL' + | '*OMIT' + | '*THIS' + | '*SAME' + | '*START' + | '*END' + + ::= '%ABS' '(' ')' + | '%ADDR' '(' ')' + | '%ALLOC' '(' ')' + | '%BITAND' '(' ':' ')' + | '%BITNOT' '(' ')' + | '%BITOR' '(' ':' ')' + | '%BITXOR' '(' ':' ')' + | '%CHAR' '(' ':' ')' + | '%CHAR' '(' ')' + | '%CHECK' '(' ':' ':' ')' + | '%CHECK' '(' ':' ')' + | '%CHECKR' '(' ':' ')' + | '%DATE' '(' ':' ')' + | '%DATE' '(' ')' + | '%DATE' '(' ')' + | '%DAYS' '(' ')' + | '%DEC' '(' ':' ':' ')' + | '%DECH' '(' ':' ':' ')' + | '%DECPOS' '(' ')' + | '%DIFF' '(' ':' ':' ')' + | '%DIV' '(' ':' ')' + | '%EDITC' '(' ':' ')' + | '%EDITFLT' '(' ')' + | '%EDITW' '(' ':' ')' + | '%ELEM' '(' ')' + | '%EOF' '(' ')' + | '%EOF' '(' ')' + | '%EQUAL' '(' ')' + | '%EQUAL' '(' ')' + | '%ERROR' '(' ')' + | '%FIELDS' '(' ')' + | '%FLOAT' '(' ')' + | '%FOUND' '(' ')' + | '%FOUND' '(' ')' + | '%GRAPH' '(' ')' + | '%HOURS' '(' ')' + | '%INT' '(' ')' + | '%INTH' '(' ')' + | '%KDS' '(' ':' ')' + | '%KDS' '(' ')' + | '%LEN' '(' ')' + | '%MINUTES' '(' ')' + | '%MONTHS' '(' ')' + | '%MSECONDS' '(' ')' + | '%NULLIND' '(' ')' + | '%OCCUR' '(' ')' + | '%OPEN' '(' ')' + | '%PADDR' '(' '*DCLCASE' ':' ')' + | '%PADDR' '(' ')' + | '%REALLOC' '(' ':' ')' + | '%REM' '(' ':' ')' + | '%REPLACE' '(' ':' ':' ':' ')' + | '%REPLACE' '(' ':' ':' ')' + | '%REPLACE' '(' ':' ')' + | '%SCAN' '(' ':' ':' ')' + | '%SCAN' '(' ':' ')' + | '%SCANR' '(' ':' ')' + | '%SECONDS' '(' ')' + | '%SHTDN' '(' ')' + | '%SIZE' '(' ')' + | '%SQRT' '(' ')' + | '%STATUS' '(' ')' + | '%STATUS' '(' ')' + | '%STR' '(' ':' ')' + | '%STR' '(' ')' + | '%SUBARR' '(' ':' ':' ')' + | '%SUBARR' '(' ':' ')' + | '%SUBST' '(' ':' ':' ')' + | '%SUBST' '(' ':' ')' + | '%THIS' '(' ')' + | '%TIME' '(' ')' + | '%TIME' '(' ')' + | '%TIMESTAMP' '(' ')' + | '%TIMESTAMP' '(' ')' + | '%TRIM' '(' ')' + | '%TRIML' '(' ')' + | '%TRIMR' '(' ')' + | '%UCS2' '(' ')' + | '%UNS' '(' ')' + | '%UNSH' '(' ')' + | '%XFOOT' '(' ')' + | '%XLATE' '(' ':' ':' ')' + | '%YEARS' '(' ')' + + ::= '*YEARS' + | '*MONTHS' + | '*DAYS' + | '*HOURS' + | '*MINUTES' + | '*SECONDS' + | '*MSECONDS' + + ::= ':' + | + + ::= ':' + | + + ::= + | '*OMIT' + + ::= ':' + | + + ::= '.' + | + + ::= + | + | + | + | + + ::= "'" "'" + + ::= + | '' + + ::= + | + | + | "''" + + ::= 'X' "'" "'" + + ::= + | + + ::= + | 'A' + | 'B' + | 'C' + | 'D' + | 'E' + | 'F' + + ::= '.' + | '.' + + ::= + + ::= + | + + ::= '*ON' + | '*OFF' + + ::= + | + + ::= + | + + ::= + | + | '_' + + ::= 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' + | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' + | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' + | 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' + | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' + | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' + | '@' | '#' | '$' + + ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + + ::= ' ' | '!' | '"' | '#' | '$' | '%' | '&' | '(' + | ')' | '*' | '+' | ',' | '-' | '.' | '/' | ':' + | '<' | '=' | '>' | '?' | '@' | '[' | '\\' + | ']' | '^' | '_' | '`' | '{' | '|' | '}' | '~'