diff --git a/Cargo.lock b/Cargo.lock index 073b40c..f51d96f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # 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 = "anstream" version = "1.0.0" @@ -64,26 +58,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[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 = "cc" version = "1.2.56" @@ -94,12 +68,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - [[package]] name = "clap" version = "4.6.0" @@ -152,49 +120,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" -[[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 = "heck" version = "0.5.0" @@ -231,22 +162,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[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 = "lazy_static" version = "1.5.0" @@ -273,21 +188,6 @@ dependencies = [ "semver", ] -[[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" @@ -300,15 +200,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[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" @@ -327,41 +218,6 @@ 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 = "regex-lite" version = "0.1.9" @@ -376,67 +232,17 @@ version = "0.1.0" name = "rust-langrpg" version = "0.1.0" dependencies = [ - "bnf", "clap", "either", "inkwell", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" -[[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 = "shlex" version = "1.3.0" @@ -492,60 +298,6 @@ 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" -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 = "windows-link" version = "0.2.1" @@ -560,35 +312,3 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] - -[[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 f59912f..9967c21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,10 +23,6 @@ default-run = "rust-langrpg" name = "rust-langrpg" path = "src/main.rs" -[[bin]] -name = "demo" -path = "src/bin/demo.rs" - # ───────────────────────────────────────────────────────────────────────────── # Library (rlib — used by the binaries and tests) # ───────────────────────────────────────────────────────────────────────────── @@ -41,7 +37,6 @@ crate-type = ["rlib"] # ───────────────────────────────────────────────────────────────────────────── [dependencies] -bnf = "0.6" clap = { version = "4", features = ["derive"] } either = "1" inkwell = { version = "0.8", features = ["llvm21-1"] } diff --git a/README.md b/README.md index 6bdba8b..a67554f 100644 --- a/README.md +++ b/README.md @@ -12,42 +12,24 @@ Language reference: https://www.ibm.com/docs/en/i/7.5.0?topic=introduction-overv cargo build --release ``` -### Running - -The compiler ships as a standalone binary that loads the embedded BNF grammar, builds a parser, and runs a suite of RPG IV snippet examples to demonstrate the grammar in action: +### Compiling an RPG IV program ```rust-langrpg/README.md -cargo run --bin demo +cargo run --release -- -o hello hello.rpg +./hello ``` You will see output similar to: ```rust-langrpg/README.md -=== RPG IV Free-Format Parser === - -[grammar] Loaded successfully. -[parser] Built successfully (all non-terminals resolved). - -=== Parsing Examples === - - ┌─ simple identifier (identifier) ───────────────────── - │ source : "myVar" - │ result : OK - └────────────────────────────────────────────── -... -=== Summary === - total : 42 - matched : 42 - failed : 0 - -All examples parsed successfully. +DSPLY Hello, World! ``` ### Hello World in RPG IV -The following is a complete Hello World program written in RPG IV free-format syntax, as understood by this parser: +The following is a complete Hello World program written in RPG IV free-format syntax: -hello.rpg: +`hello.rpg`: ```rust-langrpg/README.md CTL-OPT DFTACTGRP(*NO); @@ -68,10 +50,22 @@ Breaking it down: - `DSPLY greeting;` — displays the value of `greeting` to the operator message queue - `RETURN;` — returns from the procedure -To validate this program, execute the compiler to build the data: +### Compiler options -```sh -cargo run --release -- -o main hello.rpg +``` +rust-langrpg [OPTIONS] ... + +Arguments: + ... RPG IV source file(s) to compile + +Options: + -o Output executable path [default: a.out] + --emit-ir Print LLVM IR to stdout instead of producing a binary + -O Optimisation level 0-3 [default: 0] + --no-link Produce a .o object file, skip linking + --runtime Path to librpgrt.so [default: auto-detect] + -h, --help Print help + -V, --version Print version ``` ## Architecture @@ -90,38 +84,28 @@ RPG IV source (.rpg) │ ▼ ┌─────────────────────────────────────────┐ -│ 1. BNF validation (bnf crate) │ -│ src/rpg.bnf — embedded at compile │ -│ time via include_str! │ -└────────────────┬────────────────────────┘ - │ parse tree (validation only) - ▼ -┌─────────────────────────────────────────┐ -│ 2. Lowering pass (src/lower.rs) │ -│ Hand-written recursive-descent │ -│ tokenizer + parser → typed AST │ +│ 1. Parsing + lowering (src/lower.rs) │ +│ Hand-written tokenizer + │ +│ recursive-descent parser │ +│ → typed AST (src/ast.rs) │ └────────────────┬────────────────────────┘ │ ast::Program ▼ ┌─────────────────────────────────────────┐ -│ 3. LLVM code generation (src/codegen.rs│ +│ 2. LLVM code generation (src/codegen.rs│ │ inkwell bindings → LLVM IR module │ └────────────────┬────────────────────────┘ │ .o object file ▼ ┌─────────────────────────────────────────┐ -│ 4. Linking (cc + librpgrt.so) │ +│ 3. Linking (cc + librpgrt.so) │ │ Produces a standalone Linux ELF │ └─────────────────────────────────────────┘ ``` -### Stage 1 — BNF validation (`src/rpg.bnf` + `bnf` crate) +### Stage 1 — Parsing and lowering to a typed AST (`src/lower.rs`) -The RPG IV free-format grammar is encoded in BNF notation in `src/rpg.bnf` and embedded at compile time with `include_str!`. At startup the compiler parses the grammar with the [`bnf`](https://docs.rs/bnf/latest/bnf/) crate to build a `GrammarParser`. Each source file is validated against the top-level `` rule before any further processing. This stage acts as a gate: malformed source is rejected early with a clear parse error. - -### Stage 2 — Lowering to a typed AST (`src/lower.rs`) - -The BNF parser only validates structure; it does not produce a typed tree suitable for code generation. A hand-written tokenizer and recursive-descent parser in `lower.rs` converts the raw source text into the typed `Program` AST defined in `src/ast.rs`. +A hand-written tokenizer and recursive-descent parser converts the raw source text directly into the typed `Program` AST defined in `src/ast.rs`. RPG IV keywords are case-insensitive and the parser handles mixed-case source naturally. The AST covers the full language surface that the compiler handles: @@ -133,7 +117,7 @@ The AST covers the full language surface that the compiler handles: Unrecognised constructs produce `Statement::Unimplemented` or placeholder declaration variants rather than hard errors, so the compiler continues to lower the parts it understands. -### Stage 3 — LLVM code generation (`src/codegen.rs`) +### Stage 2 — LLVM code generation (`src/codegen.rs`) The typed `Program` is handed to the code generator, which uses [`inkwell`](https://crates.io/crates/inkwell) (safe Rust bindings to LLVM 21) to build an LLVM IR module: @@ -147,7 +131,7 @@ The typed `Program` is handed to the code generator, which uses [`inkwell`](http The module is then compiled to a native `.o` object file for the host target via LLVM's target machine API, with optional optimisation passes (`-O0` through `-O3`). -### Stage 4 — Linking +### Stage 3 — Linking The object file is linked into a standalone ELF executable by invoking the system C compiler (`cc`). The executable is linked against `librpgrt.so`. @@ -180,17 +164,14 @@ DSPLY Hello, World! ``` rust-langrpg/ ├── src/ -│ ├── rpg.bnf — RPG IV free-format BNF grammar (embedded at compile time) -│ ├── lib.rs — Grammar loader and demo helpers +│ ├── lib.rs — Library root (re-exports ast, lower, codegen) │ ├── ast.rs — Typed AST node definitions -│ ├── lower.rs — Tokenizer + recursive-descent lowering pass +│ ├── lower.rs — Tokenizer + recursive-descent parser + lowering pass │ ├── codegen.rs — LLVM IR code generation (inkwell) -│ ├── main.rs — Compiler CLI (clap) + linker invocation -│ └── bin/ -│ └── demo.rs — Grammar demo binary +│ └── main.rs — Compiler CLI (clap) + linker invocation ├── rpgrt/ │ └── src/ │ └── lib.rs — Runtime library (librpgrt.so) ├── hello.rpg — Hello World example program -└── count.rpg — Counting loop example program +└── fib.rpg — Fibonacci sequence example program ``` diff --git a/src/bin/demo.rs b/src/bin/demo.rs deleted file mode 100644 index 59a55e6..0000000 --- a/src/bin/demo.rs +++ /dev/null @@ -1,318 +0,0 @@ -//! 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 index 93b6327..121681f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,404 +1,9 @@ //! 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. -//! -//! Also provides the typed AST ([`ast`]), BNF-to-AST lowering pass ([`lower`]), -//! and LLVM code-generator ([`codegen`]) used by the compiler pipeline. +//! Provides the typed AST ([`ast`]), recursive-descent parser and lowering +//! pass ([`lower`]), and LLVM code-generator ([`codegen`]) used by the +//! compiler pipeline. pub mod ast; pub mod lower; pub mod codegen; - -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 56ecbdb..da57ac9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,10 +2,9 @@ //! //! Full compilation pipeline: //! source (.rpg) -//! → BNF validation (bnf crate) -//! → AST lowering (lower.rs) -//! → LLVM IR / object (codegen.rs via inkwell) -//! → native executable (cc linker + librpgrt.so runtime) +//! → recursive-descent parser / AST lowering (lower.rs) +//! → LLVM IR / object (codegen.rs via inkwell) +//! → native executable (cc linker + librpgrt.so runtime) //! //! ## Usage //! @@ -18,7 +17,6 @@ //! Options: //! -o Output executable path [default: a.out] //! --emit-ir Print LLVM IR to stdout instead of producing a binary -//! --emit-tree Print BNF parse tree to stdout instead of compiling //! -O Optimisation level 0-3 [default: 0] //! --no-link Produce a .o object file, skip linking //! --runtime Path to librpgrt.so [default: auto-detect] @@ -41,98 +39,7 @@ use std::{ }; use clap::Parser as ClapParser; -use rust_langrpg::{codegen, load_grammar, lower::lower, parse_as}; - -// ───────────────────────────────────────────────────────────────────────────── -// BNF pre-processing helper -// ───────────────────────────────────────────────────────────────────────────── - -/// Uppercase all keyword-like tokens in `source` while preserving the content -/// of string literals, line comments, and block comments unchanged. -/// -/// This lets the BNF grammar (which uses uppercase terminal literals) validate -/// RPG IV source that uses mixed-case keywords such as `Ctl-Opt` or `Dcl-S`. -fn uppercase_keywords_for_bnf(source: &str) -> String { - let chars: Vec = source.chars().collect(); - let mut out = String::with_capacity(source.len()); - let mut i = 0; - - while i < chars.len() { - // Line comment // … \n — copy verbatim - if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '/' { - while i < chars.len() && chars[i] != '\n' { - out.push(chars[i]); - i += 1; - } - continue; - } - - // Block comment /* … */ — copy verbatim - if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' { - out.push(chars[i]); - out.push(chars[i + 1]); - i += 2; - while i + 1 < chars.len() { - if chars[i] == '*' && chars[i + 1] == '/' { - out.push(chars[i]); - out.push(chars[i + 1]); - i += 2; - break; - } - out.push(chars[i]); - i += 1; - } - continue; - } - - // String literal '…' — copy verbatim (including '' escape) - if chars[i] == '\'' { - out.push(chars[i]); - i += 1; - while i < chars.len() { - if chars[i] == '\'' { - out.push(chars[i]); - i += 1; - // '' is an escaped quote — keep going - if i < chars.len() && chars[i] == '\'' { - out.push(chars[i]); - i += 1; - } else { - break; - } - } else { - out.push(chars[i]); - i += 1; - } - } - continue; - } - - // Identifier / keyword — uppercase it so the BNF terminals match - if chars[i].is_alphabetic() || chars[i] == '_' || chars[i] == '@' || chars[i] == '#' || chars[i] == '$' { - while i < chars.len() - && (chars[i].is_alphanumeric() - || chars[i] == '_' - || chars[i] == '@' - || chars[i] == '#' - || chars[i] == '$' - || (chars[i] == '-' - && i + 1 < chars.len() - && chars[i + 1].is_alphabetic())) - { - out.push(chars[i].to_ascii_uppercase()); - i += 1; - } - continue; - } - - // Everything else (operators, punctuation, whitespace, digits) - out.push(chars[i]); - i += 1; - } - - out -} +use rust_langrpg::{codegen, lower::lower}; // ───────────────────────────────────────────────────────────────────────────── // CLI definition @@ -161,10 +68,6 @@ struct Cli { #[arg(long = "emit-ir")] emit_ir: bool, - /// Emit the BNF parse tree to stdout instead of compiling. - #[arg(long = "emit-tree")] - emit_tree: bool, - /// Optimisation level: 0 = none, 1 = less, 2 = default, 3 = aggressive. #[arg(short = 'O', default_value = "0", value_name = "LEVEL")] opt_level: u8, @@ -186,24 +89,6 @@ struct Cli { fn main() { let cli = Cli::parse(); - // ── Load and build the BNF grammar ─────────────────────────────────────── - let grammar = match load_grammar() { - Ok(g) => g, - Err(e) => { - eprintln!("error: failed to load RPG IV grammar: {e}"); - process::exit(1); - } - }; - - let bnf_parser = match grammar.build_parser() { - Ok(p) => p, - Err(e) => { - eprintln!("error: failed to build BNF parser: {e}"); - process::exit(1); - } - }; - - // ── Process each source file ───────────────────────────────────────────── let mut any_error = false; for source_path in &cli.sources { @@ -216,100 +101,11 @@ fn main() { } }; - // ── BNF validation ──────────────────────────────────────────────────── - // RPG IV keywords are case-insensitive, but the BNF grammar uses - // uppercase terminal literals. Normalise the source before checking. - let normalised = uppercase_keywords_for_bnf(source_text.trim()); - let tree_opt = parse_as(&bnf_parser, normalised.trim(), "program") - .or_else(|| parse_as(&bnf_parser, normalised.trim(), "source-file")); - - if tree_opt.is_none() { - // BNF validation is a structural sanity-check. Emit a warning so - // the developer knows something looks off, but continue with the - // lowering pass which is more permissive and gives better errors. - eprintln!( - "warning: '{}' did not fully match the RPG IV grammar — \ - attempting to compile anyway", - source_path.display() - ); - - // ── Helpful diagnostics ────────────────────────────────────────── - // Scan for the first line the BNF cannot classify to give the user - // a concrete hint about what caused the mismatch. - let top_level_rules = &[ - "control-spec", - "standalone-decl", - "constant-decl", - "data-structure-decl", - "file-decl", - "procedure", - "subroutine", - "statement", - ]; - 'outer: for (lineno, raw_line) in source_text.lines().enumerate() { - let trimmed = raw_line.trim(); - let norm_check = trimmed.to_ascii_uppercase(); - // Skip blanks, comments, compiler directives, and lines that - // introduce multi-line constructs (DCL-PROC, END-PROC, DCL-DS, - // END-DS, DCL-PI, END-PI, BEG-SR, END-SR) — these will never - // match a single-line grammar rule and are not errors. - if trimmed.is_empty() - || trimmed.starts_with("//") - || trimmed.starts_with("/*") - || trimmed.starts_with("**") - || norm_check.starts_with("DCL-PROC") - || norm_check.starts_with("END-PROC") - || norm_check.starts_with("DCL-DS") - || norm_check.starts_with("END-DS") - || norm_check.starts_with("DCL-PI") - || norm_check.starts_with("END-PI") - || norm_check.starts_with("BEG-SR") - || norm_check.starts_with("END-SR") - { - continue; - } - // Strip inline line comments before BNF matching so that - // `fib(1) = 0; // some comment` doesn't cause a false positive. - let trimmed_no_comment = if let Some(idx) = trimmed.find("//") { - trimmed[..idx].trim_end() - } else { - trimmed - }; - let norm_line = uppercase_keywords_for_bnf(trimmed_no_comment); - let mut matched = false; - for rule in top_level_rules { - if parse_as(&bnf_parser, norm_line.trim(), rule).is_some() { - matched = true; - break; - } - } - if !matched { - eprintln!( - " hint (line {}): unrecognised grammar construct: {:?}", - lineno + 1, - if trimmed.len() > 80 { &trimmed[..80] } else { trimmed } - ); - break 'outer; - } - } - // Fall through — try lowering anyway. - } - - // ── --emit-tree: print parse tree and stop ──────────────────────────── - if cli.emit_tree { - println!("=== {} ===", source_path.display()); - println!("{}", tree_opt.unwrap()); - eprintln!("ok: {} (parse tree emitted)", source_path.display()); - continue; - } - - eprintln!("ok: {} (BNF valid)", source_path.display()); - // ── Lower to typed AST ──────────────────────────────────────────────── let program = match lower(source_text.trim()) { Ok(p) => p, Err(e) => { - eprintln!("error: lowering '{}' failed: {e}", source_path.display()); + eprintln!("error: '{}': {e}", source_path.display()); any_error = true; continue; } @@ -519,7 +315,7 @@ fn find_runtime(explicit: Option<&std::path::Path>) -> Option { } // ───────────────────────────────────────────────────────────────────────────── -// Integration smoke test (compile-time only — no process spawning needed) +// Integration smoke tests (compile-time only — no process spawning needed) // ───────────────────────────────────────────────────────────────────────────── #[cfg(test)] diff --git a/src/rpg.bnf b/src/rpg.bnf deleted file mode 100644 index 8a09371..0000000 --- a/src/rpg.bnf +++ /dev/null @@ -1,703 +0,0 @@ - ::= ' ' | ' ' | ' -' | ' -' - ::= | - ::= | '' - - ::= - | - | - - ::= '**FREE' - | '**free' - | '**Free' - - ::= - | - | - - ::= - | - - ::= - | - | - | - | - | - | - - ::= - | - - ::= 'DCL-PROC' ';' 'END-PROC' ';' - | 'DCL-PROC' ';' 'END-PROC' ';' - | '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' - - ::= ' ' | '!' | '"' | '#' | '$' | '%' | '&' | '(' - | ')' | '*' | '+' | ',' | '-' | '.' | '/' | ':' - | '<' | '=' | '>' | '?' | '@' | '[' | '\\' - | ']' | '^' | '_' | '`' | '{' | '|' | '}' | '~' diff --git a/tests/hello_rpg.rs b/tests/hello_rpg.rs index ebb872a..928a49a 100644 --- a/tests/hello_rpg.rs +++ b/tests/hello_rpg.rs @@ -1,7 +1,7 @@ //! Integration tests for the compiler binary against the Hello World program. //! //! These tests exercise the full compilation pipeline: -//! hello.rpg → BNF validation → AST lowering → LLVM codegen → native binary +//! hello.rpg → recursive-descent parser → AST lowering → LLVM codegen → native binary use std::process::Command; @@ -71,7 +71,7 @@ fn hello_rpg_produces_output_file() { } /// The compiler must print the file name to stderr with an "ok:" prefix when -/// BNF validation succeeds. +/// compilation succeeds. #[test] fn hello_rpg_reports_ok_on_stderr() { let out_path = std::env::temp_dir().join("hello_rpg_reports_ok.out"); @@ -121,33 +121,6 @@ fn hello_rpg_emit_ir() { ); } -/// `--emit-tree` must print the BNF parse tree to stdout and exit 0. -/// -/// The tree must mention `program` (the top-level grammar rule). -#[test] -fn hello_rpg_emit_tree() { - let out = run(&["--emit-tree", HELLO_RPG]); - - assert!( - out.status.success(), - "expected exit 0 with --emit-tree\nstderr: {}", - String::from_utf8_lossy(&out.stderr), - ); - - let tree = String::from_utf8_lossy(&out.stdout); - - assert!( - !tree.trim().is_empty(), - "--emit-tree output is empty — expected a parse tree", - ); - - assert!( - tree.contains("program"), - "--emit-tree output should reference the rule\n{}", - &tree[..tree.len().min(1000)], - ); -} - /// `--no-link` should produce a `.o` object file and exit 0. #[test] fn hello_rpg_no_link_produces_object() {