add: compiler
This commit is contained in:
@@ -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}",
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user