Compilers & Programming Languages
How does source code become executable behavior, and how do language and compiler design choices affect correctness, performance, safety, tooling and developer experience?
What actually happens to my code between the editor and the CPU — and which of it was a choice?
A compiler is not a translation step. It is a sequence of representations, each one built because the previous form could not answer the next question — and every transformation between them is legal only under a precondition someone had to establish. This domain is about those representations, those preconditions, and the design decisions that put them there.
Read it as a list of questions rather than a list of steps. Tokens exist because characters cannot say which of them belong together. An AST exists because a token list cannot say what applies to what. IR exists because a tree cannot say in what order things happen. Every arrow is a question the previous box could not answer — and something the new box has thrown away.
Three sentences the whole domain hangs on
If a lesson here ever seems to contradict one of these, the lesson is wrong.
A compiler is a sequence of representations, not a translation step. Every phase exists because the previous representation could not answer the next question.
An optimization is legal only if it preserves the language's defined observable behavior — so what a compiler may do is decided by language semantics, not by cleverness.
"Compiled" and "interpreted" are not properties of languages. They are implementation strategies, and real systems combine them.
This domain is not Runtime Internals, and not Computer Architecture
Both neighbours are close enough to be worth stating the line.
Object representation, garbage collection, virtual-machine execution, runtime dispatch. What happens while the program runs.
Everything up to the handover, plus the compiler-side half of every runtime mechanism — the metadata emitted, the guard inserted, the state map that makes deoptimization possible.
The ISA and the microarchitecture. We own the compiler's model of that machine, and the places where the model is deliberately wrong.
Flagship experiences
The parts of this domain that are not reading.
One program, twelve representations — tokens, AST, typed AST, IR, CFG, SSA, optimized IR, bytecode, registers, assembly, execution. Every panel is produced by the compiler in this repository, not by a fixture.
Toggle individual optimizations and watch the cascade — including the two that refuse to fire, because firing would change what the program means.
Take registers away until the allocator has to spill. Graph colouring and linear scan, side by side, on the same program.
Step the bytecode one instruction at a time, with the operand stack, the locals and the call stack visible.
The whole language in one page. Type anything and watch it go from characters to machine instructions.
C++, JavaScript, TypeScript and Python take four genuinely different routes from source to behavior. This refuses to pretend otherwise.
Which implementation strategy fits this language? Which optimization applies here? Should I build a DSL? Every leaf states its legality condition and its price.
Start at print(1 + 2). Finish with a design you can defend — then have the requirements break it.
A compiler misbehaves and you name the stage before the fix; or you apply the transformation by hand and find the trap.
Learning modules
36 modules, grouped by where they sit on the pipeline.
Before the compiler
What the language is for, and what that decides.
Source code to behavior as a sequence of representations, each existing because the previous one could not answer the next question — and what is lost at every handover.
Who the language is for decides the type system, the memory model, the concurrency model and the execution strategy. Every other answer in the domain follows from this one.
When a new language is cheaper than a library, when it is much more expensive, and what the tooling bill actually looks like once people depend on it.
Frontend
Source to a checked, structured program.
Writing down what a valid program looks like: productions, derivations, EBNF, and the ambiguity that precedence and associativity exist to resolve.
Characters to tokens, why regular languages are enough for this job, and the hazards — maximal munch, keywords that are also identifiers, numbers that run into letters.
Tokens to structure. Recursive descent and Pratt parsing by hand, LL and LR as families, and what a parser generator buys and costs.
A compiler that stops at the first error is a bad tool. Spans, recovery, synchronization, and diagnostics that name what was expected instead of saying "syntax error".
The representation every later phase is written against: node design, traversal, the visitor as the standard shape of a pass, and why the AST outlives the parser.
Names, scopes and everything the grammar could not express. Symbol tables, shadowing, resolution, and the annotated tree the type checker needs.
What the language can prove before it runs. Checking, typing rules, environments, inference, unification, polymorphism, subtyping and variance.
Composing types and representing absence: unions, intersections, algebraic data types, exhaustive pattern matching, nullability and gradual typing.
What survives to runtime and what proves memory safety: erasure versus reification, monomorphization, ownership, lifetimes and effects.
Middle-end
A representation you can analyse, and the transformations that are legal on it.
The representation the middle-end is written against. Why an IR exists at all, how many levels of it there are, and what lowering means at each step.
Turning statements into a graph you can reason about: basic blocks, edges, natural loops, dominators and the dominance frontier that SSA construction needs.
One definition per name makes data dependencies explicit. Phi functions, construction, why so many analyses get simpler, and how you leave SSA again.
One framework — facts, transfer functions, a meet operator, iterate to a fixed point — and the four classic analyses that are all instances of it.
Folding, elimination, propagation, inlining and devirtualization — each with the precondition that makes it legal and the budget that makes it wise.
Where the time actually goes: hoisting, unrolling, interchange, vectorization — and the aliasing and escape questions that decide whether any of it is allowed.
Observable behavior, the as-if rule, and undefined behavior as a licence to assume rather than a promise to crash. Semantics decide what is legal, not cleverness.
Closures, coroutines, async, exceptions and match expressions are all ordinary control flow after the compiler is done with them. This module does the transformation.
Backend
One specific machine, and where every value physically lives.
IR to instructions for one specific machine: selection by pattern matching, scheduling for a pipeline the compiler cannot observe, and the bytes that come out.
Many live values, few registers. Live ranges, interference, graph colouring, linear scan, and the spill that turns a register access into a memory access.
The contract between separately compiled code: argument passing, stack frames, saved registers, mangled names, and what breaking it costs.
Linking & trust
Composing binaries, and what the toolchain is trusted with.
Composing object files into something runnable, resolving what the compiler could not know, and handing the result to an operating system loader.
Where the first compiler came from, how a compiler comes to compile itself, and why source code alone does not capture every trust assumption in a toolchain.
Execution
Interpreters, virtual machines, and compiling while the program runs.
An instruction set you get to design. Stack versus register machines, tree-walking versus bytecode, and the dispatch loop at the centre of both.
Compiling with information a static compiler cannot have. Tiers, profiling, speculation, guards, and the deoptimization that catches a wrong guess.
Around all of it
Real pipelines, correctness, tooling, builds — and building your own.
Python, JavaScript, TypeScript, C++, Rust and Go: four genuinely different routes from source to behavior, compared without pretending they are the same.
LLVM as reusable middle-end and code generator rather than "a compiler", GCC as the other one, and WebAssembly as a portable sandboxed target.
The one program whose bugs are everyone else’s bugs: miscompilation, differential testing, fuzzing, translation validation and formal verification.
The frontend is the IDE. Abstract interpretation, interprocedural analysis, linters, formatters, concrete syntax trees and the language server that serves them all.
Mapping optimized machine code back to what you wrote — line tables, variable locations, source maps, and why a variable reads as "optimized out".
Compilation units, modules, interface files and the dependency analysis that keeps a rebuild proportional to the change rather than to the codebase.
Seeing across module boundaries with LTO, and measuring before optimizing with PGO — including what an unrepresentative profile does to the result.
A model-generated plan is a program in an untrusted language. Parse it, type it, validate it and check its permissions before any of it executes.
Build the whole thing, one stage at a time, from `print(1 + 2)` to a typed language with a bytecode VM, an SSA optimizer and a language server.
Reference
For when you already know roughly what you are looking for.
Ten levels, following the pipeline. Each ends with something you can do.
The searchable index from a line of source to what it triggers.
The same index with the column that makes it honest: when it does NOT do that.
Ten pairs people genuinely confuse, with the confusion named.
What each question tests, and the wrong answers that sound right.
Every plausible wrong reading the domain names, next to the lesson that refutes it.