add: main function
This commit is contained in:
142
Cargo.lock
generated
142
Cargo.lock
generated
@@ -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"
|
||||
|
||||
10
Cargo.toml
10
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"] }
|
||||
|
||||
@@ -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.
|
||||
|
||||
8
hello.rpg
Normal file
8
hello.rpg
Normal file
@@ -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;
|
||||
318
src/bin/demo.rs
Normal file
318
src/bin/demo.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
397
src/lib.rs
Normal file
397
src/lib.rs
Normal file
@@ -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<Grammar, bnf::Error> {
|
||||
RPG_BNF.parse::<Grammar>()
|
||||
}
|
||||
|
||||
/// 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<String> {
|
||||
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::<Vec<_>>().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
|
||||
/// `<program>` 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 <program> rule\n\nsource:\n{}",
|
||||
HELLO_RPG,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
765
src/main.rs
765
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] <SOURCES>...
|
||||
//!
|
||||
//! Arguments:
|
||||
//! <SOURCES>... RPG IV source file(s) to parse
|
||||
//!
|
||||
//! Options:
|
||||
//! -o <OUTPUT> 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<Grammar, bnf::Error> {
|
||||
RPG_BNF.parse::<Grammar>()
|
||||
}
|
||||
/// 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<PathBuf>,
|
||||
|
||||
/// 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<String> {
|
||||
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<PathBuf>,
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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::<Vec<_>>().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<dyn Write> so we can use either a file or a sink that
|
||||
// discards everything when -o was not supplied.
|
||||
let mut output: Box<dyn Write> = 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;
|
||||
}
|
||||
};
|
||||
|
||||
// ── 4. Summary ─────────────────────────────────────────────────────────
|
||||
println!("=== Summary ===");
|
||||
println!(" total : {}", total);
|
||||
println!(" matched : {}", ok);
|
||||
println!(" failed : {}", bad);
|
||||
println!();
|
||||
// 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"));
|
||||
|
||||
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);
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// 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);
|
||||
None => {
|
||||
eprintln!("error: '{}' did not match the RPG IV grammar", path.display());
|
||||
any_error = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if any_error {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<wsc> ::= ' ' | '\t' | '\n' | '\r'
|
||||
<wsc> ::= ' ' | ' ' | '
|
||||
' | '
|
||||
'
|
||||
<ws> ::= <wsc> | <wsc> <ws>
|
||||
<opt-ws> ::= <ws> | ''
|
||||
|
||||
80
tests/hello_rpg.rs
Normal file
80
tests/hello_rpg.rs
Normal file
@@ -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 <program> non-terminal.
|
||||
assert!(
|
||||
tree.contains("program"),
|
||||
"parse tree does not mention <program>:\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}",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user