CcCompilers · Lesson 1 of 7

The Compiler Pipeline

Every compiler — GCC, V8, rustc, even the TypeScript checker — is the same assembly line: characters to tokens to tree to checked tree to optimized form to output code. Learn the stations once, recognize them everywhere.

Text
source text
   |
   v  LEXER        characters -> tokens ("words")
   v  PARSER       tokens -> syntax tree (AST)
   v  SEMANTIC     names resolved, types checked
   v  IR           tree -> intermediate representation
   v  OPTIMIZER    IR -> better IR (most of the magic)
   v  CODEGEN      IR -> assembly / bytecode / JS / ...
   |
   v  output program

Front end = understand the source (lexer..semantic).
Back end  = produce the target (optimizer..codegen).

The front/back split is the field's great economy: LLVM is a shared back end, so Rust, Swift, Clang, and Julia each wrote only a front end and got world-class optimization for every CPU free. Same trick in reverse: one front end can target many back ends — that's how the same C code compiles for x86, ARM, and WebAssembly.

This pipeline isn't just for 'real' compilers. A linter is a front end that reports patterns instead of generating code. A formatter parses and prints the tree back prettily. TypeScript's compiler type-checks and then emits JavaScript — a compiler whose target language is another high-level language (a 'transpiler'). Syntax highlighting in your editor is a lexer running on every keystroke.

◆ Note
Interpreters share the front half: Python lexes, parses, and compiles your file to bytecode, then executes the bytecode in a loop instead of translating it to machine code. JIT engines like V8 do both — interpret first, compile hot paths natively while running. 'Compiled vs interpreted' is a spectrum, not a binary.