You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
5.1 KiB
5.1 KiB
+++ title = "§17 Testing the Compiler" priority = 5 status = "todo" ticket_type = "task" dependencies = [] +++
§17 Testing the Compiler — Stub to fill
File: edu/src/lisp-compiler.md, section ### 17. Testing the Compiler
Replace the stub line with full content. Target 700–900 words. Add unit tests per stage and integration tests that compile MiniLisp → C → binary → run and assert on output.
Learning objectives
- Know which unit tests to write for each compiler stage
- Write integration tests that invoke
ccand run the result - Build a test corpus covering all language features
- Understand how to test error paths (invalid programs should produce errors, not panics)
Content to write
Testing strategy
Three levels:
- Unit tests per module: already written in §8–15 for individual parsers and code-gen functions. This section consolidates and supplements them.
compile()round-trip tests: callcompile(source)and assert on the C output string (does it contain expected fragments?).- Integration tests: compile → write to temp file →
cc→ run → assert stdout.
Additional unit tests
Supplement the tests from earlier sections with edge cases:
Parser edge cases:
#[test] fn empty_program() { assert_eq!(parse("").unwrap(), vec![]); }
#[test] fn only_comments() { assert!(parse("; hello\n; world").unwrap().is_empty()); }
#[test] fn nested_parens() { parse("(f (g (h 1)))").unwrap(); }
#[test] fn string_escapes() { assert_eq!(parse(r#""\n""#).unwrap()[0], Expr::Str("\n".into())); }
Analyser error cases:
#[test] fn undefined_rejects() { assert!(analyse(parse("x").unwrap()).is_err()); }
#[test] fn bad_arity_rejects() { assert!(analyse(parse("(+ 1 2 3)").unwrap()).is_err()); }
#[test] fn valid_mutual_rec() { assert!(analyse(parse(MUTUAL_REC_SRC).unwrap()).is_ok()); }
Integration tests
Write an integration test helper that:
- Calls
compile(source)to get C source - Writes C source to a temp file (
/tmp/ml_test_{N}.c) - Invokes
cc -o /tmp/ml_test_{N} /tmp/ml_test_{N}.c - Runs the binary and captures stdout
- Asserts stdout matches expected output
- Cleans up temp files
#[cfg(test)]
fn run_minilisp(source: &str) -> String {
use std::process::Command;
let c_source = crate::compile(source).expect("compile failed");
let c_path = "/tmp/ml_test.c";
let bin_path = "/tmp/ml_test";
std::fs::write(c_path, &c_source).unwrap();
let cc_status = Command::new("cc")
.args([c_path, "-o", bin_path])
.status()
.expect("cc not found");
assert!(cc_status.success(), "C compilation failed:\n{}", c_source);
let output = Command::new(bin_path).output().expect("run failed");
String::from_utf8(output.stdout).unwrap()
}
Test corpus
Write integration tests for each major language feature:
#[test]
fn test_display_integer() {
assert_eq!(run_minilisp("(display 42)(newline)"), "42\n");
}
#[test]
fn test_arithmetic() {
assert_eq!(run_minilisp("(display (+ (* 3 4) (- 10 5)))(newline)"), "17\n");
}
#[test]
fn test_boolean_if() {
let src = "(display (if #t 1 2))(newline)";
assert_eq!(run_minilisp(src), "1\n");
}
#[test]
fn test_recursive_factorial() {
let src = r#"
(define (factorial n)
(if (= n 0) 1 (* n (factorial (- n 1)))))
(display (factorial 10))(newline)
"#;
assert_eq!(run_minilisp(src), "3628800\n");
}
#[test]
fn test_let() {
let src = "(display (let ((x 3) (y 4)) (+ x y)))(newline)";
assert_eq!(run_minilisp(src), "7\n");
}
#[test]
fn test_mutual_recursion() {
let src = r#"
(define (even? n) (if (= n 0) #t (odd? (- n 1))))
(define (odd? n) (if (= n 0) #f (even? (- n 1))))
(display (even? 10))(newline)
(display (odd? 7))(newline)
"#;
assert_eq!(run_minilisp(src), "true\ntrue\n");
}
#[test]
fn test_higher_order() {
let src = r#"
(define (apply f x) (f x))
(define (double x) (* x 2))
(display (apply double 5))(newline)
"#;
assert_eq!(run_minilisp(src), "10\n");
}
#[test]
fn test_begin() {
let src = "(begin (display 1)(display 2)(display 3)(newline))";
assert_eq!(run_minilisp(src), "123\n");
}
Testing error paths
#[test]
fn test_undefined_symbol_error() {
assert!(crate::compile("(display undefined-var)").is_err());
}
#[test]
fn test_unmatched_paren_error() {
assert!(crate::compile("(define x 1").is_err());
}
#[test]
fn test_wrong_arity_error() {
assert!(crate::compile("(+ 1 2 3)").is_err());
}
Running the tests
cargo test
Note: integration tests require cc (or gcc/clang) to be installed. On a system without a C compiler, they will panic with "cc not found". Consider gating them with #[ignore] or a feature flag.
Style notes
- The
run_minilisphelper is the key piece of infrastructure — spend time on it - The test corpus should cover every special form (define, lambda, if, let, begin) and every built-in
- Temporary file management in tests is inelegant but explicit; mention that
tempfilecrate would be cleaner in production - Flag the
ccdependency clearly — integration tests are not hermetic