Compilers & Language Implementation
Compilers have a reputation for being the deep end of computer science, and the reputation is half-earned. The theory goes as far as you want it to; the practice is a pipeline of comprehensible transformations, each turning one representation into a slightly lower-level one. Text becomes tokens, tokens become a tree, the tree gets type-checked and lowered into an intermediate representation, and the IR becomes machine code or bytecode. Every stage is a program you could write.
The reason this matters outside of compiler work is that the same pipeline appears constantly in ordinary engineering. Query planners, template engines, config parsers, linters, formatters, ORMs, regular expression engines, GraphQL executors, and every DSL anyone has ever built are compilers with different output. Knowing the shape means recognizing when you're writing one — and, more usefully, when you're writing one badly with string manipulation.
The other payoff is understanding your own tools. Why the JIT deoptimizes a hot loop, why the garbage collector paused for 200ms, why a type error points at the wrong line — these are all explicable once you know what's happening underneath.
TL;DR
- The pipeline: source → tokens → AST → typed AST → IR → optimization → target code.
- Lexing splits text into tokens; parsing builds a tree from a grammar.
- Recursive descent is the parser you should write by hand; parser generators earn their keep on complex grammars.
- An AST models the program's structure; an IR is what you actually optimize.
- Type checking is a separate pass over the AST — inference, unification, and error reporting.
- LLVM gives you optimization and every target backend for the price of emitting its IR.
- Interpreters trade speed for simplicity and portability; JIT compiles hot paths at runtime.
- Garbage collection is a language-runtime decision with enormous consequences for latency.
The Pipeline
The front end (lexing, parsing, type checking) is language-specific. The back end (optimization, code generation) is target-specific. Splitting at the IR is what lets LLVM serve dozens of languages and dozens of architectures without an N×M explosion.
Featured Topics
Front End
- Lexers & Parsers — Tokenization, grammars, recursive descent, and Pratt parsing
- ASTs & Intermediate Representations — Tree design, visitors, lowering, and SSA form
- Type Systems & Type Checking — Inference, unification, soundness, and useful error messages
Back End
- Code Generation — Instruction selection, register allocation, and calling conventions
- LLVM — Emitting LLVM IR and inheriting an industrial optimizer and every backend
Runtimes
- Interpreters & Bytecode VMs — Tree-walking vs. bytecode, stack vs. register VMs, dispatch
- JIT Compilation — Tiered compilation, inline caches, speculation, and deoptimization
- Garbage Collection — Tracing vs. reference counting, generational collection, and pause times
Common Questions
Do I need to know compilers to be a good engineer?
Not required, and unusually high-leverage. The pipeline shows up everywhere — SQL planners, template engines, linters, formatters, build tools, GraphQL, ORMs, and every configuration language. Recognizing "this is a parsing problem" is what stops people from writing the regex that almost handles nested structures. And understanding JIT and GC behavior is what turns unexplainable performance into diagnosable performance.
What's the best way to learn?
Write one. Build a small interpreter for a toy language — arithmetic, variables, functions, closures — then extend it to a bytecode VM. Crafting Interpreters walks through exactly this twice, once in Java and once in C, and it's the most effective on-ramp in the field. A weekend gets you to expression evaluation; a few weeks gets you a real language. Reading compiler theory before building something is the slow path.
Should I write a parser by hand or use a generator?
By hand, for most purposes. Recursive descent with Pratt parsing for expressions is straightforward, gives excellent error messages, handles the context-sensitivity real languages have, and is easy to debug — which is why most production compilers, including Clang, GCC, and TypeScript, hand-write theirs. Generators (ANTLR, yacc, tree-sitter) earn their place for complex or rapidly changing grammars and for tooling that needs incremental reparsing.
What does LLVM actually give me?
Everything after the IR. You emit LLVM IR from your front end and get a mature optimizer, register allocation, and code generation for x86, ARM, RISC-V, WebAssembly, and more. Rust, Swift, Julia, Zig, and Clang all work this way. The costs are a large dependency, slow compile times relative to a simple backend, and a learning curve on the IR itself — which is why some projects (Go, and Zig's own backend) write their own for build-speed reasons. See LLVM.
Compiled or interpreted — is that even a real distinction?
Not really, and it's more useful to think about when compilation happens. Python compiles to bytecode then interpretes it. Java compiles to bytecode then JITs the hot parts to native code. JavaScript engines interpret first and progressively compile. C compiles fully ahead of time. The spectrum runs from tree-walking (slowest, simplest) through bytecode to JIT to AOT, and most modern languages sit somewhere in the middle with several tiers.
Where does WebAssembly fit?
As a compilation target — a portable, low-level bytecode with a defined stack machine and a safe execution model. From a language implementer's perspective it's another backend, and an unusually pleasant one: well-specified, sandboxed, and supported by LLVM. See WebAssembly.
Learning Path
Beginner
Write a tree-walking interpreter for arithmetic and variables. Add functions and closures. Understand tokens, grammars, and ASTs.
Intermediate
Move to a bytecode VM (interpreters). Add a type checker. Implement a simple garbage collector.
Advanced
Lower to an SSA-based IR, write optimization passes, emit LLVM IR or native code, and study JIT tiering and deoptimization.
Related Hubs
- Programming Languages — The languages being implemented, and systems programming
- Algorithms & Data Structures — Graphs, trees, and the analyses compilers run on them
- Performance & Optimization — Why understanding the runtime matters
- Developer Tools — Build tools and linters are compilers too