2026-03-12 21:09:49 -07:00
|
|
|
//! rust-langrpg — RPG IV compiler CLI
|
2026-03-12 20:55:01 -07:00
|
|
|
//!
|
2026-03-12 21:09:49 -07:00
|
|
|
//! 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
|
|
|
|
|
//! ```
|
2026-03-12 20:55:01 -07:00
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
use std::{
|
|
|
|
|
fs,
|
|
|
|
|
io::{self, Write},
|
|
|
|
|
path::PathBuf,
|
|
|
|
|
process,
|
|
|
|
|
};
|
2026-03-12 20:55:01 -07:00
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
use clap::Parser;
|
|
|
|
|
use rust_langrpg::{load_grammar, parse_as};
|
2026-03-12 20:55:01 -07:00
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
2026-03-12 21:09:49 -07:00
|
|
|
// CLI definition
|
2026-03-12 20:55:01 -07:00
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
/// 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>,
|
|
|
|
|
|
|
|
|
|
/// Write the parse tree(s) to this file.
|
|
|
|
|
/// If omitted the tree is not printed.
|
|
|
|
|
#[arg(short = 'o', value_name = "OUTPUT")]
|
|
|
|
|
output: Option<PathBuf>,
|
2026-03-12 20:55:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
2026-03-12 21:09:49 -07:00
|
|
|
// Entry point
|
2026-03-12 20:55:01 -07:00
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-03-05 22:28:14 -08:00
|
|
|
fn main() {
|
2026-03-12 21:09:49 -07:00
|
|
|
let cli = Cli::parse();
|
2026-03-12 20:55:01 -07:00
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
// ── Load grammar ─────────────────────────────────────────────────────────
|
2026-03-12 20:55:01 -07:00
|
|
|
let grammar = match load_grammar() {
|
2026-03-12 21:09:49 -07:00
|
|
|
Ok(g) => g,
|
2026-03-12 20:55:01 -07:00
|
|
|
Err(e) => {
|
2026-03-12 21:09:49 -07:00
|
|
|
eprintln!("error: failed to load RPG IV grammar: {e}");
|
|
|
|
|
process::exit(1);
|
2026-03-12 20:55:01 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
// ── Build parser ─────────────────────────────────────────────────────────
|
2026-03-12 20:55:01 -07:00
|
|
|
let parser = match grammar.build_parser() {
|
2026-03-12 21:09:49 -07:00
|
|
|
Ok(p) => p,
|
2026-03-12 20:55:01 -07:00
|
|
|
Err(e) => {
|
2026-03-12 21:09:49 -07:00
|
|
|
eprintln!("error: failed to build parser: {e}");
|
|
|
|
|
process::exit(1);
|
2026-03-12 20:55:01 -07:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
// ── 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))
|
2026-03-12 20:55:01 -07:00
|
|
|
}
|
2026-03-12 21:09:49 -07:00
|
|
|
None => Box::new(io::sink()),
|
|
|
|
|
};
|
2026-03-12 20:55:01 -07:00
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
// ── Process each source file ──────────────────────────────────────────────
|
|
|
|
|
let mut any_error = false;
|
|
|
|
|
|
|
|
|
|
for path in &cli.sources {
|
|
|
|
|
let source = match fs::read_to_string(path) {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("error: cannot read '{}': {e}", path.display());
|
|
|
|
|
any_error = true;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Try the top-level "program" rule first; fall back to "source-file"
|
|
|
|
|
// so the binary is useful even if only one of those rule names exists
|
|
|
|
|
// in the grammar.
|
|
|
|
|
let tree = parse_as(&parser, source.trim(), "program")
|
|
|
|
|
.or_else(|| parse_as(&parser, source.trim(), "source-file"));
|
|
|
|
|
|
|
|
|
|
match tree {
|
|
|
|
|
Some(t) => {
|
|
|
|
|
eprintln!("ok: {}", path.display());
|
|
|
|
|
writeln!(output, "=== {} ===", path.display())
|
|
|
|
|
.and_then(|_| writeln!(output, "{t}"))
|
|
|
|
|
.unwrap_or_else(|e| {
|
|
|
|
|
eprintln!("error: write failed: {e}");
|
|
|
|
|
any_error = true;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
eprintln!("error: '{}' did not match the RPG IV grammar", path.display());
|
|
|
|
|
any_error = true;
|
|
|
|
|
}
|
2026-03-12 20:55:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 21:09:49 -07:00
|
|
|
if any_error {
|
|
|
|
|
process::exit(1);
|
2026-03-12 20:55:01 -07:00
|
|
|
}
|
2026-03-05 22:28:14 -08:00
|
|
|
}
|