Compiler Design
The Three-End Pipeline
Almost every compiler decomposes into the same three stages, each with its own inputs, outputs, and error classes.
Figure 1 — Each stage narrows the problem; each reports its own kind of error.
Front-End
Source to validated tree: lexer, parser, and semantic analysis (the Phase-1 topics). All errors here are about the program’s text and meaning.
Middle-End
Tree to intermediate representation, transformed by optimization. This is where compilers earn their keep for large languages; DSLs often skip or shrink it.
Back-End
IR to the target: bytecode for a VM, native code, or a call into a heavy tool like LLVM (Phase 3). DSL back-ends are usually tiny because the IR is tiny.
Design Decisions
Compiler vs. Interpreter
Compile-then-run separates translation from execution (startup cost, later find bugs); interpreting executes the tree directly (faster to build, slower at runtime). The pragmatic DSL choice: compile to bytecode, interpret bytecode, and keep the deck open for later optimization.
Single-Pass vs. Multi-Pass
Single-pass compilers emit as they parse (simple, restrictive); multi-pass compilers build a tree, analyze, then emit (flexible, debuggable). Multi-pass wins for DSLs: readability of each pass beats a few saved clock cycles.
Host and Target
Pick the host language for the compiler (fast to write, easy to embed) and the target (interpreter, bytecode, native, another DSL). Writing your DSL compiler in the DSL itself is elegant later — for a first version, choose the boring, well-tested host.
Correctness Strategy
A compiler is worth exactly as much as its tests: it is a deterministic function from source to code, so almost everything is testable.
Test Ladder
Unit-test each pass on tiny programs; keep golden files (input plus expected output) for every example in the docs; and run a corpus of bad programs to verify each diagnostic still fires.
Property Checks
For expressions, compare the result of running compiled and interpreted versions of the same input (differential testing); for parsers/printers, round-trip source through parse–print and assert identity.
Errors Are a Pipeline Feature
Which Stage Owns Which Error
Lexical and syntax errors belong to the front-end; undefined names and type mismatches to semantic analysis; impossible instructions to the back-end. Every stage must fail loudly, with the message style from Errors & Diagnostics, so the user always knows which contract was broken.
Never Swallow a Stage
A DSL that silently accepts a program it cannot compile teaches users that the tool lies. Prefer an explicit “not yet implemented” diagnostic over a silent wrong answer.
Example: The Driver Loop
The architecture fits in ten lines: each stage is a function from one artifact to the next, and the pipeline is the program.
main_compile() (commented)
# driver.py — the whole pipeline as a function chain
def main_compile(source: str):
tokens = lex(source) # front-end 1: characters -> tokens
tree = parse(tokens) # front-end 2: tokens -> AST
check(tree) # front-end 3: names, types (Semantic Analysis)
ir = lower(tree) # middle-end: AST -> three-address IR
bc = emit(ir) # back-end: IR -> bytecode
dump(bc) # a golden file for the test ladder
vm_run(bc) # bytecode into the Virtual Machine
How to Read It
Every later Phase-2 page fills in exactly one function: Semantic Analysis the check, Intermediate Representations the lower, Bytecode & Codegen the emit, Virtual Machines the vm_run.
Next Steps
Continue Phase 2
Next the tree becomes checked and decorated: Semantic Analysis, then Intermediate Representations.