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/archive/edu-nc61--16-the-compilatio...

4.3 KiB

title status type priority created_at updated_at
§16 The Compilation Pipeline completed task normal 2026-03-10T23:30:01Z 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 600800 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):

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:

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

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:

# 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:

echo "(define (f x) (g x))" | cargo run  # g is undefined
# stderr: semantic error: undefined symbol: `g`
# exit code: 1

Build and validate

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