add: more samples and make them work
This commit is contained in:
@@ -156,6 +156,59 @@ pub extern "C" fn rpg_dsply_f64(f: f64) {
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_dsply_read — display a prompt and read an integer response from stdin
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Display `prompt` (a null-terminated C string) with a `DSPLY` prefix, then
|
||||
/// read one line from stdin and parse it as a signed 64-bit integer. The
|
||||
/// parsed value is written through `response`.
|
||||
///
|
||||
/// This implements the three-operand form of the `DSPLY` opcode:
|
||||
/// ```rpg
|
||||
/// Dsply prompt ' ' response_var;
|
||||
/// ```
|
||||
/// where the third operand is the variable that receives the operator's reply.
|
||||
///
|
||||
/// If stdin is exhausted (EOF) or the line cannot be parsed as an integer the
|
||||
/// response is left unchanged.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// * `prompt` must be a valid null-terminated C string (or null, treated as
|
||||
/// an empty string).
|
||||
/// * `response` must be a valid, aligned, writable pointer to an `i64`.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn rpg_dsply_read(
|
||||
prompt: *const std::os::raw::c_char,
|
||||
response: *mut i64,
|
||||
) {
|
||||
use std::io::BufRead;
|
||||
|
||||
// Display the prompt.
|
||||
let text = if prompt.is_null() {
|
||||
std::borrow::Cow::Borrowed("")
|
||||
} else {
|
||||
unsafe { CStr::from_ptr(prompt).to_string_lossy() }
|
||||
};
|
||||
{
|
||||
let stdout = io::stdout();
|
||||
let mut out = stdout.lock();
|
||||
let _ = writeln!(out, "DSPLY {}", text);
|
||||
let _ = out.flush();
|
||||
}
|
||||
|
||||
// Read one line from stdin and parse it as i64.
|
||||
let stdin = io::stdin();
|
||||
let mut line = String::new();
|
||||
if stdin.lock().read_line(&mut line).is_ok() {
|
||||
let trimmed = line.trim();
|
||||
if let Ok(n) = trimmed.parse::<i64>() {
|
||||
unsafe { *response = n; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// rpg_halt — abnormal termination
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
44
samples/3np1.rpg
Normal file
44
samples/3np1.rpg
Normal file
@@ -0,0 +1,44 @@
|
||||
**FREE
|
||||
Ctl-Opt Main(ThreeNPlusOne);
|
||||
|
||||
Dcl-Proc ThreeNPlusOne;
|
||||
Dcl-S n Packed(10);
|
||||
Dcl-S counter Int(10) Inz(0);
|
||||
Dcl-S input_prompt VarChar(50) Inz('Enter a positive integer (or 0 to exit):');
|
||||
|
||||
// Use an infinite loop, exit when user enters 0
|
||||
Dow (1 = 1);
|
||||
Dsply input_prompt ' ' n;
|
||||
|
||||
If n = 0;
|
||||
Leave;
|
||||
EndIf;
|
||||
|
||||
If n < 0;
|
||||
input_prompt = 'Positive integers only. Enter a number:';
|
||||
Iter;
|
||||
EndIf;
|
||||
|
||||
// Start sequence calculation
|
||||
input_prompt = 'Enter a positive integer (or 0 to exit):'; // Reset prompt
|
||||
counter = 0;
|
||||
Dsply ('Sequence for ' + %Char(n) + ':');
|
||||
|
||||
Dow n > 1;
|
||||
If %Rem(n:2) = 0;
|
||||
// n is even, divide by 2
|
||||
n = n / 2;
|
||||
Else;
|
||||
// n is odd, multiply by 3 and add 1
|
||||
n = (n * 3) + 1;
|
||||
EndIf;
|
||||
|
||||
counter = counter + 1;
|
||||
Dsply %Char(n);
|
||||
EndDo;
|
||||
|
||||
Dsply ('Reached 1 in ' + %Char(counter) + ' iterations.');
|
||||
Dsply ' '; // Add a blank line for readability
|
||||
EndDo;
|
||||
|
||||
End-Proc ThreeNPlusOne;
|
||||
8
samples/3np1.rpg.golden
Normal file
8
samples/3np1.rpg.golden
Normal file
@@ -0,0 +1,8 @@
|
||||
DSPLY
|
||||
DSPLY Sequence for 8:
|
||||
DSPLY 4
|
||||
DSPLY 2
|
||||
DSPLY 1
|
||||
DSPLY Reached 1 in 3 iterations.
|
||||
DSPLY
|
||||
DSPLY
|
||||
18
samples/fizzbuzz.rpg
Normal file
18
samples/fizzbuzz.rpg
Normal file
@@ -0,0 +1,18 @@
|
||||
**FREE
|
||||
Ctl-Opt Main(FizzBuzz);
|
||||
|
||||
Dcl-Proc FizzBuzz;
|
||||
Dcl-S num Int(10);
|
||||
|
||||
For num = 1 To 100;
|
||||
If %Rem(num:3) = 0 And %Rem(num:5) = 0;
|
||||
Dsply ('num - ' + %Char(num) + ' FIZZBUZZ');
|
||||
ElseIf %Rem(num:3) = 0;
|
||||
Dsply ('num - ' + %Char(num) + ' FIZZ');
|
||||
ElseIf %Rem(num:5) = 0;
|
||||
Dsply ('num - ' + %Char(num) + ' BUZZ');
|
||||
Else;
|
||||
Dsply ('num - ' + %Char(num));
|
||||
EndIf;
|
||||
EndFor;
|
||||
End-Proc FizzBuzz;
|
||||
100
samples/fizzbuzz.rpg.golden
Normal file
100
samples/fizzbuzz.rpg.golden
Normal file
@@ -0,0 +1,100 @@
|
||||
DSPLY num - 1
|
||||
DSPLY num - 2
|
||||
DSPLY num - 3 FIZZ
|
||||
DSPLY num - 4
|
||||
DSPLY num - 5 BUZZ
|
||||
DSPLY num - 6 FIZZ
|
||||
DSPLY num - 7
|
||||
DSPLY num - 8
|
||||
DSPLY num - 9 FIZZ
|
||||
DSPLY num - 10 BUZZ
|
||||
DSPLY num - 11
|
||||
DSPLY num - 12 FIZZ
|
||||
DSPLY num - 13
|
||||
DSPLY num - 14
|
||||
DSPLY num - 15 FIZZBUZZ
|
||||
DSPLY num - 16
|
||||
DSPLY num - 17
|
||||
DSPLY num - 18 FIZZ
|
||||
DSPLY num - 19
|
||||
DSPLY num - 20 BUZZ
|
||||
DSPLY num - 21 FIZZ
|
||||
DSPLY num - 22
|
||||
DSPLY num - 23
|
||||
DSPLY num - 24 FIZZ
|
||||
DSPLY num - 25 BUZZ
|
||||
DSPLY num - 26
|
||||
DSPLY num - 27 FIZZ
|
||||
DSPLY num - 28
|
||||
DSPLY num - 29
|
||||
DSPLY num - 30 FIZZBUZZ
|
||||
DSPLY num - 31
|
||||
DSPLY num - 32
|
||||
DSPLY num - 33 FIZZ
|
||||
DSPLY num - 34
|
||||
DSPLY num - 35 BUZZ
|
||||
DSPLY num - 36 FIZZ
|
||||
DSPLY num - 37
|
||||
DSPLY num - 38
|
||||
DSPLY num - 39 FIZZ
|
||||
DSPLY num - 40 BUZZ
|
||||
DSPLY num - 41
|
||||
DSPLY num - 42 FIZZ
|
||||
DSPLY num - 43
|
||||
DSPLY num - 44
|
||||
DSPLY num - 45 FIZZBUZZ
|
||||
DSPLY num - 46
|
||||
DSPLY num - 47
|
||||
DSPLY num - 48 FIZZ
|
||||
DSPLY num - 49
|
||||
DSPLY num - 50 BUZZ
|
||||
DSPLY num - 51 FIZZ
|
||||
DSPLY num - 52
|
||||
DSPLY num - 53
|
||||
DSPLY num - 54 FIZZ
|
||||
DSPLY num - 55 BUZZ
|
||||
DSPLY num - 56
|
||||
DSPLY num - 57 FIZZ
|
||||
DSPLY num - 58
|
||||
DSPLY num - 59
|
||||
DSPLY num - 60 FIZZBUZZ
|
||||
DSPLY num - 61
|
||||
DSPLY num - 62
|
||||
DSPLY num - 63 FIZZ
|
||||
DSPLY num - 64
|
||||
DSPLY num - 65 BUZZ
|
||||
DSPLY num - 66 FIZZ
|
||||
DSPLY num - 67
|
||||
DSPLY num - 68
|
||||
DSPLY num - 69 FIZZ
|
||||
DSPLY num - 70 BUZZ
|
||||
DSPLY num - 71
|
||||
DSPLY num - 72 FIZZ
|
||||
DSPLY num - 73
|
||||
DSPLY num - 74
|
||||
DSPLY num - 75 FIZZBUZZ
|
||||
DSPLY num - 76
|
||||
DSPLY num - 77
|
||||
DSPLY num - 78 FIZZ
|
||||
DSPLY num - 79
|
||||
DSPLY num - 80 BUZZ
|
||||
DSPLY num - 81 FIZZ
|
||||
DSPLY num - 82
|
||||
DSPLY num - 83
|
||||
DSPLY num - 84 FIZZ
|
||||
DSPLY num - 85 BUZZ
|
||||
DSPLY num - 86
|
||||
DSPLY num - 87 FIZZ
|
||||
DSPLY num - 88
|
||||
DSPLY num - 89
|
||||
DSPLY num - 90 FIZZBUZZ
|
||||
DSPLY num - 91
|
||||
DSPLY num - 92
|
||||
DSPLY num - 93 FIZZ
|
||||
DSPLY num - 94
|
||||
DSPLY num - 95 BUZZ
|
||||
DSPLY num - 96 FIZZ
|
||||
DSPLY num - 97
|
||||
DSPLY num - 98
|
||||
DSPLY num - 99 FIZZ
|
||||
DSPLY num - 100 BUZZ
|
||||
122
src/codegen.rs
122
src/codegen.rs
@@ -316,6 +316,15 @@ impl<'ctx> Codegen<'ctx> {
|
||||
// i8* rpg_concat(i8* a, i8* b) — concatenate two C strings
|
||||
let concat_ty = i8_ptr.fn_type(&[i8_ptr.into(), i8_ptr.into()], false);
|
||||
self.module.add_function("rpg_concat", concat_ty, None);
|
||||
|
||||
// void rpg_dsply_read(const char *prompt, i64 *response)
|
||||
// Three-operand DSPLY: display prompt and read an i64 from stdin.
|
||||
let i64_ptr = self.context.ptr_type(AddressSpace::default());
|
||||
let dsply_read_ty = void_t.fn_type(
|
||||
&[i8_ptr.into(), i64_ptr.into()],
|
||||
false,
|
||||
);
|
||||
self.module.add_function("rpg_dsply_read", dsply_read_ty, None);
|
||||
}
|
||||
|
||||
// ── Global declarations ─────────────────────────────────────────────────
|
||||
@@ -474,6 +483,49 @@ impl<'ctx> Codegen<'ctx> {
|
||||
self.builder.build_alloca(arr_ty, name).unwrap()
|
||||
}
|
||||
|
||||
/// Allocate an `i64` slot in the **entry block** of `func`, regardless of
|
||||
/// where the builder is currently positioned.
|
||||
///
|
||||
/// LLVM's `mem2reg` pass (and general best-practice) requires that all
|
||||
/// `alloca` instructions live in the function entry block. When code is
|
||||
/// generated inside a loop or branch the builder's insertion point is not
|
||||
/// the entry block; calling `build_alloca` there produces an alloca that is
|
||||
/// re-executed on every iteration, creating a new stack slot each time and
|
||||
/// losing any value stored in a previous iteration.
|
||||
///
|
||||
/// This helper saves the current insertion point, moves to the first
|
||||
/// instruction of the entry block (so the alloca is prepended before any
|
||||
/// existing code), emits the alloca, then restores the original position.
|
||||
fn alloca_i64_in_entry(
|
||||
&self,
|
||||
func: inkwell::values::FunctionValue<'ctx>,
|
||||
name: &str,
|
||||
) -> PointerValue<'ctx> {
|
||||
let i64_t = self.context.i64_type();
|
||||
let entry_bb = func.get_first_basic_block().expect("function has no entry block");
|
||||
|
||||
// Remember where we are now.
|
||||
let saved_bb = self.builder.get_insert_block();
|
||||
|
||||
// Position at the very start of the entry block so the alloca is
|
||||
// placed before any other instructions (branches, stores, etc.).
|
||||
match entry_bb.get_first_instruction() {
|
||||
Some(first) => self.builder.position_before(&first),
|
||||
None => self.builder.position_at_end(entry_bb),
|
||||
}
|
||||
|
||||
let ptr = self.builder.build_alloca(i64_t, name).unwrap();
|
||||
|
||||
// Restore the builder's original position.
|
||||
// We always generate code at the end of the current block, so
|
||||
// position_at_end on the saved block is sufficient.
|
||||
if let Some(bb) = saved_bb {
|
||||
self.builder.position_at_end(bb);
|
||||
}
|
||||
|
||||
ptr
|
||||
}
|
||||
|
||||
/// Allocate storage for an array of `n` elements of type `ty`.
|
||||
fn alloca_for_type_dim(&self, ty: &TypeSpec, name: &str, n: u64) -> PointerValue<'ctx> {
|
||||
let elem_size = ty.byte_size().unwrap_or(8) as u32;
|
||||
@@ -777,6 +829,69 @@ impl<'ctx> Codegen<'ctx> {
|
||||
let dsply = self.module.get_function("rpg_dsply")
|
||||
.ok_or_else(|| CodegenError::new("rpg_dsply not declared"))?;
|
||||
|
||||
// ── Three-operand form: DSPLY expr msgq response ──────────────────
|
||||
// When a response variable is present we display the prompt and then
|
||||
// read an integer from stdin into the response variable.
|
||||
if let Some(resp_name) = &d.response {
|
||||
// Coerce the prompt expression to a C string pointer.
|
||||
let prompt_ptr = match &d.expr {
|
||||
Expression::Variable(qname) => {
|
||||
let name = qname.leaf();
|
||||
if let Some((ptr, _)) = self.resolve_var(name, state) {
|
||||
ptr.into()
|
||||
} else {
|
||||
self.intern_string("").into()
|
||||
}
|
||||
}
|
||||
Expression::Literal(Literal::String(s)) => {
|
||||
self.intern_string(s).into()
|
||||
}
|
||||
other => {
|
||||
if let Ok(BasicValueEnum::PointerValue(ptr)) =
|
||||
self.gen_expression(other, state)
|
||||
{
|
||||
ptr.into()
|
||||
} else {
|
||||
self.intern_string("").into()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Resolve the response variable; allocate a fresh i64 slot in the
|
||||
// function entry block if not already present. Using
|
||||
// alloca_i64_in_entry ensures the slot is created once regardless
|
||||
// of how many times the DSPLY statement is executed (e.g. inside a
|
||||
// loop), so the stored value persists across iterations.
|
||||
let resp_ptr: inkwell::values::PointerValue =
|
||||
if let Some((ptr, TypeSpec::Int(n))) = state.locals.get(resp_name) {
|
||||
if matches!(n.as_ref(), Expression::Literal(Literal::Integer(20))) {
|
||||
*ptr
|
||||
} else {
|
||||
let p = self.alloca_i64_in_entry(state.function, resp_name);
|
||||
state.locals.insert(
|
||||
resp_name.clone(),
|
||||
(p, TypeSpec::Int(Box::new(Expression::Literal(Literal::Integer(20))))),
|
||||
);
|
||||
p
|
||||
}
|
||||
} else {
|
||||
let p = self.alloca_i64_in_entry(state.function, resp_name);
|
||||
state.locals.insert(
|
||||
resp_name.clone(),
|
||||
(p, TypeSpec::Int(Box::new(Expression::Literal(Literal::Integer(20))))),
|
||||
);
|
||||
p
|
||||
};
|
||||
|
||||
if let Some(read_fn) = self.module.get_function("rpg_dsply_read") {
|
||||
self.builder
|
||||
.build_call(read_fn, &[prompt_ptr, resp_ptr.into()], "dsply_read")
|
||||
.ok();
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// ── One-operand form: DSPLY expr ──────────────────────────────────
|
||||
match &d.expr {
|
||||
Expression::Variable(qname) => {
|
||||
// Look up the variable, then pass ptr + len.
|
||||
@@ -1029,11 +1144,12 @@ impl<'ctx> Codegen<'ctx> {
|
||||
*ptr
|
||||
} else {
|
||||
// Declared as a narrower int (e.g. INT(10) = 4 bytes).
|
||||
// Allocate a fresh 8-byte slot; locals will be updated below.
|
||||
self.builder.build_alloca(i64_t, &f.var).unwrap()
|
||||
// Allocate a fresh 8-byte slot in the entry block so it is
|
||||
// not re-created on every loop iteration.
|
||||
self.alloca_i64_in_entry(func, &f.var)
|
||||
}
|
||||
}
|
||||
_ => self.builder.build_alloca(i64_t, &f.var).unwrap(),
|
||||
_ => self.alloca_i64_in_entry(func, &f.var),
|
||||
};
|
||||
let start = self.gen_expression(&f.start, state)?;
|
||||
let start_i = self.coerce_to_i64(start);
|
||||
|
||||
67
src/lower.rs
67
src/lower.rs
@@ -1264,8 +1264,12 @@ impl Parser {
|
||||
self.advance();
|
||||
self.expect(&Token::LParen)?;
|
||||
let digits = self.parse_expression()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
let decimals = self.parse_expression()?;
|
||||
// Decimal positions are optional — `Packed(10)` means `Packed(10:0)`.
|
||||
let decimals = if self.eat(&Token::Colon) {
|
||||
self.parse_expression()?
|
||||
} else {
|
||||
Expression::Literal(Literal::Integer(0))
|
||||
};
|
||||
self.expect(&Token::RParen)?;
|
||||
Ok(TypeSpec::Packed(Box::new(digits), Box::new(decimals)))
|
||||
}
|
||||
@@ -1273,8 +1277,12 @@ impl Parser {
|
||||
self.advance();
|
||||
self.expect(&Token::LParen)?;
|
||||
let digits = self.parse_expression()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
let decimals = self.parse_expression()?;
|
||||
// Decimal positions are optional — `Zoned(10)` means `Zoned(10:0)`.
|
||||
let decimals = if self.eat(&Token::Colon) {
|
||||
self.parse_expression()?
|
||||
} else {
|
||||
Expression::Literal(Literal::Integer(0))
|
||||
};
|
||||
self.expect(&Token::RParen)?;
|
||||
Ok(TypeSpec::Zoned(Box::new(digits), Box::new(decimals)))
|
||||
}
|
||||
@@ -1282,8 +1290,12 @@ impl Parser {
|
||||
self.advance();
|
||||
self.expect(&Token::LParen)?;
|
||||
let digits = self.parse_expression()?;
|
||||
self.expect(&Token::Colon)?;
|
||||
let decimals = self.parse_expression()?;
|
||||
// Decimal positions are optional — `Bindec(10)` means `Bindec(10:0)`.
|
||||
let decimals = if self.eat(&Token::Colon) {
|
||||
self.parse_expression()?
|
||||
} else {
|
||||
Expression::Literal(Literal::Integer(0))
|
||||
};
|
||||
self.expect(&Token::RParen)?;
|
||||
Ok(TypeSpec::Bindec(Box::new(digits), Box::new(decimals)))
|
||||
}
|
||||
@@ -1627,28 +1639,21 @@ impl Parser {
|
||||
|
||||
fn parse_dsply(&mut self) -> Result<Statement, LowerError> {
|
||||
self.advance(); // KwDsply
|
||||
// Two forms:
|
||||
// Three forms:
|
||||
// DSPLY expr;
|
||||
// DSPLY (expr : msgq : response);
|
||||
// DSPLY (expr : msgq : response); ← parenthesised colon-separated
|
||||
// DSPLY expr msgq response; ← space-separated (no parens)
|
||||
if self.peek() == &Token::LParen {
|
||||
// peek ahead — if the next token after '(' looks like an expression
|
||||
// followed by ':' it's the three-arg form
|
||||
self.advance(); // (
|
||||
let expr = self.parse_expression()?;
|
||||
let mut msg_q = None;
|
||||
let mut response = None;
|
||||
if self.eat(&Token::Colon) {
|
||||
if let Token::Identifier(s) = self.peek().clone() {
|
||||
self.advance();
|
||||
msg_q = Some(s);
|
||||
} else {
|
||||
self.eat(&Token::Colon);
|
||||
}
|
||||
// Accept any name-like token for msgq / response, including
|
||||
// tokens that collide with keywords (e.g. a variable `n`).
|
||||
msg_q = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
|
||||
if self.eat(&Token::Colon) {
|
||||
if let Token::Identifier(s) = self.peek().clone() {
|
||||
self.advance();
|
||||
response = Some(s);
|
||||
}
|
||||
response = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
|
||||
}
|
||||
}
|
||||
self.eat(&Token::RParen);
|
||||
@@ -1656,8 +1661,28 @@ impl Parser {
|
||||
Ok(Statement::Dsply(DsplyStmt { expr, msg_q, response }))
|
||||
} else {
|
||||
let expr = self.parse_expression()?;
|
||||
// Space-separated msgq and response operands (no parentheses):
|
||||
// DSPLY prompt ' ' response_var;
|
||||
// DSPLY prompt msgq response_var;
|
||||
// After the expression, a string literal or another identifier
|
||||
// signals the optional msgq operand, followed by the response var.
|
||||
let mut msg_q = None;
|
||||
let mut response = None;
|
||||
match self.peek().clone() {
|
||||
Token::StringLit(s) => {
|
||||
self.advance();
|
||||
msg_q = Some(s);
|
||||
// Optional response variable follows the msgq.
|
||||
response = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
|
||||
}
|
||||
Token::Identifier(_) => {
|
||||
msg_q = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
|
||||
response = self.try_parse_ident_or_name().map(|s| s.to_lowercase());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
self.eat_semicolon();
|
||||
Ok(Statement::Dsply(DsplyStmt { expr, msg_q: None, response: None }))
|
||||
Ok(Statement::Dsply(DsplyStmt { expr, msg_q, response }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
119
tests/samples.rs
119
tests/samples.rs
@@ -3,15 +3,22 @@
|
||||
//!
|
||||
//! Each test follows the same three-step pipeline:
|
||||
//! 1. `rust-langrpg -o <tmp> <sample>.rpg` — compile
|
||||
//! 2. `<tmp>` — execute
|
||||
//! 2. `<tmp>` — execute (optionally with stdin)
|
||||
//! 3. compare stdout with `<sample>.rpg.golden`
|
||||
//!
|
||||
//! # Adding a new sample
|
||||
//!
|
||||
//! Drop the `.rpg` source and a `.rpg.golden` file into `samples/` and add a
|
||||
//! one-line `#[test]` that calls `run_sample("your_file.rpg")`.
|
||||
//! `#[test]` that calls either:
|
||||
//! * `run_sample("your_file.rpg")` — no stdin input
|
||||
//! * `run_sample_stdin("your_file.rpg", b"input\n")` — bytes fed to stdin
|
||||
|
||||
use std::{fs, path::PathBuf, process::Command};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write as _,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Path to the freshly-built compiler binary, injected by Cargo.
|
||||
const BIN: &str = env!("CARGO_BIN_EXE_rust-langrpg");
|
||||
@@ -23,12 +30,28 @@ const SAMPLES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/samples");
|
||||
// Shared driver
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Compile `sample_name` from `samples/`, run the resulting binary, and assert
|
||||
/// Compile `sample_name` from `samples/`, run it with no stdin, and assert
|
||||
/// that its stdout matches `samples/<sample_name>.golden` line-for-line.
|
||||
///
|
||||
/// Temp binaries are written to the OS temp directory with a name derived from
|
||||
/// the sample so parallel test execution doesn't cause collisions.
|
||||
fn run_sample(sample_name: &str) {
|
||||
run_sample_inner(sample_name, &[]);
|
||||
}
|
||||
|
||||
/// Like [`run_sample`] but feeds `stdin_bytes` to the program's standard input.
|
||||
///
|
||||
/// Use this for programs that read operator responses via the `DSPLY … response`
|
||||
/// opcode. Pass every line the program will request, each terminated by `\n`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// run_sample_stdin("3np1.rpg", b"8\n0\n");
|
||||
/// ```
|
||||
fn run_sample_stdin(sample_name: &str, stdin_bytes: &[u8]) {
|
||||
run_sample_inner(sample_name, stdin_bytes);
|
||||
}
|
||||
|
||||
/// Internal driver shared by [`run_sample`] and [`run_sample_stdin`].
|
||||
fn run_sample_inner(sample_name: &str, stdin_bytes: &[u8]) {
|
||||
let src = PathBuf::from(SAMPLES_DIR).join(sample_name);
|
||||
let golden_path = PathBuf::from(SAMPLES_DIR).join(format!("{}.golden", sample_name));
|
||||
|
||||
@@ -46,9 +69,11 @@ fn run_sample(sample_name: &str) {
|
||||
|
||||
// ── 1. Compile ────────────────────────────────────────────────────────────
|
||||
|
||||
// Sanitise the stem so it is safe to embed in a file name (e.g. "for.rpg"
|
||||
// → "for_rpg") and prefix with a fixed string to make it easy to identify.
|
||||
let safe_stem = sample_name.replace('.', "_").replace(std::path::MAIN_SEPARATOR, "_");
|
||||
// Sanitise the name so it is safe to use in a file path (e.g. "for.rpg"
|
||||
// becomes "for_rpg") and prefix so artefacts are easy to identify.
|
||||
let safe_stem = sample_name
|
||||
.replace('.', "_")
|
||||
.replace(std::path::MAIN_SEPARATOR, "_");
|
||||
let exe = std::env::temp_dir().join(format!("rpg_sample_test_{}.out", safe_stem));
|
||||
|
||||
let compile_out = Command::new(BIN)
|
||||
@@ -73,9 +98,35 @@ fn run_sample(sample_name: &str) {
|
||||
|
||||
// ── 2. Execute ────────────────────────────────────────────────────────────
|
||||
|
||||
let run_out = Command::new(&exe)
|
||||
let run_out = if stdin_bytes.is_empty() {
|
||||
// No input needed — let stdin inherit /dev/null so the process never
|
||||
// blocks waiting for a read that will never come.
|
||||
Command::new(&exe)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap_or_else(|e| panic!("failed to run compiled binary for '{}': {e}", sample_name));
|
||||
.unwrap_or_else(|e| panic!("failed to run '{}': {e}", sample_name))
|
||||
} else {
|
||||
// Pipe the provided bytes to the program's stdin.
|
||||
let mut child = Command::new(&exe)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap_or_else(|e| panic!("failed to spawn '{}': {e}", sample_name));
|
||||
|
||||
// Write all input at once; the programs we test read only a handful of
|
||||
// lines so this never fills the OS pipe buffer.
|
||||
child
|
||||
.stdin
|
||||
.take()
|
||||
.expect("stdin was piped")
|
||||
.write_all(stdin_bytes)
|
||||
.unwrap_or_else(|e| panic!("failed to write stdin for '{}': {e}", sample_name));
|
||||
|
||||
child
|
||||
.wait_with_output()
|
||||
.unwrap_or_else(|e| panic!("failed to wait on '{}': {e}", sample_name))
|
||||
};
|
||||
|
||||
assert!(
|
||||
run_out.status.success(),
|
||||
@@ -89,23 +140,31 @@ fn run_sample(sample_name: &str) {
|
||||
// ── 3. Compare against golden ─────────────────────────────────────────────
|
||||
|
||||
let actual_raw = String::from_utf8_lossy(&run_out.stdout);
|
||||
let expected_raw = fs::read_to_string(&golden_path).unwrap_or_else(|e| {
|
||||
panic!("could not read golden file '{}': {e}", golden_path.display())
|
||||
});
|
||||
let expected_raw = fs::read_to_string(&golden_path)
|
||||
.unwrap_or_else(|e| panic!("could not read golden '{}': {e}", golden_path.display()));
|
||||
|
||||
// Normalise both sides: trim trailing whitespace from every line and ignore
|
||||
// a trailing blank line, so golden files don't need a precise final newline.
|
||||
// Normalise both sides: trim trailing whitespace per line and ignore a
|
||||
// lone trailing blank line so golden files don't need a precise final newline.
|
||||
let normalise = |s: &str| -> Vec<String> {
|
||||
s.lines().map(|l| l.trim_end().to_string()).collect()
|
||||
let mut lines: Vec<String> = s.lines().map(|l| l.trim_end().to_string()).collect();
|
||||
if lines.last().map(|l| l.is_empty()).unwrap_or(false) {
|
||||
lines.pop();
|
||||
}
|
||||
lines
|
||||
};
|
||||
|
||||
let actual_lines = normalise(&actual_raw);
|
||||
let expected_lines = normalise(&expected_raw);
|
||||
|
||||
if actual_lines != expected_lines {
|
||||
if actual_lines == expected_lines {
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Build a readable diff-style failure message ───────────────────────────
|
||||
|
||||
let mut msg = format!(
|
||||
"stdout mismatch for '{}'\n\n\
|
||||
── expected ({} line{}) ──────────────────────\n",
|
||||
── expected ({} line{}) ─────────────────────────────────\n",
|
||||
sample_name,
|
||||
expected_lines.len(),
|
||||
if expected_lines.len() == 1 { "" } else { "s" },
|
||||
@@ -116,7 +175,7 @@ fn run_sample(sample_name: &str) {
|
||||
msg.push('\n');
|
||||
}
|
||||
msg.push_str(&format!(
|
||||
"\n── actual ({} line{}) ────────────────────────\n",
|
||||
"\n── actual ({} line{}) ─────────────────────────────────\n",
|
||||
actual_lines.len(),
|
||||
if actual_lines.len() == 1 { "" } else { "s" },
|
||||
));
|
||||
@@ -126,7 +185,7 @@ fn run_sample(sample_name: &str) {
|
||||
msg.push('\n');
|
||||
}
|
||||
|
||||
// Highlight the first diverging line to make failures easy to spot.
|
||||
// Point at the first diverging line to make the failure easy to locate.
|
||||
for (i, (exp, act)) in expected_lines.iter().zip(actual_lines.iter()).enumerate() {
|
||||
if exp != act {
|
||||
msg.push_str(&format!(
|
||||
@@ -140,14 +199,13 @@ fn run_sample(sample_name: &str) {
|
||||
}
|
||||
if actual_lines.len() != expected_lines.len() {
|
||||
msg.push_str(&format!(
|
||||
"\nline count differs: expected {}, got {}\n",
|
||||
"\nline count: expected {}, got {}\n",
|
||||
expected_lines.len(),
|
||||
actual_lines.len(),
|
||||
));
|
||||
}
|
||||
|
||||
panic!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
@@ -168,3 +226,16 @@ fn sample_for() {
|
||||
fn sample_fib() {
|
||||
run_sample("fib.rpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_fizzbuzz() {
|
||||
run_sample("fizzbuzz.rpg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_3np1() {
|
||||
// Test the Collatz (3n+1) program with a starting value of 8.
|
||||
// The sequence 8 → 4 → 2 → 1 takes 3 steps.
|
||||
// A second input of 0 tells the outer loop to exit.
|
||||
run_sample_stdin("3np1.rpg", b"8\n0\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user