add: main function
This commit is contained in:
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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user