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.
vibed/edu/.beans/edu-v0ud--17-testing-the-co...

179 lines
5.2 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

---
# edu-v0ud
title: §17 Testing the Compiler
status: completed
type: task
priority: normal
created_at: 2026-03-10T23:30:01Z
updated_at: 2026-03-10T23:30:01Z
---
## §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 700900 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 §815 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