--- # edu-nc61 title: §16 The Compilation Pipeline status: completed type: task priority: normal created_at: 2026-03-10T23:30:01Z updated_at: 2026-03-10T23:30:01Z --- ## §16 The Compilation Pipeline — Stub to fill File: `edu/src/lisp-compiler.md`, section `### 16. The Compilation Pipeline` Replace the stub line with full content. Target 600–800 words. Wire all stages into a working CLI binary. Trace the complete factorial example end-to-end. ## Learning objectives - Implement the `compile` function that chains parse → analyse → generate - Write a CLI `main.rs` that reads from a file or stdin and writes to stdout - Handle and display errors from any stage - Demonstrate the complete workflow: `.lisp` → `.c` → compile → run ## Content to write ### The `compile` function In `src/main.rs` (or a `src/lib.rs`): ```rust pub fn compile(source: &str) -> Result { let exprs = parser::parse(source)?; let exprs = analyser::analyse(exprs)?; let c = codegen::generate(exprs); Ok(c) } ``` ### Handling top-level non-define expressions The code generator in §14 skipped top-level expressions that are not `define` forms (e.g., `(display (factorial 10))`). These must be emitted inside a C `main` function. Complete the `generate` function: ```rust pub fn generate(exprs: Vec) -> String { let mut out = String::new(); out.push_str(PREAMBLE); // Forward declarations for expr in &exprs { if let Expr::Define { name, value } = expr { out.push_str(&gen_forward_decl(name, value)); } } out.push('\n'); // Function and variable definitions for expr in &exprs { if let Expr::Define { name, value } = expr { match value.as_ref() { Expr::Lambda { params, body } => out.push_str(&gen_function_def(name, params, body)), _ => out.push_str(&gen_variable_def(name, value)), } } } // main(): emit top-level non-define expressions out.push_str("\nint main(void) {\n"); for expr in &exprs { if !matches!(expr, Expr::Define { .. }) { out.push_str(&format!(" {};\n", gen_stmt(expr))); } } out.push_str(" return 0;\n}\n"); out } ``` ### The CLI: `src/main.rs` ```rust use std::{env, fs, io::{self, Read}, process}; fn main() { let args: Vec = env::args().collect(); let source = match args.get(1) { Some(path) => fs::read_to_string(path).unwrap_or_else(|e| { eprintln!("error reading {}: {}", path, e); process::exit(1); }), None => { let mut buf = String::new(); io::stdin().read_to_string(&mut buf).unwrap(); buf } }; match compile(&source) { Ok(c_source) => print!("{}", c_source), Err(e) => { eprintln!("{}", e); process::exit(1); } } } ``` Explain: if a file path is given as `argv[1]`, read from it; otherwise read from stdin. Always write C to stdout. This allows `minilisp factorial.lisp > factorial.c`. ### The end-to-end workflow Walk through the complete factorial example step by step: ```sh # 1. Write a MiniLisp program cat > factorial.lisp <<'EOF' (define (factorial n) (if (= n 0) 1 (* n (factorial (- n 1))))) (display (factorial 10)) (newline) EOF # 2. Compile to C cargo run -- factorial.lisp > factorial.c # 3. Compile the C cc -o factorial factorial.c # 4. Run ./factorial # Output: 3628800 ``` Show the generated `factorial.c` in full so the reader can verify the output looks correct. ### Error handling demo Show what happens when the compiler rejects invalid input: ```sh echo "(define (f x) (g x))" | cargo run # g is undefined # stderr: semantic error: undefined symbol: `g` # exit code: 1 ``` ### Build and validate ```sh cargo fmt && cargo check && cargo clippy && cargo test ``` All should pass. This is the project's first fully working state. ## Style notes - The end-to-end workflow walkthrough is the climax of the implementation sections — give it space - Show the generated C in full; readers deserve to see the fruit of their work - The error demo is quick but important — confirm the pipeline fails gracefully - End with a note of congratulation: the reader has just built a compiler