+++ 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 `cc` and 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: 1. **Unit tests** per module: already written in §8–15 for individual parsers and code-gen functions. This section consolidates and supplements them. 2. **`compile()` round-trip tests**: call `compile(source)` and assert on the C output string (does it contain expected fragments?). 3. **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:** ```rust #[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:** ```rust #[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: 1. Calls `compile(source)` to get C source 2. Writes C source to a temp file (`/tmp/ml_test_{N}.c`) 3. Invokes `cc -o /tmp/ml_test_{N} /tmp/ml_test_{N}.c` 4. Runs the binary and captures stdout 5. Asserts stdout matches expected output 6. Cleans up temp files ```rust #[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: ```rust #[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 ```rust #[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 ```sh 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_minilisp` helper 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 `tempfile` crate would be cleaner in production - Flag the `cc` dependency clearly — integration tests are not hermetic