From 90de2206db53fab7be05cb4a9b09c6295b97c7ef Mon Sep 17 00:00:00 2001 From: charles Date: Thu, 12 Mar 2026 21:09:49 -0700 Subject: [PATCH] add: main function --- Cargo.lock | 142 +++++++++ Cargo.toml | 10 + README.md | 4 + hello.rpg | 8 + src/bin/demo.rs | 318 +++++++++++++++++++ src/lib.rs | 397 +++++++++++++++++++++++ src/main.rs | 771 ++++++--------------------------------------- src/rpg.bnf | 3 +- tests/hello_rpg.rs | 80 +++++ 9 files changed, 1062 insertions(+), 671 deletions(-) create mode 100644 hello.rpg create mode 100644 src/bin/demo.rs create mode 100644 src/lib.rs create mode 100644 tests/hello_rpg.rs diff --git a/Cargo.lock b/Cargo.lock index 06d08dc..e4ce1de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,56 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "bnf" version = "0.6.0" @@ -34,6 +84,52 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "equivalent" version = "1.0.2" @@ -71,6 +167,18 @@ dependencies = [ "foldhash", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.17" @@ -114,6 +222,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -181,6 +295,7 @@ name = "rust-langrpg" version = "0.1.0" dependencies = [ "bnf", + "clap", ] [[package]] @@ -232,6 +347,12 @@ dependencies = [ "zmij", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.117" @@ -249,6 +370,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "wasip2" version = "1.0.2+wasi-0.2.9" @@ -303,6 +430,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index cff2a34..88e3bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,16 @@ name = "rust-langrpg" version = "0.1.0" edition = "2024" +default-run = "rust-langrpg" + +[[bin]] +name = "rust-langrpg" +path = "src/main.rs" + +[[bin]] +name = "demo" +path = "src/bin/demo.rs" [dependencies] bnf = "0.6" +clap = { version = "4", features = ["derive"] } diff --git a/README.md b/README.md index e2f5948..d0d05db 100644 --- a/README.md +++ b/README.md @@ -77,3 +77,7 @@ cargo run --release -- -o main hello.rpg ## Implementation The RPG language was converted to an BNF, and fed into the bnf crate (https://docs.rs/bnf/latest/bnf/). + +The parse tree generated here is then given to LLVM, via Inkwell (https://crates.io/crates/inkwell) to create executable binaries. + +The binary can run standalone on a Linux system. Only the built-in function make hello world are implemented, and link to functions writen in Rust that are available as a shared library. diff --git a/hello.rpg b/hello.rpg new file mode 100644 index 0000000..456684f --- /dev/null +++ b/hello.rpg @@ -0,0 +1,8 @@ +CTL-OPT DFTACTGRP(*NO); + +DCL-S greeting CHAR(25) INZ('Hello, World!'); + +DCL-PROC main EXPORT; + DSPLY greeting; + RETURN; +END-PROC; diff --git a/src/bin/demo.rs b/src/bin/demo.rs new file mode 100644 index 0000000..59a55e6 --- /dev/null +++ b/src/bin/demo.rs @@ -0,0 +1,318 @@ +//! demo — runs the built-in RPG IV snippet suite and prints results. +//! +//! ``` +//! cargo run --bin demo +//! ``` + +use bnf::Term; +use rust_langrpg::{load_grammar, run_example, Example}; + +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 ───────────────────────────────────────────────── + 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 { + eprintln!("{} example(s) did not parse — check the BNF or the snippet.", bad); + std::process::exit(1); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..70b1fd2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,397 @@ +//! rust-langrpg — RPG IV free-format parser library +//! +//! Loads the BNF grammar embedded at compile time, builds a [`bnf::GrammarParser`], +//! and exposes helpers used by both the compiler binary and the demo binary. + +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 +// ───────────────────────────────────────────────────────────────────────────── + +pub struct Example { + pub label: &'static str, + pub rule: &'static str, + pub src: &'static str, +} + +pub 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) => { + let preview: String = tree.lines().take(6).collect::>().join("\n │ "); + println!(" │ result : OK"); + println!(" │ tree : {}", preview); + } + None => println!(" │ result : NO PARSE"), + } + println!(" └──────────────────────────────────────────────"); + println!(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// 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); + } + } + + // ── Hello World program ─────────────────────────────────────────────────── + + /// The Hello World source from the README, embedded at compile time so the + /// test works regardless of the working directory. + const HELLO_RPG: &str = include_str!("../hello.rpg"); + + /// Verify that the full hello.rpg program parses against the top-level + /// `` grammar rule. + #[test] + fn hello_rpg_parses() { + let p = make_parser(); + let tree = parse_as(&p, HELLO_RPG.trim(), "program"); + assert!( + tree.is_some(), + "hello.rpg did not match the rule\n\nsource:\n{}", + HELLO_RPG, + ); + } + + +} diff --git a/src/main.rs b/src/main.rs index 62369d2..9fe8c7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,701 +1,132 @@ -//! rust-langrpg — RPG IV free-format parser +//! rust-langrpg — RPG IV compiler CLI //! -//! Loads the BNF grammar embedded at compile time, builds a [`bnf::GrammarParser`], -//! and demonstrates parsing a handful of RPG IV free-format snippets. +//! Parses one or more RPG IV source files using the embedded BNF grammar +//! and optionally writes the resulting parse tree to an output file. +//! +//! ## Usage +//! +//! ```text +//! rust-langrpg [OPTIONS] ... +//! +//! Arguments: +//! ... RPG IV source file(s) to parse +//! +//! Options: +//! -o Write the parse tree to this file +//! -h, --help Print help +//! -V, --version Print version +//! ``` +//! +//! ## Example +//! +//! ```text +//! cargo run --release -- -o out.txt hello.rpg +//! ``` -use bnf::{Grammar, Term}; +use std::{ + fs, + io::{self, Write}, + path::PathBuf, + process, +}; -/// The RPG IV BNF grammar, embedded at compile time from `src/rpg.bnf`. -const RPG_BNF: &str = include_str!("rpg.bnf"); +use clap::Parser; +use rust_langrpg::{load_grammar, parse_as}; // ───────────────────────────────────────────────────────────────────────────── -// Public API +// CLI definition // ───────────────────────────────────────────────────────────────────────────── -/// Load and validate the RPG IV grammar. -/// -/// Returns `Err` if the BNF source is malformed. -pub fn load_grammar() -> Result { - RPG_BNF.parse::() -} +/// RPG IV free-format compiler — parses source files and emits parse trees. +#[derive(Parser, Debug)] +#[command(name = "rust-langrpg", version, about, long_about = None)] +struct Cli { + /// RPG IV source file(s) to parse. + #[arg(required = true, value_name = "SOURCES")] + sources: Vec, -/// 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()) + /// Write the parse tree(s) to this file. + /// If omitted the tree is not printed. + #[arg(short = 'o', value_name = "OUTPUT")] + output: Option, } // ───────────────────────────────────────────────────────────────────────────── -// 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 +// Entry point // ───────────────────────────────────────────────────────────────────────────── fn main() { - // ── 1. Load grammar ──────────────────────────────────────────────────── - println!("=== RPG IV Free-Format Parser ==="); - println!(); + let cli = Cli::parse(); + // ── Load grammar ───────────────────────────────────────────────────────── let grammar = match load_grammar() { - Ok(g) => { - println!("[grammar] Loaded successfully."); - g - } + Ok(g) => g, Err(e) => { - eprintln!("[grammar] Failed to parse BNF: {}", e); - std::process::exit(1); + eprintln!("error: failed to load RPG IV grammar: {e}"); + process::exit(1); } }; - // ── 2. Build parser ──────────────────────────────────────────────────── + // ── Build parser ───────────────────────────────────────────────────────── let parser = match grammar.build_parser() { - Ok(p) => { - println!("[parser] Built successfully (all non-terminals resolved)."); - p - } + Ok(p) => p, Err(e) => { - eprintln!("[parser] Failed to build: {}", e); - std::process::exit(1); + eprintln!("error: failed to build parser: {e}"); + 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; + // ── Open output sink ────────────────────────────────────────────────────── + // `output` is Box so we can use either a file or a sink that + // discards everything when -o was not supplied. + let mut output: Box = match &cli.output { + Some(path) => { + let file = fs::File::create(path).unwrap_or_else(|e| { + eprintln!("error: cannot open output file '{}': {e}", path.display()); + process::exit(1); + }); + Box::new(io::BufWriter::new(file)) } + None => Box::new(io::sink()), + }; - run_example(&parser, ex); + // ── Process each source file ────────────────────────────────────────────── + let mut any_error = false; + + for path in &cli.sources { + let source = match fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + eprintln!("error: cannot read '{}': {e}", path.display()); + any_error = true; + continue; + } + }; + + // Try the top-level "program" rule first; fall back to "source-file" + // so the binary is useful even if only one of those rule names exists + // in the grammar. + let tree = parse_as(&parser, source.trim(), "program") + .or_else(|| parse_as(&parser, source.trim(), "source-file")); + + match tree { + Some(t) => { + eprintln!("ok: {}", path.display()); + writeln!(output, "=== {} ===", path.display()) + .and_then(|_| writeln!(output, "{t}")) + .unwrap_or_else(|e| { + eprintln!("error: write failed: {e}"); + any_error = true; + }); + } + None => { + eprintln!("error: '{}' did not match the RPG IV grammar", path.display()); + any_error = true; + } + } } - // ── 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); - } + if any_error { + process::exit(1); } } diff --git a/src/rpg.bnf b/src/rpg.bnf index 60fc018..be4a391 100644 --- a/src/rpg.bnf +++ b/src/rpg.bnf @@ -1,4 +1,5 @@ - ::= ' ' | '\t' | '\n' | '\r' + ::= ' ' | ' ' | ' +' | ' ' ::= | ::= | '' diff --git a/tests/hello_rpg.rs b/tests/hello_rpg.rs new file mode 100644 index 0000000..4f31361 --- /dev/null +++ b/tests/hello_rpg.rs @@ -0,0 +1,80 @@ +//! Integration tests for the compiler binary against the Hello World program. + +use std::process::Command; + +/// `CARGO_BIN_EXE_rust-langrpg` is injected by Cargo for integration tests and +/// always points at the freshly-built binary under `target/`. +const BIN: &str = env!("CARGO_BIN_EXE_rust-langrpg"); + +/// Absolute path to hello.rpg, resolved at compile time relative to the crate +/// root so the test works regardless of the working directory. +const HELLO_RPG: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/hello.rpg"); + +// ───────────────────────────────────────────────────────────────────────────── +// Helper +// ───────────────────────────────────────────────────────────────────────────── + +fn run(args: &[&str]) -> std::process::Output { + Command::new(BIN) + .args(args) + .output() + .unwrap_or_else(|e| panic!("failed to spawn '{}': {e}", BIN)) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Tests +// ───────────────────────────────────────────────────────────────────────────── + +/// The compiler should exit 0 when given hello.rpg (no -o flag — tree is +/// discarded but the parse must still succeed). +#[test] +fn hello_rpg_exits_ok() { + let out = run(&[HELLO_RPG]); + assert!( + out.status.success(), + "expected exit 0 for hello.rpg\nstderr: {}", + String::from_utf8_lossy(&out.stderr), + ); +} + +/// When -o is supplied the output file must be created and contain a non-empty +/// parse tree. +#[test] +fn hello_rpg_writes_tree() { + let out_path = std::env::temp_dir().join("hello_rpg_test_tree.txt"); + + let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]); + + assert!( + out.status.success(), + "compiler failed with -o flag\nstderr: {}", + String::from_utf8_lossy(&out.stderr), + ); + + let tree = std::fs::read_to_string(&out_path) + .unwrap_or_else(|e| panic!("could not read output file '{}': {e}", out_path.display())); + + assert!( + !tree.trim().is_empty(), + "output file is empty — expected a parse tree", + ); + + // The tree should reference at least the top-level non-terminal. + assert!( + tree.contains("program"), + "parse tree does not mention :\n{tree}", + ); +} + +/// The compiler must print the file name to stderr as "ok: hello.rpg" (or the +/// full path) when the parse succeeds. +#[test] +fn hello_rpg_reports_ok_on_stderr() { + let out = run(&[HELLO_RPG]); + + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.starts_with("ok:"), + "expected stderr to start with 'ok:'\ngot: {stderr}", + ); +}