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

View File

@@ -1,4 +1,7 @@
//! 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;
@@ -25,11 +28,12 @@ fn run(args: &[&str]) -> std::process::Output {
// Tests
// ─────────────────────────────────────────────────────────────────────────────
/// The compiler should exit 0 when given hello.rpg (no -o flag — tree is
/// discarded but the parse must still succeed).
/// The compiler should exit 0 when given hello.rpg (no -o flag — the output
/// executable is written to a.out but the important thing is no error).
#[test]
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!(
out.status.success(),
"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
/// parse tree.
/// When -o is supplied the output file must be created as a non-empty compiled
/// artifact (executable binary).
#[test]
fn hello_rpg_writes_tree() {
let out_path = std::env::temp_dir().join("hello_rpg_test_tree.txt");
fn hello_rpg_produces_output_file() {
let out_path = std::env::temp_dir().join("hello_rpg_test_output.out");
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),
);
let tree = std::fs::read_to_string(&out_path)
.unwrap_or_else(|e| panic!("could not read output file '{}': {e}", out_path.display()));
assert!(
!tree.trim().is_empty(),
"output file is empty — expected a parse tree",
out_path.exists(),
"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!(
tree.contains("program"),
"parse tree does not mention <program>:\n{tree}",
metadata.len() > 0,
"output file is empty — expected a compiled artifact",
);
}
/// The compiler must print the file name to stderr as "ok: hello.rpg" (or the
/// full path) when the parse succeeds.
/// The compiler must print the file name to stderr with an "ok:" prefix when
/// BNF validation succeeds.
#[test]
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);
assert!(
stderr.starts_with("ok:"),
"expected stderr to start with 'ok:'\ngot: {stderr}",
stderr.contains("ok:"),
"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}",
);
}