Compiler Phases: Working of Each Phase in Detail (With an End-to-End Example)
A typical compiler converts a source program into machine-executable code through a pipeline of phases: lexical analysis , syntax analysis , semantic analysis , intermediate code generation , optimization , and code generation.
A helpful way to understand the pipeline is to walk the same program through each phase, observing what data structures are built (tokens, AST, symbol tables, IR, etc.) and what transformations occur. The example below uses a small expression language fragment (like many textbooks) so the changes are visible without getting lost in full language features like control-flow or functions.
Example input (source language):
1int a = 3; 2int b = 4; 3int c = a * (b + 5) - 2;
Compiler Phases (Lexical, Syntax, Semantic, IR, Codegen) — overview
Compiler pipeline (conceptual sequence)
Lexical Analysis
Phase 1Source characters → tokens (e.g., keywords, identifiers, numbers, operators)."
Syntax Analysis
Phase 2Tokens → parse tree/AST using grammar rules."
Semantic Analysis
Phase 3Type-checking, scope, symbol table population."
IR Generation
Phase 4AST → intermediate representation (often three-address code)."
Optimization
Phase 5Fold constants, remove common subexpressions, reduce redundant ops."
Code Generation
Phase 6IR → target assembly/machine instructions with register/stack handling."
Below, each phase is described in detail, followed by what happens to the running example. When possible, I’ll show representative outputs (tokens, AST shapes, symbol table entries, IR instructions, and final code sketch).
Phase 1 — Lexical Analysis (Scanner)
Goal: Convert a character stream into a token stream.
Token , lexeme , regular expression , DFA.
How it works
- The lexer reads input left-to-right.
- It classifies substrings into tokens using patterns (often implemented with DFA tables derived from regular expressions).
- It typically:
- Skips whitespace and comments.
- Recognizes multi-character operators (
==,<=,*, etc.). - Distinguishes keywords from identifiers (e.g.,
intis a keyword,ais an identifier). - Produces numeric literals (e.g.,
3,4,5,2).
Representative tokens for the example (simplified):
1INT a ASSIGN NUM(3) SEMI 2INT b ASSIGN NUM(4) SEMI 3INT c ASSIGN 4IDENT(a) MUL LPAREN 5IDENT(b) PLUS NUM(5) RPAREN 6MINUS NUM(2) SEMI
Typical outputs/data
- A sequence of tokens:
- Optional token attributes: identifier names, numeric values, source locations for error reporting.
Common failure modes
- Illegal characters (e.g.,
$if not allowed). - Malformed numbers (e.g.,
12..3). - Unterminated comments or strings (if supported).
[CalloutBlock]
Tip: Lexing should preserve position info
Keep line/column offsets in tokens so syntax/semantic errors can point to the exact region in the source.
Phase 2 — Syntax Analysis (Parser)
Goal: Check the grammar structure and build a hierarchical representation (parse tree / AST).
Context-free grammar (CFG) , parse tree , AST , shift-reduce.
How it works
- The parser consumes the token stream.
- Using a grammar (CFG), it decides whether the token sequence forms valid constructs.
- It builds a structured representation:
- Parse tree (faithful to grammar)
- or AST (more semantic, fewer punctuation nodes)
For many expression languages, the parser organizes the expression by precedence and associativity:
*binds tighter than+/-- parentheses override precedence.
Representative AST idea for the expression
The right-hand side of c is:
1a * (b + 5) - 2
AST shape (conceptual):
- Root:
-- Left:
*- Left:
a - Right:
+- Left:
b - Right:
5
- Left:
- Left:
- Right:
2
- Left:
Typical outputs/data
- An AST for the whole program (or one per function/block)
- Error messages when grammar constraints are violated (missing semicolons, mismatched parentheses, etc.)
Warning: Parser errors usually stem from earlier lexer decisions
If the lexer misclassifies tokens (e.g., treating int as an identifier), the parser can fail in misleading ways.
Phase 3 — Semantic Analysis
Goal: Ensure the program is meaningful beyond syntax.
Symbol table , scope , type checking , lvalue.
How it works
- Build/populate the symbol table.
- Resolve identifiers (
a,b,c) to declarations. - Perform checks:
- Variables declared before use.
- Type compatibility for operations (
+,*,-). - Assignment correctness (RHS type matches LHS type, or implicit conversions allowed).
- Optionally annotate AST nodes with types (e.g.,
int).
Symbol table entries (conceptual)
After int a = 3;
a : int(assigned an address or intermediate storage location)
After int b = 4;
b : int
After int c = ...;
c : int
Then for the expression:
a,bareintb + 5producesinta * (b + 5)producesint... - 2producesint- Therefore assignment to
cis type-correct.
What semantic analysis “adds”
- Attributes like:
node.type = intnode.symbolRef = <symbol table entry>- resolved scopes/links
Phase 4 — Intermediate Code Generation (IR Generation)
Goal: Translate the AST into an intermediate representation that’s easier to optimize and analyze.
Intermediate representation (IR) , three-address code , temporary variable (temp) , basic block.
How it works
- Walk the AST in an evaluation order.
- Emit IR instructions for each operation, using temporaries for subexpressions.
Three-address IR sketch
Let temps be t1, t2, t3, t4.
Expression: a * (b + 5) - 2
- Compute
(b + 5):t1 = b + 5
- Compute
a * t1:t2 = a * t1
- Compute
t2 - 2:t3 = t2 - 2
- Assign to
c:c = t3
Also, the declarations/assignments:
a = 3b = 4c = t3
A combined IR listing (simplified):
1a = 3 2b = 4 3t1 = b + 5 4t2 = a * t1 5t3 = t2 - 2 6c = t3
Intermediate products
- A list of IR instructions
- Control-flow graph (CFG) if the language has branches/loops
- For straight-line code, it’s essentially one basic block.
Phase 5 — Optimization
Goal: Improve performance (time/space) or reduce code size while preserving semantics.
constant folding , common subexpression elimination , dead code elimination , strength reduction.
What can be optimized in the example
Because b is assigned 4, t1 = b + 5 can be partially constant-folded:
t1 = 4 + 5 = 9Then:t2 = a * t1, witha = 3→t2 = 3 * 9 = 27Then:t3 = t2 - 2 = 27 - 2 = 25Finally:c = 25
So the optimized IR may collapse to:
1a = 3 2b = 4 3c = 25
Mermaid view of dataflow optimization
Phase 6 — Code Generation
Goal: Convert optimized IR into target-specific assembly/machine code.
calling convention , instruction selection , register allocation , prologue/epilogue.
How it works (conceptually)
- Instruction selection: Map IR operations to target ISA operations.
- Register allocation: Decide which temps live in registers and which spill to stack.
- Addressing/storage layout: Determine where variables
a,b,clive (registers/stack/memory). - Emit instructions: Produce final assembly.
Sketch for a simple target (illustrative)
Since constant folding already simplified to c = 25, code generation is trivial:
1; assume integer vars stored in registers or stack slots 2mov a, 3 3mov b, 4 4mov c, 25
If optimizations were not applied, you’d see multiply/add/sub instructions and temps, e.g., compute t1, t2, t3 and then store to c.
Full end-to-end walk-through (compact view)
Source
1int a = 3; int b = 4; int c = a * (b + 5) - 2;
- Lexical analysis → tokens (
int,a,=,3,*,(,b,+,5,),-,2,;) - Syntax analysis → AST with operator hierarchy:
- ( * a ( + b 5 ) ) 2 - Semantic analysis
- Build symbol table:
a:int,b:int,c:int - Type-check arithmetic: everything is
int
- Build symbol table:
- IR generation (3-address code)
t1 = b + 5t2 = a * t1t3 = t2 - 2c = t3
- Optimization
- Propagate constants:
t1=9,t2=27,t3=25 - Final:
c = 25
- Propagate constants:
- Code generation
- Emit simple target instructions to store
a,b,c
- Emit simple target instructions to store
Process: Send a program through compiler phases (example-driven)
- 1Step 1
Tokenize keywords/identifiers/literals/operators; build token stream with attributes and locations.
- 2Step 2
Parse tokens using the grammar; build parse tree/AST enforcing precedence and associativity (e.g.,
*before-). - 3Step 3
Create/populate symbol table; resolve identifiers; check types and assignment validity; annotate AST nodes with types.
- 4Step 4
Traverse AST and emit three-address IR using temporaries for intermediate subexpressions.
- 5Step 5
Apply optimizations (constant folding/propagation, dead code elimination) to simplify IR.
- 6Step 6
Select target instructions, allocate registers, assign storage for variables, and emit final machine/assembly code.
Common exam-style questions about compiler phases
Compiler phases: what they transform
High-level transformation of program representation across phases (conceptual)
Quick self-check flashcards
Knowledge Check
In a typical compiler pipeline, which phase converts a token stream into an AST (or parse tree)?