The six phases of a compiler, end to end
A bird's-eye map of every stage a compiler passes your code through, from raw characters to final machine instructions, using one tiny expression as the running example.
An assembly line, not one giant machine
A car factory doesn't build a car in one single step. Raw metal goes through a series of stations — cutting, welding, painting, assembling — each one handing its output to the next. A compiler works exactly the same way: it's an assembly line of phases, each one transforming your program into a new representation before handing it to the next phase.
This lesson is the map for the rest of the course. Every single module from here on is really just a deep dive into one station on this assembly line. We'll follow one tiny piece of source code,x = a + b * 2;, through every single phase, so you can see the shape it takes at each stop.
The six phases, in order
source code
│
▼
[1] Lexical Analysis → stream of tokens
│
▼
[2] Syntax Analysis → parse tree / AST
│ (parsing)
▼
[3] Semantic Analysis → annotated AST + symbol table
│
▼
[4] Intermediate Code Generation → three-address code (TAC)
│
▼
[5] Code Optimization → smaller/faster intermediate code
│
▼
[6] Target Code Generation → machine code / assembly
Two supporting components run alongside every phase rather than being a single step themselves: the symbol table (a running record of every name your program declares — variables, functions — and facts about them) and the error handler (which catches and reports problems at whichever phase they're detected, instead of crashing silently).
Phase 1: Lexical Analysis — chopping text into words
The compiler's first job is deceptively simple: turn a raw stream of characters into a stream of meaningful chunks called tokens — the "words" of your programming language. This phase (covered in full in Module 2) is done by a component called the lexer or scanner.
Input (raw characters):x = a + b * 2;
Output (tokens):
IDENTIFIER(x) ASSIGN(=) IDENTIFIER(a) PLUS(+) IDENTIFIER(b) STAR(*) NUMBER(2) SEMICOLON(;)
IDENTIFIER, NUMBER, PLUS) plus, when relevant, a value (the identifier's actual name, the number's actual value). The lexer has no idea yet whether this sequence of tokens forms a valid statement — it just chops and labels.
Phase 2: Syntax Analysis — checking the grammar and building a tree
The parser (Module 3) takes that flat stream of tokens and checks whether it matches the language's grammar rules, and if it does, builds a tree-shaped structure representing the statement's actual structure — an Abstract Syntax Tree (AST).
=
/ \
x +
/ \
a *
/ \
b 2
b * 2) is nested below addition, meaning it must be computed first — exactly matching the math rule you learned in school (multiplication before addition). This is the parser doing real work: a + b * 2 and (a + b) * 2 produce genuinely different tree shapes, even though they use nearly the same tokens.
Phase 3: Semantic Analysis — does this actually make sense?
Syntax alone doesn't catch everything wrong with a program.x = a + b * 2; is grammatically perfect even if b was never declared, or if a is text (a string) and adding a number to it makes no sense. Semantic analysis (Module 4) walks the AST and checks meaning-level rules:
- Was every variable declared before use? (checked against the symbol table)
- Do the types on either side of an operator make sense together?
- Is this name being used consistently with what it was declared as?
This phase typically annotates the tree with extra information, like each node's resolved data type:
= : int
/ \
x:int + : int
/ \
a:int * : int
/ \
b:int 2:int
Phase 4: Intermediate Code Generation — a universal middle language
Rather than jumping straight from the AST to a specific processor's machine code, most real compilers first generate a simpler, generic instruction format called intermediate code (Module 6). A very common form is three-address code (TAC), where every instruction does at most one operation:
t1 = b * 2
t2 = a + t1
x = t2
t1 and t2 are compiler-invented temporary variables, holding intermediate results. Why bother with this extra step instead of generating final machine code directly? Because intermediate code is independent of any specific processor, which means the next phase — optimization — can be written once and reused no matter what machine you're ultimately compiling for.
Phase 5: Code Optimization — the same answer, done cheaper
Optimization (Module 7) rewrites intermediate code to be faster or smaller, without changing what the program computes. For our example, a smart compiler notices thatb * 2 can be computed with a cheap bit-shift instead of full multiplication, or that if b is a compile-time-known constant, the whole expression can be pre-computed. A trivial local optimization here:
t1 = b * 2 → (kept as-is if b is unknown; simplified to a shift if the target favors it)
t2 = a + t1
x = t2
The full picture — constant propagation, common subexpression elimination, liveness analysis — gets an entire module later in this course, because this is where a huge fraction of real-world compiler engineering effort actually goes.
Phase 6: Target Code Generation — the final translation
Finally, the (optimized) intermediate code gets translated into actual instructions for a specific target — either real machine code for a specific processor, or human-readable assembly language as a stand-in for it:
MOV R1, b
IMUL R1, 2
MOV R2, a
ADD R2, R1
MOV x, R2
This phase also has to solve a genuinely hard resource problem: real processors only have a handful of physical registers (fast, tiny storage slots), while a program can have arbitrarily many variables and temporaries. Deciding which values live in registers versus slower memory at any given moment is called register allocation, and it's one of the most mathematically interesting problems in this entire field (it can be modeled as graph coloring, if you've seen that idea before).
A little math: why phases matter for correctness, not just organization
You might wonder if a compiler could just skip straight from tokens to machine code, cutting out the middle phases. Here's the mathematical reason it can't, in one line: each phase is defined as a function that only knows how to transform its own well-defined input structure.
Composing them, the whole compiler is just function composition:
Each function in that chain is dramatically simpler to write, test, and prove correct in isolation than one giant function trying to go straight from characters to machine code. This is the same reason the car factory has separate cutting and painting stations instead of one machine that tries to do everything simultaneously — smaller, well-defined jobs are verifiably correct jobs.
Common mistakes
- Assuming every compiler must have exactly these six phases, always separate. Simple ("toy") compilers often merge phases — for instance, generating code directly while parsing, skipping a separate AST. The six-phase breakdown is the clean conceptual model; real compilers often optimize this pipeline together for speed.
- Thinking optimization is optional polish. For any compiler used in production (GCC, Clang, the JVM's JIT), optimization phases are often the single largest chunk of the compiler's actual codebase, not an afterthought.
- Believing the AST and the parse tree are the same thing. A parse tree records literally every grammar rule applied; an AST is a cleaned-up, simplified version that drops redundant structure. Module 3 draws this distinction precisely.
Summary
A compiler is a pipeline of six phases — lexical analysis, syntax analysis, semantic analysis, intermediate code generation, optimization, and target code generation — each one a well-defined transformation from one structured representation to the next, glued together by a shared symbol table and error handler. Followingx = a + b * 2; through all six phases shows exactly why this layered design exists: each phase can be understood, tested, and even swapped out (a new target architecture only needs a new final phase) without touching the others.