add: compiler

This commit is contained in:
2026-03-12 21:41:30 -07:00
parent 90de2206db
commit 3498b018e5
10 changed files with 6190 additions and 78 deletions

21
rpgrt/Cargo.toml Normal file
View 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
View 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);
}
}