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