4.1 KiB
+++ title = "§1 Introduction: What We're Building" priority = 5 status = "done" ticket_type = "task" dependencies = [] +++
§1 Introduction: What We're Building — Stub to fill
File: edu/src/lisp-compiler.md, section ### 1. Introduction: What We're Building
Replace the stub line with full content. Target 600–900 words. Match the style of Section 1 in markov.md: motivating prose paragraphs that build genuine enthusiasm before introducing any technical detail.
Learning objectives
- Understand what a compiler is and how it differs from an interpreter
- Know what MiniLisp looks like and what the compiler will produce
- Understand why Rust + nom is a good toolchain for this task
- Know what prerequisite Rust knowledge is assumed
- Have a concrete mental picture of the end goal before writing any code
Content to write
What is a compiler? A compiler is a program that reads source code in one language and produces equivalent code in another. Unlike an interpreter (which executes source code directly), a compiler's output is a new program that can be run independently. Our compiler reads MiniLisp and writes C. That C can then be compiled by any standard C compiler (cc, gcc, clang) into a native binary.
Why Lisp? Lisp is the ideal first compiler target. Its syntax is maximally regular — every expression is either an atom or a parenthesised list. There is no operator precedence to track, no statement/expression ambiguity, and no complex grammar rules. The AST almost directly mirrors the syntax. This regularity lets the course focus on the concepts of compilation rather than the incidental complexity of a messier language.
Why compile to C? C is essentially portable assembly. It is available everywhere, compiles quickly, and produces fast native code. By targeting C rather than actual assembly, we get a working native compiler without managing registers, calling conventions, or instruction sets. C handles all of that.
A teaser: what the compiler produces. Show a realistic MiniLisp program (recursive factorial + display call) alongside the C output the compiler will emit. This makes the goal concrete from the first page.
MiniLisp source:
; Compute n!
(define (factorial n)
(if (= n 0)
1
(* n (factorial (- n 1)))))
(display (factorial 10))
(newline)
Compiler output:
#include <stdio.h>
#include <stdint.h>
#define TRUE 1
#define FALSE 0
typedef int64_t ml_int;
/* forward declarations */
ml_int ml_factorial(ml_int ml_n);
ml_int ml_factorial(ml_int ml_n) {
return (ml_n == 0) ? 1 : (ml_n * ml_factorial((ml_n - 1)));
}
int main(void) {
printf("%ld\n", ml_factorial(10));
return 0;
}
Why Rust and nom? Rust's type system makes compiler writing unusually safe: exhaustive pattern matching means you cannot forget a case, Result<T, E> enforces error handling at every stage, and the borrow checker prevents accidental aliasing of AST nodes. nom is a parser-combinator library — parsers are ordinary Rust functions that compose, test, and debug naturally. No grammar files, no code generation, no build scripts.
What this course assumes. The reader should be comfortable with: ownership and borrowing, enums and pattern matching, Result and Option, basic trait usage (impl Trait for Type), and #[test]. No prior compiler or parsing experience is required.
How to follow along. Reading sections (Parts 1, 3) can be read anywhere. Implementation sections build on each other in order — each section adds code to the same project. A reference solution appears in a collapsible block at the end of each exercise so you can verify your work without being spoiled.
Style notes
- Open with a hook: writing a compiler is a rite of passage; it demystifies the tools programmers use every day
- Put the teaser code block early — visual motivation before prose motivation
- Keep the tone encouraging: this is achievable for any Rust programmer, no specialist knowledge required
- End by telling the reader exactly what they will have at the end: a binary that compiles MiniLisp programs to runnable C