add: compiler
This commit is contained in:
116
Cargo.lock
generated
116
Cargo.lock
generated
@@ -58,6 +58,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bnf"
|
name = "bnf"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
@@ -78,6 +84,16 @@ version = "3.20.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.2.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2"
|
||||||
|
dependencies = [
|
||||||
|
"find-msvc-tools",
|
||||||
|
"shlex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -130,12 +146,24 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "find-msvc-tools"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -173,6 +201,30 @@ version = "0.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inkwell"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1def4112dfb2ce2993db7027f7acdb43c1f4ee1c70a082a2eef306ed5d0df365"
|
||||||
|
dependencies = [
|
||||||
|
"inkwell_internals",
|
||||||
|
"libc",
|
||||||
|
"llvm-sys",
|
||||||
|
"once_cell",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inkwell_internals"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63736175c9a30ea123f7018de9f26163e0b39cd6978990ae486b510c4f3bad69"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -195,12 +247,32 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.183"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "llvm-sys"
|
||||||
|
version = "211.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "108b3ad2b2eaf2a561fc74196273b20e3436e4a688b8b44e250d83974dc1b2e2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"cc",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"regex-lite",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.0"
|
version = "2.8.0"
|
||||||
@@ -290,12 +362,24 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex-lite"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpgrt"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-langrpg"
|
name = "rust-langrpg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bnf",
|
"bnf",
|
||||||
"clap",
|
"clap",
|
||||||
|
"either",
|
||||||
|
"inkwell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -304,6 +388,12 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -347,6 +437,12 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shlex"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -364,6 +460,26 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|||||||
30
Cargo.toml
30
Cargo.toml
@@ -1,9 +1,24 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
".",
|
||||||
|
"rpgrt",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Main compiler package
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "rust-langrpg"
|
name = "rust-langrpg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
default-run = "rust-langrpg"
|
default-run = "rust-langrpg"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Binaries
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rust-langrpg"
|
name = "rust-langrpg"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
@@ -12,6 +27,21 @@ path = "src/main.rs"
|
|||||||
name = "demo"
|
name = "demo"
|
||||||
path = "src/bin/demo.rs"
|
path = "src/bin/demo.rs"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Library (rlib — used by the binaries and tests)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rust_langrpg"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Dependencies
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bnf = "0.6"
|
bnf = "0.6"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
either = "1"
|
||||||
|
inkwell = { version = "0.8", features = ["llvm21-1"] }
|
||||||
|
|||||||
21
rpgrt/Cargo.toml
Normal file
21
rpgrt/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "rpgrt"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Build as a C-compatible shared library (librpgrt.so) so that RPG IV programs
|
||||||
|
# compiled by rust-langrpg can link against it at runtime.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "rpgrt"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
# cdylib → produces librpgrt.so (loaded by compiled RPG binaries)
|
||||||
|
# rlib → allows `cargo test` to run the unit tests in src/lib.rs
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# No external dependencies — the runtime is intentionally minimal and relies
|
||||||
|
# only on the Rust standard library and libc (linked automatically).
|
||||||
496
rpgrt/src/lib.rs
Normal file
496
rpgrt/src/lib.rs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
//! rpgrt.rs — RPG IV runtime library.
|
||||||
|
//!
|
||||||
|
//! This crate is compiled as a C-compatible shared library (`librpgrt.so`) that
|
||||||
|
//! RPG IV programs compiled by `rust-langrpg` link against at runtime.
|
||||||
|
//!
|
||||||
|
//! ## Exported symbols
|
||||||
|
//!
|
||||||
|
//! | Symbol | Signature | Description |
|
||||||
|
//! |---------------------|----------------------------------------|--------------------------------------|
|
||||||
|
//! | `rpg_dsply` | `(ptr: *const u8, len: i64)` | Display a fixed-length char field |
|
||||||
|
//! | `rpg_dsply_cstr` | `(ptr: *const c_char)` | Display a null-terminated C string |
|
||||||
|
//! | `rpg_dsply_i64` | `(n: i64)` | Display a signed 64-bit integer |
|
||||||
|
//! | `rpg_dsply_f64` | `(f: f64)` | Display a double-precision float |
|
||||||
|
//! | `rpg_halt` | `(code: i32)` | Abnormal program termination |
|
||||||
|
//!
|
||||||
|
//! ## Building
|
||||||
|
//!
|
||||||
|
//! The runtime is built automatically by `build.rs` as part of the normal
|
||||||
|
//! `cargo build` invocation. The resulting `librpgrt.so` is placed in the
|
||||||
|
//! Cargo output directory and the compiler binary links executables against it.
|
||||||
|
//!
|
||||||
|
//! To build it standalone:
|
||||||
|
//!
|
||||||
|
//! ```sh
|
||||||
|
//! rustc --edition 2024 --crate-type cdylib -o librpgrt.so src/bin/rpgrt.rs
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! ## DSPLY semantics
|
||||||
|
//!
|
||||||
|
//! In a real IBM i system, `DSPLY` writes a message to the *program message
|
||||||
|
//! queue* (an interactive operator message queue). On a Linux host there is no
|
||||||
|
//! equivalent facility, so we write to **stdout**, mirroring the message format
|
||||||
|
//! IBM i uses:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! DSPLY Hello, World!
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The output is flushed immediately after every `DSPLY` call, matching the
|
||||||
|
//! interactive behaviour of the IBM i runtime.
|
||||||
|
//!
|
||||||
|
//! Trailing ASCII spaces (0x20) are stripped from fixed-length `CHAR` fields
|
||||||
|
//! before display, exactly as IBM i does.
|
||||||
|
|
||||||
|
#![allow(clippy::missing_safety_doc)]
|
||||||
|
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::slice;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_dsply — display a fixed-length character field
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Display the first `len` bytes pointed to by `ptr`, trimming trailing spaces,
|
||||||
|
/// then print a newline and flush stdout.
|
||||||
|
///
|
||||||
|
/// This is the primary entry point called by the LLVM-compiled RPG procedure
|
||||||
|
/// for `DSPLY variable_name;`.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// * `ptr` must be valid for at least `len` bytes.
|
||||||
|
/// * `len` must be ≥ 0. A negative `len` is silently treated as 0.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_dsply(ptr: *const u8, len: i64) {
|
||||||
|
let bytes = if ptr.is_null() || len <= 0 {
|
||||||
|
b"" as &[u8]
|
||||||
|
} else {
|
||||||
|
unsafe { slice::from_raw_parts(ptr, len as usize) }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Strip trailing spaces (IBM i CHAR fields are space-padded to their
|
||||||
|
// declared length).
|
||||||
|
let trimmed = rtrim_spaces(bytes);
|
||||||
|
|
||||||
|
// Convert to a lossy UTF-8 string so non-ASCII EBCDIC-origin data at
|
||||||
|
// least renders something printable rather than crashing.
|
||||||
|
let text = String::from_utf8_lossy(trimmed);
|
||||||
|
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut out = stdout.lock();
|
||||||
|
// Mimic IBM i DSPLY prefix.
|
||||||
|
let _ = writeln!(out, "DSPLY {}", text);
|
||||||
|
let _ = out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_dsply_cstr — display a null-terminated C string
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Display a null-terminated C string with a `DSPLY` prefix.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// `ptr` must point to a valid null-terminated C string.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_dsply_cstr(ptr: *const std::os::raw::c_char) {
|
||||||
|
let text = if ptr.is_null() {
|
||||||
|
std::borrow::Cow::Borrowed("")
|
||||||
|
} else {
|
||||||
|
unsafe { CStr::from_ptr(ptr).to_string_lossy() }
|
||||||
|
};
|
||||||
|
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut out = stdout.lock();
|
||||||
|
let _ = writeln!(out, "DSPLY {}", text);
|
||||||
|
let _ = out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_dsply_i64 — display a signed 64-bit integer
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Display the decimal representation of a signed 64-bit integer.
|
||||||
|
///
|
||||||
|
/// Used when the argument to `DSPLY` is an integer expression rather than a
|
||||||
|
/// character variable.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn rpg_dsply_i64(n: i64) {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut out = stdout.lock();
|
||||||
|
let _ = writeln!(out, "DSPLY {}", n);
|
||||||
|
let _ = out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_dsply_f64 — display a double-precision float
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Display the decimal representation of a 64-bit IEEE 754 float.
|
||||||
|
///
|
||||||
|
/// Matches the numeric formatting IBM i uses for packed-decimal fields when
|
||||||
|
/// displayed via `DSPLY`.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn rpg_dsply_f64(f: f64) {
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let mut out = stdout.lock();
|
||||||
|
// Format with enough precision to round-trip.
|
||||||
|
let _ = writeln!(out, "DSPLY {}", f);
|
||||||
|
let _ = out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_halt — abnormal termination
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Terminate the program with the given exit code after printing an error
|
||||||
|
/// banner to stderr.
|
||||||
|
///
|
||||||
|
/// Maps roughly to the IBM i concept of an *unhandled exception* ending the
|
||||||
|
/// job.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn rpg_halt(code: i32) {
|
||||||
|
eprintln!("RPG program halted with code {}", code);
|
||||||
|
std::process::exit(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_memset_char — fill a CHAR field with a repeated byte
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Fill the first `len` bytes at `ptr` with `fill_byte`.
|
||||||
|
///
|
||||||
|
/// Used by `CLEAR` and `RESET` for character fields (fill with space 0x20).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// `ptr` must be valid for at least `len` bytes and must be writable.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_memset_char(ptr: *mut u8, fill_byte: u8, len: i64) {
|
||||||
|
if ptr.is_null() || len <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let slice = unsafe { slice::from_raw_parts_mut(ptr, len as usize) };
|
||||||
|
slice.fill(fill_byte);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_move_char — move (copy) a CHAR field, padding / truncating as needed
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Copy `src_len` bytes from `src` into a `dst_len`-byte field at `dst`.
|
||||||
|
///
|
||||||
|
/// * If `src_len` < `dst_len` the destination is right-padded with spaces.
|
||||||
|
/// * If `src_len` > `dst_len` only the first `dst_len` bytes of `src` are
|
||||||
|
/// copied (left-truncation rule, matching RPG IV `MOVE` semantics).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Both `src` and `dst` must be valid for their respective lengths.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_move_char(
|
||||||
|
dst: *mut u8,
|
||||||
|
dst_len: i64,
|
||||||
|
src: *const u8,
|
||||||
|
src_len: i64,
|
||||||
|
) {
|
||||||
|
if dst.is_null() || src.is_null() || dst_len <= 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dst_slice = unsafe { slice::from_raw_parts_mut(dst, dst_len as usize) };
|
||||||
|
let copy_len = (src_len.min(dst_len)) as usize;
|
||||||
|
|
||||||
|
if src_len > 0 {
|
||||||
|
let src_slice = unsafe { slice::from_raw_parts(src, src_len as usize) };
|
||||||
|
dst_slice[..copy_len].copy_from_slice(&src_slice[..copy_len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad remainder with spaces.
|
||||||
|
if (copy_len as i64) < dst_len {
|
||||||
|
dst_slice[copy_len..].fill(b' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_trim — return pointer + new length for a space-trimmed CHAR field
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Write the trimmed start pointer and trimmed length of a CHAR field into
|
||||||
|
/// `out_ptr` and `out_len` respectively.
|
||||||
|
///
|
||||||
|
/// Leading *and* trailing spaces are stripped (equivalent to `%TRIM`).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// * `ptr` must be valid for `len` bytes.
|
||||||
|
/// * `out_ptr` and `out_len` must be valid writable pointers.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_trim(
|
||||||
|
ptr: *const u8,
|
||||||
|
len: i64,
|
||||||
|
out_ptr: *mut *const u8,
|
||||||
|
out_len: *mut i64,
|
||||||
|
) {
|
||||||
|
if ptr.is_null() || len <= 0 || out_ptr.is_null() || out_len.is_null() {
|
||||||
|
if !out_ptr.is_null() { unsafe { *out_ptr = ptr; } }
|
||||||
|
if !out_len.is_null() { unsafe { *out_len = 0; } }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bytes = unsafe { slice::from_raw_parts(ptr, len as usize) };
|
||||||
|
let trimmed = bytes
|
||||||
|
.iter()
|
||||||
|
.position(|&b| b != b' ')
|
||||||
|
.map(|start| {
|
||||||
|
let end = bytes.iter().rposition(|&b| b != b' ').unwrap_or(start) + 1;
|
||||||
|
&bytes[start..end]
|
||||||
|
})
|
||||||
|
.unwrap_or(&bytes[0..0]);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
*out_ptr = trimmed.as_ptr();
|
||||||
|
*out_len = trimmed.len() as i64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_len — return the non-space length of a CHAR field (%LEN semantics)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Return the *declared* length of a CHAR field (i.e. `len` itself), not the
|
||||||
|
/// trimmed length. This matches RPG IV `%LEN` which returns the declared size.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// No pointer dereference is performed; this function is trivially safe.
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn rpg_len(_ptr: *const u8, len: i64) -> i64 {
|
||||||
|
len
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_scan — %SCAN(search : source [: start])
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Search for `search_ptr[0..search_len]` inside `src_ptr[0..src_len]`
|
||||||
|
/// starting at byte offset `start` (1-based, RPG IV convention).
|
||||||
|
///
|
||||||
|
/// Returns the 1-based position of the first match, or 0 if not found.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// Both pointers must be valid for their respective lengths.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_scan(
|
||||||
|
search_ptr: *const u8,
|
||||||
|
search_len: i64,
|
||||||
|
src_ptr: *const u8,
|
||||||
|
src_len: i64,
|
||||||
|
start: i64, // 1-based; 0 means "from beginning" (treated as 1)
|
||||||
|
) -> i64 {
|
||||||
|
if search_ptr.is_null() || src_ptr.is_null()
|
||||||
|
|| search_len <= 0 || src_len <= 0
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let needle = unsafe { slice::from_raw_parts(search_ptr, search_len as usize) };
|
||||||
|
let hay = unsafe { slice::from_raw_parts(src_ptr, src_len as usize) };
|
||||||
|
|
||||||
|
let from = if start <= 0 { 0 } else { (start - 1) as usize };
|
||||||
|
if from >= hay.len() { return 0; }
|
||||||
|
|
||||||
|
hay[from..]
|
||||||
|
.windows(needle.len())
|
||||||
|
.position(|w| w == needle)
|
||||||
|
.map(|p| (from + p + 1) as i64) // convert back to 1-based
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// rpg_subst — %SUBST(str : start [: len])
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Write up to `sub_len` bytes from `src_ptr` starting at byte `start`
|
||||||
|
/// (1-based) into `dst_ptr`. Returns the number of bytes written.
|
||||||
|
///
|
||||||
|
/// If `sub_len` is 0 the function copies from `start` to the end of the
|
||||||
|
/// source field (mirrors RPG IV `%SUBST` two-argument form).
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// All pointers must be valid for their respective lengths.
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn rpg_subst(
|
||||||
|
src_ptr: *const u8,
|
||||||
|
src_len: i64,
|
||||||
|
start: i64, // 1-based
|
||||||
|
sub_len: i64, // 0 = "to end"
|
||||||
|
dst_ptr: *mut u8,
|
||||||
|
dst_len: i64,
|
||||||
|
) -> i64 {
|
||||||
|
if src_ptr.is_null() || dst_ptr.is_null() || src_len <= 0 || dst_len <= 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let src = unsafe { slice::from_raw_parts(src_ptr, src_len as usize) };
|
||||||
|
let dst = unsafe { slice::from_raw_parts_mut(dst_ptr, dst_len as usize) };
|
||||||
|
|
||||||
|
let from = if start <= 1 { 0 } else { (start - 1) as usize };
|
||||||
|
if from >= src.len() { return 0; }
|
||||||
|
|
||||||
|
let available = src.len() - from;
|
||||||
|
let want = if sub_len <= 0 {
|
||||||
|
available
|
||||||
|
} else {
|
||||||
|
(sub_len as usize).min(available)
|
||||||
|
};
|
||||||
|
let copy = want.min(dst.len());
|
||||||
|
|
||||||
|
dst[..copy].copy_from_slice(&src[from..from + copy]);
|
||||||
|
copy as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Helper: trim trailing ASCII spaces
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn rtrim_spaces(bytes: &[u8]) -> &[u8] {
|
||||||
|
let end = bytes
|
||||||
|
.iter()
|
||||||
|
.rposition(|&b| b != b' ')
|
||||||
|
.map(|i| i + 1)
|
||||||
|
.unwrap_or(0);
|
||||||
|
&bytes[..end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Tests
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rtrim_strips_spaces() {
|
||||||
|
assert_eq!(rtrim_spaces(b"hello "), b"hello");
|
||||||
|
assert_eq!(rtrim_spaces(b"hello"), b"hello");
|
||||||
|
assert_eq!(rtrim_spaces(b" "), b"");
|
||||||
|
assert_eq!(rtrim_spaces(b""), b"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rtrim_preserves_internal_spaces() {
|
||||||
|
assert_eq!(rtrim_spaces(b"hello world "), b"hello world");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_scan_finds_match() {
|
||||||
|
let hay = b"Hello, World!";
|
||||||
|
let needle = b"World";
|
||||||
|
let pos = unsafe {
|
||||||
|
rpg_scan(
|
||||||
|
needle.as_ptr(), needle.len() as i64,
|
||||||
|
hay.as_ptr(), hay.len() as i64,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(pos, 8); // 1-based position of 'W'
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_scan_not_found() {
|
||||||
|
let hay = b"Hello";
|
||||||
|
let needle = b"XYZ";
|
||||||
|
let pos = unsafe {
|
||||||
|
rpg_scan(
|
||||||
|
needle.as_ptr(), needle.len() as i64,
|
||||||
|
hay.as_ptr(), hay.len() as i64,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(pos, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_subst_copies_correctly() {
|
||||||
|
let src = b"Hello, World!";
|
||||||
|
let mut dst = vec![0u8; 5];
|
||||||
|
let written = unsafe {
|
||||||
|
rpg_subst(
|
||||||
|
src.as_ptr(), src.len() as i64,
|
||||||
|
8, // start at 'W' (1-based)
|
||||||
|
5,
|
||||||
|
dst.as_mut_ptr(), dst.len() as i64,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
assert_eq!(written, 5);
|
||||||
|
assert_eq!(&dst, b"World");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_move_char_pads_with_spaces() {
|
||||||
|
let src = b"Hi";
|
||||||
|
let mut dst = vec![0u8; 5];
|
||||||
|
unsafe {
|
||||||
|
rpg_move_char(
|
||||||
|
dst.as_mut_ptr(), dst.len() as i64,
|
||||||
|
src.as_ptr(), src.len() as i64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(&dst, b"Hi ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_move_char_truncates() {
|
||||||
|
let src = b"Hello, World!";
|
||||||
|
let mut dst = vec![0u8; 5];
|
||||||
|
unsafe {
|
||||||
|
rpg_move_char(
|
||||||
|
dst.as_mut_ptr(), dst.len() as i64,
|
||||||
|
src.as_ptr(), src.len() as i64,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(&dst, b"Hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_trim_removes_leading_and_trailing() {
|
||||||
|
let input = b" hello ";
|
||||||
|
let mut out_ptr: *const u8 = std::ptr::null();
|
||||||
|
let mut out_len: i64 = 0;
|
||||||
|
unsafe {
|
||||||
|
rpg_trim(
|
||||||
|
input.as_ptr(), input.len() as i64,
|
||||||
|
&mut out_ptr, &mut out_len,
|
||||||
|
);
|
||||||
|
let result = std::slice::from_raw_parts(out_ptr, out_len as usize);
|
||||||
|
assert_eq!(result, b"hello");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_dsply_smoke() {
|
||||||
|
// Just ensure it doesn't panic.
|
||||||
|
let msg = b"Hello, World! ";
|
||||||
|
unsafe { rpg_dsply(msg.as_ptr(), msg.len() as i64) };
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_dsply_i64_smoke() {
|
||||||
|
rpg_dsply_i64(42);
|
||||||
|
rpg_dsply_i64(-1);
|
||||||
|
rpg_dsply_i64(i64::MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rpg_dsply_f64_smoke() {
|
||||||
|
rpg_dsply_f64(3.14159);
|
||||||
|
rpg_dsply_f64(-0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
651
src/ast.rs
Normal file
651
src/ast.rs
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
//! ast.rs — Typed Abstract Syntax Tree for RPG IV free-format programs.
|
||||||
|
//!
|
||||||
|
//! This module defines the in-memory representation produced by the lowering
|
||||||
|
//! pass (`lower.rs`) and consumed by the LLVM code-generator (`codegen.rs`).
|
||||||
|
//!
|
||||||
|
//! Only the subset of the language that is needed to compile `hello.rpg` (and
|
||||||
|
//! small programs like it) is fully fleshed out. Everything else is kept as
|
||||||
|
//! placeholder variants so the lowering pass can represent the whole parse tree
|
||||||
|
//! without panicking, and the codegen can skip unimplemented nodes gracefully.
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Top-level program
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A complete RPG IV source file.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Program {
|
||||||
|
/// Zero or more top-level declarations (CTL-OPT, DCL-S, DCL-C, DCL-DS,
|
||||||
|
/// file declarations, subroutines …).
|
||||||
|
pub declarations: Vec<Declaration>,
|
||||||
|
/// Zero or more procedure definitions (`DCL-PROC … END-PROC`).
|
||||||
|
pub procedures: Vec<Procedure>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Declarations
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Declaration {
|
||||||
|
/// `CTL-OPT keyword-list;`
|
||||||
|
ControlSpec(ControlSpec),
|
||||||
|
/// `DCL-S name type [keywords];`
|
||||||
|
Standalone(StandaloneDecl),
|
||||||
|
/// `DCL-C name literal;` or `DCL-C name CONST(literal);`
|
||||||
|
Constant(ConstantDecl),
|
||||||
|
/// `DCL-C name *named-constant;`
|
||||||
|
NamedConstantDecl(NamedConstantDecl),
|
||||||
|
/// `DCL-DS name … END-DS;`
|
||||||
|
DataStructure(DataStructureDecl),
|
||||||
|
/// `DCL-F name …;`
|
||||||
|
File(FileDecl),
|
||||||
|
/// `BEG-SR name; … END-SR;`
|
||||||
|
Subroutine(Subroutine),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Control spec ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ControlSpec {
|
||||||
|
pub keywords: Vec<CtlKeyword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CtlKeyword {
|
||||||
|
DftActGrp(bool), // *YES / *NO
|
||||||
|
NoMain,
|
||||||
|
Main(String),
|
||||||
|
Other(String), // catch-all for keywords we don't generate code for
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Standalone variable ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StandaloneDecl {
|
||||||
|
pub name: String,
|
||||||
|
pub ty: TypeSpec,
|
||||||
|
pub keywords: Vec<VarKeyword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Constant declaration ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConstantDecl {
|
||||||
|
pub name: String,
|
||||||
|
pub value: Literal,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NamedConstantDecl {
|
||||||
|
pub name: String,
|
||||||
|
pub value: NamedConstant,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Data structure ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DataStructureDecl {
|
||||||
|
pub name: String,
|
||||||
|
pub keywords: Vec<DsKeyword>,
|
||||||
|
pub fields: Vec<DsField>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum DsKeyword {
|
||||||
|
Qualified,
|
||||||
|
Template,
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DsField {
|
||||||
|
pub name: String,
|
||||||
|
pub ty: TypeSpec,
|
||||||
|
pub keywords: Vec<VarKeyword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── File declaration ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FileDecl {
|
||||||
|
pub name: String,
|
||||||
|
pub keywords: Vec<String>, // simplified — not code-gen'd
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Subroutine ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Subroutine {
|
||||||
|
pub name: String,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Type specifications
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum TypeSpec {
|
||||||
|
/// `CHAR(n)` — fixed-length character field.
|
||||||
|
Char(Box<Expression>),
|
||||||
|
/// `VARCHAR(n)` — variable-length character.
|
||||||
|
VarChar(Box<Expression>),
|
||||||
|
/// `INT(n)` — signed integer (n = 3, 5, 10, or 20).
|
||||||
|
Int(Box<Expression>),
|
||||||
|
/// `UNS(n)` — unsigned integer.
|
||||||
|
Uns(Box<Expression>),
|
||||||
|
/// `FLOAT(n)` — floating-point.
|
||||||
|
Float(Box<Expression>),
|
||||||
|
/// `PACKED(digits:decimals)`
|
||||||
|
Packed(Box<Expression>, Box<Expression>),
|
||||||
|
/// `ZONED(digits:decimals)`
|
||||||
|
Zoned(Box<Expression>, Box<Expression>),
|
||||||
|
/// `BINDEC(digits:decimals)`
|
||||||
|
Bindec(Box<Expression>, Box<Expression>),
|
||||||
|
/// `IND` — indicator (boolean).
|
||||||
|
Ind,
|
||||||
|
/// `DATE [(*fmt)]`
|
||||||
|
Date,
|
||||||
|
/// `TIME [(*fmt)]`
|
||||||
|
Time,
|
||||||
|
/// `TIMESTAMP`
|
||||||
|
Timestamp,
|
||||||
|
/// `POINTER`
|
||||||
|
Pointer,
|
||||||
|
/// `LIKE(name)`
|
||||||
|
Like(String),
|
||||||
|
/// `LIKEDS(name)`
|
||||||
|
LikeDs(String),
|
||||||
|
/// Unrecognised / not yet implemented type.
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeSpec {
|
||||||
|
/// Return the number of bytes this type occupies at runtime on a 64-bit
|
||||||
|
/// Linux host. Returns `None` for types whose size is not statically known.
|
||||||
|
pub fn byte_size(&self) -> Option<u64> {
|
||||||
|
match self {
|
||||||
|
TypeSpec::Char(expr) | TypeSpec::VarChar(expr) => {
|
||||||
|
if let Expression::Literal(Literal::Integer(n)) = expr.as_ref() {
|
||||||
|
Some(*n as u64)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeSpec::Int(expr) | TypeSpec::Uns(expr) => {
|
||||||
|
if let Expression::Literal(Literal::Integer(n)) = expr.as_ref() {
|
||||||
|
Some(match n {
|
||||||
|
3 => 1,
|
||||||
|
5 => 2,
|
||||||
|
10 => 4,
|
||||||
|
20 => 8,
|
||||||
|
_ => 8, // default to 8 bytes
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeSpec::Float(expr) => {
|
||||||
|
if let Expression::Literal(Literal::Integer(n)) = expr.as_ref() {
|
||||||
|
Some(if *n <= 4 { 4 } else { 8 })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TypeSpec::Ind => Some(1),
|
||||||
|
TypeSpec::Pointer => Some(8),
|
||||||
|
TypeSpec::Packed(digits, _) => {
|
||||||
|
if let Expression::Literal(Literal::Integer(n)) = digits.as_ref() {
|
||||||
|
Some((*n as u64 / 2) + 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Variable / declaration keywords
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum VarKeyword {
|
||||||
|
/// `INZ` — default initialisation.
|
||||||
|
Inz,
|
||||||
|
/// `INZ(expr)` — explicit initialisation value.
|
||||||
|
InzExpr(Expression),
|
||||||
|
/// `INZ(*named-constant)` — initialise to named constant.
|
||||||
|
InzNamed(NamedConstant),
|
||||||
|
Static,
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Procedures
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Procedure {
|
||||||
|
pub name: String,
|
||||||
|
pub exported: bool,
|
||||||
|
pub pi: Option<PiSpec>,
|
||||||
|
/// Local declarations (DCL-S, DCL-C, etc.) inside the procedure.
|
||||||
|
pub locals: Vec<Declaration>,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Procedure Interface specification (`DCL-PI … END-PI`).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PiSpec {
|
||||||
|
pub name: String,
|
||||||
|
pub return_ty: Option<TypeSpec>,
|
||||||
|
pub params: Vec<PiParam>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PiParam {
|
||||||
|
pub name: String,
|
||||||
|
pub ty: TypeSpec,
|
||||||
|
pub keywords: Vec<ParamKeyword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ParamKeyword {
|
||||||
|
Value,
|
||||||
|
Const,
|
||||||
|
Other(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Statements
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum Statement {
|
||||||
|
/// `lvalue = expr;` or `EVAL lvalue = expr;`
|
||||||
|
Assign(AssignStmt),
|
||||||
|
/// `IF expr; … [ELSEIF …] [ELSE …] ENDIF;`
|
||||||
|
If(IfStmt),
|
||||||
|
/// `DOW expr; … ENDDO;`
|
||||||
|
DoWhile(DoWhileStmt),
|
||||||
|
/// `DOU expr; … ENDDO;`
|
||||||
|
DoUntil(DoUntilStmt),
|
||||||
|
/// `FOR i = start TO/DOWNTO end [BY step]; … ENDFOR;`
|
||||||
|
For(ForStmt),
|
||||||
|
/// `SELECT; WHEN … [OTHER …] ENDSL;`
|
||||||
|
Select(SelectStmt),
|
||||||
|
/// `MONITOR; … ON-ERROR … ENDMON;`
|
||||||
|
Monitor(MonitorStmt),
|
||||||
|
/// `CALLP name(args);` or bare procedure call `name(args);`
|
||||||
|
CallP(CallPStmt),
|
||||||
|
/// `RETURN [expr];`
|
||||||
|
Return(ReturnStmt),
|
||||||
|
/// `LEAVE;`
|
||||||
|
Leave,
|
||||||
|
/// `ITER;`
|
||||||
|
Iter,
|
||||||
|
/// `LEAVESR;`
|
||||||
|
LeaveSr,
|
||||||
|
/// `EXSR name;`
|
||||||
|
ExSr(String),
|
||||||
|
/// `DSPLY expr;`
|
||||||
|
Dsply(DsplyStmt),
|
||||||
|
/// `RESET lvalue;` / `RESET *ALL;`
|
||||||
|
Reset(ResetStmt),
|
||||||
|
/// `CLEAR lvalue;`
|
||||||
|
Clear(LValue),
|
||||||
|
/// Any I/O statement (READ, WRITE, CHAIN, etc.) — kept as opaque for now.
|
||||||
|
Io(IoStatement),
|
||||||
|
/// Catch-all for statements not yet lowered.
|
||||||
|
Unimplemented(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Assignment ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AssignStmt {
|
||||||
|
pub target: LValue,
|
||||||
|
pub value: Expression,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── If / ElseIf / Else ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct IfStmt {
|
||||||
|
pub condition: Expression,
|
||||||
|
pub then_body: Vec<Statement>,
|
||||||
|
pub elseifs: Vec<ElseIf>,
|
||||||
|
pub else_body: Option<Vec<Statement>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ElseIf {
|
||||||
|
pub condition: Expression,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DOW loop ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DoWhileStmt {
|
||||||
|
pub condition: Expression,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DOU loop ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DoUntilStmt {
|
||||||
|
pub condition: Expression,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── FOR loop ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ForStmt {
|
||||||
|
pub var: String,
|
||||||
|
pub start: Expression,
|
||||||
|
pub limit: Expression,
|
||||||
|
pub step: Option<Expression>,
|
||||||
|
pub downto: bool,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── SELECT / WHEN ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SelectStmt {
|
||||||
|
pub whens: Vec<WhenClause>,
|
||||||
|
pub other: Option<Vec<Statement>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WhenClause {
|
||||||
|
pub condition: Expression,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── MONITOR ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MonitorStmt {
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
pub handlers: Vec<OnError>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct OnError {
|
||||||
|
pub codes: Vec<ErrorCode>,
|
||||||
|
pub body: Vec<Statement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ErrorCode {
|
||||||
|
Integer(u32),
|
||||||
|
Program,
|
||||||
|
File,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CALLP ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CallPStmt {
|
||||||
|
pub name: String,
|
||||||
|
pub args: Vec<Arg>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RETURN ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ReturnStmt {
|
||||||
|
pub value: Option<Expression>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── DSPLY ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DsplyStmt {
|
||||||
|
/// The expression to display.
|
||||||
|
pub expr: Expression,
|
||||||
|
/// Optional message queue identifier (two-operand form).
|
||||||
|
pub msg_q: Option<String>,
|
||||||
|
pub response: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── RESET ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ResetStmt {
|
||||||
|
Target(LValue),
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── I/O (opaque) ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum IoStatement {
|
||||||
|
Read { file: String },
|
||||||
|
ReadP { file: String },
|
||||||
|
Write { record: String },
|
||||||
|
Update { record: String },
|
||||||
|
Delete { key: Expression, file: String },
|
||||||
|
Chain { key: Expression, file: String },
|
||||||
|
SetLL { key: SetKey, file: String },
|
||||||
|
SetGT { key: SetKey, file: String },
|
||||||
|
Open { file: String },
|
||||||
|
Close { file: Option<String> }, // None = *ALL
|
||||||
|
Except { format: Option<String> },
|
||||||
|
ExFmt { format: String },
|
||||||
|
Post { file: String },
|
||||||
|
Feod { file: String },
|
||||||
|
Unlock { file: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum SetKey {
|
||||||
|
Expr(Expression),
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// L-values
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// An assignable location.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum LValue {
|
||||||
|
/// Simple or dotted name: `myVar` or `ds.field`.
|
||||||
|
Name(QualifiedName),
|
||||||
|
/// Array element: `arr(i)`.
|
||||||
|
Index(QualifiedName, Vec<Expression>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LValue {
|
||||||
|
/// Return the base name (first component of the qualified name).
|
||||||
|
pub fn base_name(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
LValue::Name(q) | LValue::Index(q, _) => &q.parts[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Expressions
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Expression {
|
||||||
|
Literal(Literal),
|
||||||
|
Named(NamedConstant),
|
||||||
|
Special(SpecialValue),
|
||||||
|
Variable(QualifiedName),
|
||||||
|
/// Array / function-style subscript: `name(idx)`.
|
||||||
|
Index(QualifiedName, Vec<Expression>),
|
||||||
|
/// Procedure / built-in call as expression: `name(args)`.
|
||||||
|
Call(String, Vec<Arg>),
|
||||||
|
BuiltIn(BuiltIn),
|
||||||
|
UnaryMinus(Box<Expression>),
|
||||||
|
UnaryPlus(Box<Expression>),
|
||||||
|
BinOp(BinOp, Box<Expression>, Box<Expression>),
|
||||||
|
Not(Box<Expression>),
|
||||||
|
Paren(Box<Expression>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum BinOp {
|
||||||
|
Add, Sub, Mul, Div, Pow,
|
||||||
|
Eq, Ne, Lt, Le, Gt, Ge,
|
||||||
|
And, Or,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Literals
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Literal {
|
||||||
|
String(String),
|
||||||
|
Integer(i64),
|
||||||
|
Float(f64),
|
||||||
|
Hex(Vec<u8>),
|
||||||
|
/// `*ON` / `*OFF` as a literal.
|
||||||
|
Indicator(bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Named constants (`*ON`, `*OFF`, `*BLANK`, …)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum NamedConstant {
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Blank,
|
||||||
|
Blanks,
|
||||||
|
Zero,
|
||||||
|
Zeros,
|
||||||
|
HiVal,
|
||||||
|
LoVal,
|
||||||
|
Null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Special values (`*IN`, `*START`, …)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum SpecialValue {
|
||||||
|
/// `*IN(n)` — indicator by number.
|
||||||
|
In(Box<Expression>),
|
||||||
|
InAll,
|
||||||
|
On,
|
||||||
|
Off,
|
||||||
|
Blank,
|
||||||
|
Blanks,
|
||||||
|
Zero,
|
||||||
|
Zeros,
|
||||||
|
HiVal,
|
||||||
|
LoVal,
|
||||||
|
Null,
|
||||||
|
/// `*ALL'string'`
|
||||||
|
All(String),
|
||||||
|
Omit,
|
||||||
|
This,
|
||||||
|
Same,
|
||||||
|
Start,
|
||||||
|
End,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Built-in functions
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// The RPG IV `%BUILTIN(…)` functions we actually lower to code.
|
||||||
|
/// All others are wrapped in `Other`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum BuiltIn {
|
||||||
|
/// `%LEN(identifier)` — byte length of a field.
|
||||||
|
Len(Box<Expression>),
|
||||||
|
/// `%TRIM(expr)` — trim leading and trailing blanks.
|
||||||
|
Trim(Box<Expression>),
|
||||||
|
/// `%TRIML(expr)` — trim leading blanks.
|
||||||
|
TrimL(Box<Expression>),
|
||||||
|
/// `%TRIMR(expr)` — trim trailing blanks.
|
||||||
|
TrimR(Box<Expression>),
|
||||||
|
/// `%CHAR(expr)` — convert to character string.
|
||||||
|
Char(Box<Expression>),
|
||||||
|
/// `%INT(expr)` — convert to integer.
|
||||||
|
Int(Box<Expression>),
|
||||||
|
/// `%DEC(expr:digits:decimals)` — convert to packed decimal.
|
||||||
|
Dec(Box<Expression>, Box<Expression>, Box<Expression>),
|
||||||
|
/// `%ABS(expr)` — absolute value.
|
||||||
|
Abs(Box<Expression>),
|
||||||
|
/// `%SQRT(expr)` — square root.
|
||||||
|
Sqrt(Box<Expression>),
|
||||||
|
/// `%EOF[(file)]`
|
||||||
|
Eof(Option<String>),
|
||||||
|
/// `%FOUND[(file)]`
|
||||||
|
Found(Option<String>),
|
||||||
|
/// `%ERROR()`
|
||||||
|
Error,
|
||||||
|
/// `%SUBST(str:start:len)` or `%SUBST(str:start)`.
|
||||||
|
Subst(Box<Expression>, Box<Expression>, Option<Box<Expression>>),
|
||||||
|
/// `%SCAN(pattern:source[:start])`.
|
||||||
|
Scan(Box<Expression>, Box<Expression>, Option<Box<Expression>>),
|
||||||
|
/// `%SIZE(identifier)`.
|
||||||
|
Size(Box<Expression>),
|
||||||
|
/// `%ADDR(identifier)`.
|
||||||
|
Addr(Box<Expression>),
|
||||||
|
/// `%ALLOC(size)`.
|
||||||
|
Alloc(Box<Expression>),
|
||||||
|
/// `%REM(a:b)`.
|
||||||
|
Rem(Box<Expression>, Box<Expression>),
|
||||||
|
/// `%DIV(a:b)`.
|
||||||
|
Div(Box<Expression>, Box<Expression>),
|
||||||
|
/// Any built-in we haven't individually modelled.
|
||||||
|
Other(String, Vec<Expression>),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Qualified names and argument lists
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// A dot-separated name: `ds.subDs.leaf`.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct QualifiedName {
|
||||||
|
pub parts: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QualifiedName {
|
||||||
|
pub fn simple(name: impl Into<String>) -> Self {
|
||||||
|
QualifiedName { parts: vec![name.into()] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_simple(&self) -> bool {
|
||||||
|
self.parts.len() == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the leaf (last) component.
|
||||||
|
pub fn leaf(&self) -> &str {
|
||||||
|
self.parts.last().map(|s| s.as_str()).unwrap_or("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for QualifiedName {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.parts.join("."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A call argument.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Arg {
|
||||||
|
Expr(Expression),
|
||||||
|
Omit,
|
||||||
|
}
|
||||||
1589
src/codegen.rs
Normal file
1589
src/codegen.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,13 @@
|
|||||||
//!
|
//!
|
||||||
//! Loads the BNF grammar embedded at compile time, builds a [`bnf::GrammarParser`],
|
//! 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.
|
//! and exposes helpers used by both the compiler binary and the demo binary.
|
||||||
|
//!
|
||||||
|
//! Also provides the typed AST ([`ast`]), BNF-to-AST lowering pass ([`lower`]),
|
||||||
|
//! and LLVM code-generator ([`codegen`]) used by the compiler pipeline.
|
||||||
|
|
||||||
|
pub mod ast;
|
||||||
|
pub mod lower;
|
||||||
|
pub mod codegen;
|
||||||
|
|
||||||
use bnf::{Grammar, Term};
|
use bnf::{Grammar, Term};
|
||||||
|
|
||||||
|
|||||||
2758
src/lower.rs
Normal file
2758
src/lower.rs
Normal file
File diff suppressed because it is too large
Load Diff
421
src/main.rs
421
src/main.rs
@@ -1,7 +1,11 @@
|
|||||||
//! rust-langrpg — RPG IV compiler CLI
|
//! rust-langrpg — RPG IV compiler CLI
|
||||||
//!
|
//!
|
||||||
//! Parses one or more RPG IV source files using the embedded BNF grammar
|
//! Full compilation pipeline:
|
||||||
//! and optionally writes the resulting parse tree to an output file.
|
//! source (.rpg)
|
||||||
|
//! → BNF validation (bnf crate)
|
||||||
|
//! → AST lowering (lower.rs)
|
||||||
|
//! → LLVM IR / object (codegen.rs via inkwell)
|
||||||
|
//! → native executable (cc linker + librpgrt.so runtime)
|
||||||
//!
|
//!
|
||||||
//! ## Usage
|
//! ## Usage
|
||||||
//!
|
//!
|
||||||
@@ -9,10 +13,15 @@
|
|||||||
//! rust-langrpg [OPTIONS] <SOURCES>...
|
//! rust-langrpg [OPTIONS] <SOURCES>...
|
||||||
//!
|
//!
|
||||||
//! Arguments:
|
//! Arguments:
|
||||||
//! <SOURCES>... RPG IV source file(s) to parse
|
//! <SOURCES>... RPG IV source file(s) to compile
|
||||||
//!
|
//!
|
||||||
//! Options:
|
//! Options:
|
||||||
//! -o <OUTPUT> Write the parse tree to this file
|
//! -o <OUTPUT> Output executable path [default: a.out]
|
||||||
|
//! --emit-ir Print LLVM IR to stdout instead of producing a binary
|
||||||
|
//! --emit-tree Print BNF parse tree to stdout instead of compiling
|
||||||
|
//! -O <LEVEL> Optimisation level 0-3 [default: 0]
|
||||||
|
//! --no-link Produce a .o object file, skip linking
|
||||||
|
//! --runtime <PATH> Path to librpgrt.so [default: auto-detect]
|
||||||
//! -h, --help Print help
|
//! -h, --help Print help
|
||||||
//! -V, --version Print version
|
//! -V, --version Print version
|
||||||
//! ```
|
//! ```
|
||||||
@@ -20,35 +29,64 @@
|
|||||||
//! ## Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
//! ```text
|
//! ```text
|
||||||
//! cargo run --release -- -o out.txt hello.rpg
|
//! cargo run --release -- -o main hello.rpg
|
||||||
|
//! ./main
|
||||||
|
//! DSPLY Hello, World!
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs,
|
||||||
io::{self, Write},
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process,
|
process,
|
||||||
};
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser as ClapParser;
|
||||||
use rust_langrpg::{load_grammar, parse_as};
|
use rust_langrpg::{codegen, load_grammar, lower::lower, parse_as};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// CLI definition
|
// CLI definition
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// RPG IV free-format compiler — parses source files and emits parse trees.
|
/// RPG IV free-format compiler — produces native Linux executables from RPG IV
|
||||||
#[derive(Parser, Debug)]
|
/// source files using LLVM as the back-end.
|
||||||
#[command(name = "rust-langrpg", version, about, long_about = None)]
|
#[derive(ClapParser, Debug)]
|
||||||
|
#[command(
|
||||||
|
name = "rust-langrpg",
|
||||||
|
version,
|
||||||
|
about = "RPG IV compiler (LLVM back-end)",
|
||||||
|
long_about = None,
|
||||||
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// RPG IV source file(s) to parse.
|
/// RPG IV source file(s) to compile.
|
||||||
#[arg(required = true, value_name = "SOURCES")]
|
#[arg(required = true, value_name = "SOURCES")]
|
||||||
sources: Vec<PathBuf>,
|
sources: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Write the parse tree(s) to this file.
|
/// Write the output executable (or object with --no-link) to this path.
|
||||||
/// If omitted the tree is not printed.
|
/// If omitted the binary is written to `a.out`.
|
||||||
#[arg(short = 'o', value_name = "OUTPUT")]
|
#[arg(short = 'o', value_name = "OUTPUT")]
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Emit LLVM IR text to stdout instead of compiling to a binary.
|
||||||
|
#[arg(long = "emit-ir")]
|
||||||
|
emit_ir: bool,
|
||||||
|
|
||||||
|
/// Emit the BNF parse tree to stdout instead of compiling.
|
||||||
|
#[arg(long = "emit-tree")]
|
||||||
|
emit_tree: bool,
|
||||||
|
|
||||||
|
/// Optimisation level: 0 = none, 1 = less, 2 = default, 3 = aggressive.
|
||||||
|
#[arg(short = 'O', default_value = "0", value_name = "LEVEL")]
|
||||||
|
opt_level: u8,
|
||||||
|
|
||||||
|
/// Produce a `.o` object file but do not invoke the linker.
|
||||||
|
#[arg(long = "no-link")]
|
||||||
|
no_link: bool,
|
||||||
|
|
||||||
|
/// Path to the `librpgrt.so` runtime shared library.
|
||||||
|
/// If not specified the compiler searches in common locations.
|
||||||
|
#[arg(long = "runtime", value_name = "PATH")]
|
||||||
|
runtime: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -58,7 +96,7 @@ struct Cli {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
// ── Load grammar ─────────────────────────────────────────────────────────
|
// ── Load and build the BNF grammar ───────────────────────────────────────
|
||||||
let grammar = match load_grammar() {
|
let grammar = match load_grammar() {
|
||||||
Ok(g) => g,
|
Ok(g) => g,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -67,61 +105,139 @@ fn main() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Build parser ─────────────────────────────────────────────────────────
|
let bnf_parser = match grammar.build_parser() {
|
||||||
let parser = match grammar.build_parser() {
|
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: failed to build parser: {e}");
|
eprintln!("error: failed to build BNF parser: {e}");
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Open output sink ──────────────────────────────────────────────────────
|
// ── Process each source file ─────────────────────────────────────────────
|
||||||
// `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()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Process each source file ──────────────────────────────────────────────
|
|
||||||
let mut any_error = false;
|
let mut any_error = false;
|
||||||
|
|
||||||
for path in &cli.sources {
|
for source_path in &cli.sources {
|
||||||
let source = match fs::read_to_string(path) {
|
let source_text = match fs::read_to_string(source_path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("error: cannot read '{}': {e}", path.display());
|
eprintln!("error: cannot read '{}': {e}", source_path.display());
|
||||||
any_error = true;
|
any_error = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try the top-level "program" rule first; fall back to "source-file"
|
// ── BNF validation ────────────────────────────────────────────────────
|
||||||
// so the binary is useful even if only one of those rule names exists
|
let tree_opt = parse_as(&bnf_parser, source_text.trim(), "program")
|
||||||
// in the grammar.
|
.or_else(|| parse_as(&bnf_parser, source_text.trim(), "source-file"));
|
||||||
let tree = parse_as(&parser, source.trim(), "program")
|
|
||||||
.or_else(|| parse_as(&parser, source.trim(), "source-file"));
|
|
||||||
|
|
||||||
match tree {
|
if tree_opt.is_none() {
|
||||||
Some(t) => {
|
eprintln!(
|
||||||
eprintln!("ok: {}", path.display());
|
"error: '{}' did not match the RPG IV grammar",
|
||||||
writeln!(output, "=== {} ===", path.display())
|
source_path.display()
|
||||||
.and_then(|_| writeln!(output, "{t}"))
|
);
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("error: write failed: {e}");
|
|
||||||
any_error = true;
|
any_error = true;
|
||||||
});
|
continue;
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
eprintln!("error: '{}' did not match the RPG IV grammar", path.display());
|
// ── --emit-tree: print parse tree and stop ────────────────────────────
|
||||||
|
if cli.emit_tree {
|
||||||
|
println!("=== {} ===", source_path.display());
|
||||||
|
println!("{}", tree_opt.unwrap());
|
||||||
|
eprintln!("ok: {} (parse tree emitted)", source_path.display());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!("ok: {} (BNF valid)", source_path.display());
|
||||||
|
|
||||||
|
// ── Lower to typed AST ────────────────────────────────────────────────
|
||||||
|
let program = match lower(source_text.trim()) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: lowering '{}' failed: {e}", source_path.display());
|
||||||
any_error = true;
|
any_error = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"ok: {} ({} declaration(s), {} procedure(s))",
|
||||||
|
source_path.display(),
|
||||||
|
program.declarations.len(),
|
||||||
|
program.procedures.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── --emit-ir: print LLVM IR and stop ────────────────────────────────
|
||||||
|
if cli.emit_ir {
|
||||||
|
match codegen::emit_ir(&program) {
|
||||||
|
Ok(ir) => {
|
||||||
|
print!("{}", ir);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: IR emission failed for '{}': {e}", source_path.display());
|
||||||
|
any_error = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Determine output path ─────────────────────────────────────────────
|
||||||
|
let out_path = if cli.no_link {
|
||||||
|
// Object file: replace source extension with .o
|
||||||
|
let mut p = cli.output.clone().unwrap_or_else(|| {
|
||||||
|
let mut base = source_path.clone();
|
||||||
|
base.set_extension("o");
|
||||||
|
base
|
||||||
|
});
|
||||||
|
if p.extension().and_then(|e| e.to_str()) != Some("o") {
|
||||||
|
p.set_extension("o");
|
||||||
|
}
|
||||||
|
p
|
||||||
|
} else {
|
||||||
|
// Executable: use -o, or default to a.out
|
||||||
|
cli.output.clone().unwrap_or_else(|| PathBuf::from("a.out"))
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Compile to object file ────────────────────────────────────────────
|
||||||
|
let obj_path: PathBuf = if cli.no_link {
|
||||||
|
out_path.clone()
|
||||||
|
} else {
|
||||||
|
// Temporary object file alongside the final binary.
|
||||||
|
let stem = source_path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("rpg_prog");
|
||||||
|
let mut tmp = std::env::temp_dir();
|
||||||
|
tmp.push(format!("{}.rpg.o", stem));
|
||||||
|
tmp
|
||||||
|
};
|
||||||
|
|
||||||
|
match codegen::compile_to_object(&program, &obj_path, cli.opt_level) {
|
||||||
|
Ok(()) => {
|
||||||
|
eprintln!("ok: object → {}", obj_path.display());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"error: codegen failed for '{}': {e}",
|
||||||
|
source_path.display()
|
||||||
|
);
|
||||||
|
any_error = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Link if requested ─────────────────────────────────────────────────
|
||||||
|
if !cli.no_link {
|
||||||
|
let runtime = find_runtime(cli.runtime.as_deref());
|
||||||
|
match link_executable(&obj_path, &out_path, runtime.as_deref()) {
|
||||||
|
Ok(()) => {
|
||||||
|
eprintln!("ok: executable → {}", out_path.display());
|
||||||
|
// Clean up the temporary object.
|
||||||
|
let _ = fs::remove_file(&obj_path);
|
||||||
|
}
|
||||||
|
Err(msg) => {
|
||||||
|
eprintln!("error: linking failed: {msg}");
|
||||||
|
any_error = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,3 +246,206 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Linker invocation
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Invoke the system C compiler to link `obj_path` into `exe_path`.
|
||||||
|
///
|
||||||
|
/// We use `cc` (which wraps the system linker) rather than calling `ld`
|
||||||
|
/// directly so that the C runtime startup files (`crt0.o`, `crti.o`, etc.) are
|
||||||
|
/// included automatically — this is the same approach Clang uses when building
|
||||||
|
/// executables.
|
||||||
|
fn link_executable(
|
||||||
|
obj_path: &std::path::Path,
|
||||||
|
exe_path: &std::path::Path,
|
||||||
|
runtime: Option<&std::path::Path>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut cmd = process::Command::new("cc");
|
||||||
|
|
||||||
|
cmd.arg(obj_path)
|
||||||
|
.arg("-o")
|
||||||
|
.arg(exe_path);
|
||||||
|
|
||||||
|
// Link against the RPG runtime shared library.
|
||||||
|
match runtime {
|
||||||
|
Some(rt) => {
|
||||||
|
// Explicit path: use -L <dir> -lrpgrt (or pass the .so directly).
|
||||||
|
if rt.is_file() {
|
||||||
|
// Absolute path to the .so — pass directly.
|
||||||
|
cmd.arg(rt);
|
||||||
|
} else if rt.is_dir() {
|
||||||
|
cmd.arg(format!("-L{}", rt.display()))
|
||||||
|
.arg("-lrpgrt");
|
||||||
|
} else {
|
||||||
|
cmd.arg(format!("-L{}", rt.display()))
|
||||||
|
.arg("-lrpgrt");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// No explicit runtime specified — link against libc only.
|
||||||
|
// The program will need librpgrt.so to be in LD_LIBRARY_PATH at
|
||||||
|
// runtime, or the user must build and install it separately.
|
||||||
|
cmd.arg("-lc");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the runtime library to be found at execution time relative to the
|
||||||
|
// executable (rpath tricks).
|
||||||
|
if let Some(rt) = runtime {
|
||||||
|
if let Some(dir) = rt.parent() {
|
||||||
|
let rpath = format!("-Wl,-rpath,{}", dir.display());
|
||||||
|
cmd.arg(rpath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = cmd
|
||||||
|
.status()
|
||||||
|
.map_err(|e| format!("could not run linker `cc`: {e}"))?;
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("`cc` exited with status {}", status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Runtime library discovery
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Search for `librpgrt.so` in well-known locations.
|
||||||
|
///
|
||||||
|
/// Checked in order:
|
||||||
|
/// 1. `RPGRT_LIB` environment variable
|
||||||
|
/// 2. Same directory as the compiler executable
|
||||||
|
/// 3. `target/debug/` or `target/release/` relative to the current directory
|
||||||
|
/// (useful when running via `cargo run`)
|
||||||
|
/// 4. `/usr/local/lib`
|
||||||
|
/// 5. `/usr/lib`
|
||||||
|
fn find_runtime(explicit: Option<&std::path::Path>) -> Option<PathBuf> {
|
||||||
|
// Honour an explicitly supplied path first.
|
||||||
|
if let Some(p) = explicit {
|
||||||
|
return Some(p.to_path_buf());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the environment variable.
|
||||||
|
if let Ok(val) = std::env::var("RPGRT_LIB") {
|
||||||
|
let p = PathBuf::from(val);
|
||||||
|
if p.exists() {
|
||||||
|
return Some(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe standard locations.
|
||||||
|
let candidates = [
|
||||||
|
// Alongside the running binary.
|
||||||
|
std::env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|e| e.parent().map(|d| d.join("librpgrt.so"))),
|
||||||
|
// Cargo target directories.
|
||||||
|
Some(PathBuf::from("target/debug/librpgrt.so")),
|
||||||
|
Some(PathBuf::from("target/release/librpgrt.so")),
|
||||||
|
Some(PathBuf::from("target/debug/deps/librpgrt.so")),
|
||||||
|
// System-wide.
|
||||||
|
Some(PathBuf::from("/usr/local/lib/librpgrt.so")),
|
||||||
|
Some(PathBuf::from("/usr/lib/librpgrt.so")),
|
||||||
|
];
|
||||||
|
|
||||||
|
for candidate in candidates.into_iter().flatten() {
|
||||||
|
if candidate.exists() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Integration smoke test (compile-time only — no process spawning needed)
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rust_langrpg::{codegen::emit_ir, lower::lower};
|
||||||
|
|
||||||
|
/// The hello.rpg from the repository root must compile all the way through
|
||||||
|
/// to LLVM IR without errors.
|
||||||
|
#[test]
|
||||||
|
fn hello_rpg_emits_ir() {
|
||||||
|
let src = include_str!("../hello.rpg");
|
||||||
|
let prog = lower(src.trim()).expect("lower hello.rpg");
|
||||||
|
let ir = emit_ir(&prog).expect("emit_ir hello.rpg");
|
||||||
|
|
||||||
|
// The IR must define at least one function.
|
||||||
|
assert!(
|
||||||
|
ir.contains("define"),
|
||||||
|
"IR should contain at least one function definition:\n{}",
|
||||||
|
&ir[..ir.len().min(1000)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// The IR must reference the dsply runtime call.
|
||||||
|
assert!(
|
||||||
|
ir.contains("rpg_dsply"),
|
||||||
|
"IR should reference rpg_dsply:\n{}",
|
||||||
|
&ir[..ir.len().min(1000)]
|
||||||
|
);
|
||||||
|
|
||||||
|
// There must be a C main() wrapper so the binary is directly executable.
|
||||||
|
assert!(
|
||||||
|
ir.contains("@main"),
|
||||||
|
"IR should contain a @main entry point:\n{}",
|
||||||
|
&ir[..ir.len().min(1000)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A minimal RPG IV program with an integer variable and a loop must
|
||||||
|
/// compile to IR that contains branch instructions (i.e. the loop was
|
||||||
|
/// actually code-generated, not silently dropped).
|
||||||
|
#[test]
|
||||||
|
fn loop_program_emits_branches() {
|
||||||
|
let src = r#"
|
||||||
|
CTL-OPT DFTACTGRP(*NO);
|
||||||
|
|
||||||
|
DCL-S counter INT(10) INZ(0);
|
||||||
|
|
||||||
|
DCL-PROC main EXPORT;
|
||||||
|
DCL-S i INT(10);
|
||||||
|
FOR i = 1 TO 10;
|
||||||
|
counter = counter + i;
|
||||||
|
ENDFOR;
|
||||||
|
RETURN;
|
||||||
|
END-PROC;
|
||||||
|
"#;
|
||||||
|
let prog = lower(src.trim()).expect("lower loop program");
|
||||||
|
let ir = emit_ir(&prog).expect("emit_ir loop program");
|
||||||
|
assert!(
|
||||||
|
ir.contains("br "),
|
||||||
|
"loop IR should contain branch instructions:\n{}",
|
||||||
|
&ir[..ir.len().min(2000)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An IF/ELSE conditional must produce a conditional branch in the IR.
|
||||||
|
#[test]
|
||||||
|
fn conditional_program_emits_conditional_branch() {
|
||||||
|
let src = r#"
|
||||||
|
DCL-PROC check EXPORT;
|
||||||
|
DCL-S x INT(10) INZ(5);
|
||||||
|
IF x = 5;
|
||||||
|
RETURN;
|
||||||
|
ELSE;
|
||||||
|
RETURN;
|
||||||
|
ENDIF;
|
||||||
|
END-PROC;
|
||||||
|
"#;
|
||||||
|
let prog = lower(src.trim()).expect("lower conditional program");
|
||||||
|
let ir = emit_ir(&prog).expect("emit_ir conditional program");
|
||||||
|
assert!(
|
||||||
|
ir.contains("br i1"),
|
||||||
|
"conditional IR should contain 'br i1':\n{}",
|
||||||
|
&ir[..ir.len().min(2000)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
//! Integration tests for the compiler binary against the Hello World program.
|
//! Integration tests for the compiler binary against the Hello World program.
|
||||||
|
//!
|
||||||
|
//! These tests exercise the full compilation pipeline:
|
||||||
|
//! hello.rpg → BNF validation → AST lowering → LLVM codegen → native binary
|
||||||
|
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -25,11 +28,12 @@ fn run(args: &[&str]) -> std::process::Output {
|
|||||||
// Tests
|
// Tests
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// The compiler should exit 0 when given hello.rpg (no -o flag — tree is
|
/// The compiler should exit 0 when given hello.rpg (no -o flag — the output
|
||||||
/// discarded but the parse must still succeed).
|
/// executable is written to a.out but the important thing is no error).
|
||||||
#[test]
|
#[test]
|
||||||
fn hello_rpg_exits_ok() {
|
fn hello_rpg_exits_ok() {
|
||||||
let out = run(&[HELLO_RPG]);
|
let out_path = std::env::temp_dir().join("hello_rpg_exits_ok.out");
|
||||||
|
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
|
||||||
assert!(
|
assert!(
|
||||||
out.status.success(),
|
out.status.success(),
|
||||||
"expected exit 0 for hello.rpg\nstderr: {}",
|
"expected exit 0 for hello.rpg\nstderr: {}",
|
||||||
@@ -37,11 +41,11 @@ fn hello_rpg_exits_ok() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When -o is supplied the output file must be created and contain a non-empty
|
/// When -o is supplied the output file must be created as a non-empty compiled
|
||||||
/// parse tree.
|
/// artifact (executable binary).
|
||||||
#[test]
|
#[test]
|
||||||
fn hello_rpg_writes_tree() {
|
fn hello_rpg_produces_output_file() {
|
||||||
let out_path = std::env::temp_dir().join("hello_rpg_test_tree.txt");
|
let out_path = std::env::temp_dir().join("hello_rpg_test_output.out");
|
||||||
|
|
||||||
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
|
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
|
||||||
|
|
||||||
@@ -51,30 +55,151 @@ fn hello_rpg_writes_tree() {
|
|||||||
String::from_utf8_lossy(&out.stderr),
|
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!(
|
assert!(
|
||||||
!tree.trim().is_empty(),
|
out_path.exists(),
|
||||||
"output file is empty — expected a parse tree",
|
"output file '{}' was not created",
|
||||||
|
out_path.display(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// The tree should reference at least the top-level <program> non-terminal.
|
let metadata = std::fs::metadata(&out_path)
|
||||||
|
.unwrap_or_else(|e| panic!("could not stat output file: {e}"));
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
tree.contains("program"),
|
metadata.len() > 0,
|
||||||
"parse tree does not mention <program>:\n{tree}",
|
"output file is empty — expected a compiled artifact",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The compiler must print the file name to stderr as "ok: hello.rpg" (or the
|
/// The compiler must print the file name to stderr with an "ok:" prefix when
|
||||||
/// full path) when the parse succeeds.
|
/// BNF validation succeeds.
|
||||||
#[test]
|
#[test]
|
||||||
fn hello_rpg_reports_ok_on_stderr() {
|
fn hello_rpg_reports_ok_on_stderr() {
|
||||||
let out = run(&[HELLO_RPG]);
|
let out_path = std::env::temp_dir().join("hello_rpg_reports_ok.out");
|
||||||
|
let out = run(&["-o", out_path.to_str().unwrap(), HELLO_RPG]);
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
assert!(
|
assert!(
|
||||||
stderr.starts_with("ok:"),
|
stderr.contains("ok:"),
|
||||||
"expected stderr to start with 'ok:'\ngot: {stderr}",
|
"expected stderr to contain 'ok:'\ngot: {stderr}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `--emit-ir` must print LLVM IR to stdout and exit 0.
|
||||||
|
///
|
||||||
|
/// The IR must contain:
|
||||||
|
/// * At least one `define` (a function definition)
|
||||||
|
/// * A reference to `rpg_dsply` (the DSPLY runtime call)
|
||||||
|
/// * A `@main` entry point (the C main wrapper)
|
||||||
|
#[test]
|
||||||
|
fn hello_rpg_emit_ir() {
|
||||||
|
let out = run(&["--emit-ir", HELLO_RPG]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"expected exit 0 with --emit-ir\nstderr: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr),
|
||||||
|
);
|
||||||
|
|
||||||
|
let ir = String::from_utf8_lossy(&out.stdout);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ir.contains("define"),
|
||||||
|
"--emit-ir should produce at least one LLVM function definition\nIR:\n{}",
|
||||||
|
&ir[..ir.len().min(2000)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ir.contains("rpg_dsply"),
|
||||||
|
"--emit-ir should reference the rpg_dsply runtime symbol\nIR:\n{}",
|
||||||
|
&ir[..ir.len().min(2000)],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
ir.contains("@main"),
|
||||||
|
"--emit-ir should contain a @main entry-point wrapper\nIR:\n{}",
|
||||||
|
&ir[..ir.len().min(2000)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `--emit-tree` must print the BNF parse tree to stdout and exit 0.
|
||||||
|
///
|
||||||
|
/// The tree must mention `program` (the top-level grammar rule).
|
||||||
|
#[test]
|
||||||
|
fn hello_rpg_emit_tree() {
|
||||||
|
let out = run(&["--emit-tree", HELLO_RPG]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"expected exit 0 with --emit-tree\nstderr: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr),
|
||||||
|
);
|
||||||
|
|
||||||
|
let tree = String::from_utf8_lossy(&out.stdout);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!tree.trim().is_empty(),
|
||||||
|
"--emit-tree output is empty — expected a parse tree",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
tree.contains("program"),
|
||||||
|
"--emit-tree output should reference the <program> rule\n{}",
|
||||||
|
&tree[..tree.len().min(1000)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `--no-link` should produce a `.o` object file and exit 0.
|
||||||
|
#[test]
|
||||||
|
fn hello_rpg_no_link_produces_object() {
|
||||||
|
let obj_path = std::env::temp_dir().join("hello_rpg_test.o");
|
||||||
|
|
||||||
|
let out = run(&["--no-link", "-o", obj_path.to_str().unwrap(), HELLO_RPG]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"expected exit 0 with --no-link\nstderr: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
obj_path.exists(),
|
||||||
|
"object file '{}' was not created",
|
||||||
|
obj_path.display(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let metadata = std::fs::metadata(&obj_path)
|
||||||
|
.unwrap_or_else(|e| panic!("could not stat object file: {e}"));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
metadata.len() > 0,
|
||||||
|
"object file is empty — expected compiled LLVM output",
|
||||||
|
);
|
||||||
|
|
||||||
|
// A valid ELF object file starts with the ELF magic bytes 0x7f 'E' 'L' 'F'.
|
||||||
|
let bytes = std::fs::read(&obj_path)
|
||||||
|
.unwrap_or_else(|e| panic!("could not read object file: {e}"));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
bytes.starts_with(b"\x7fELF"),
|
||||||
|
"expected an ELF object file, got unexpected magic bytes: {:?}",
|
||||||
|
&bytes[..bytes.len().min(4)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Passing a non-existent file should cause the compiler to exit non-zero and
|
||||||
|
/// print an error to stderr.
|
||||||
|
#[test]
|
||||||
|
fn nonexistent_source_exits_error() {
|
||||||
|
let out = run(&["no_such_file_xyz.rpg"]);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!out.status.success(),
|
||||||
|
"expected non-zero exit for a missing source file",
|
||||||
|
);
|
||||||
|
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(
|
||||||
|
stderr.contains("error"),
|
||||||
|
"expected an error message on stderr\ngot: {stderr}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user