|
|
+++
|
|
|
title = "§16 The Compilation Pipeline"
|
|
|
priority = 5
|
|
|
status = "todo"
|
|
|
ticket_type = "task"
|
|
|
dependencies = []
|
|
|
+++
|
|
|
|
|
|
## §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<String, error::CompileError> {
|
|
|
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<Expr>) -> 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<String> = 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
|