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

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

Back End

Runtimes

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