UNIT 2: COMPILER DESIGN - SHORT NOTES
I. INTRODUCTION & COMPILER OVERVIEW
A compiler is a program that translates source code written in a high-level language into an equivalent target program (typically machine code or assembly).
Phases of a Compiler (Linear Pass Model)
-
Lexical Analysis (Scanning): Reads input characters, groups them into lexemes, and produces a stream of tokens.
-
Syntax Analysis (Parsing): Organizes tokens into a hierarchical structure (parse tree/abstract syntax tree) according to the language's grammar.
-
Semantic Analysis: Checks for semantic consistency (e.g., type checking) and enriches the parse tree with semantic information (e.g., symbol table entries).
-
Intermediate Code Generation: Produces a platform-independent, low-level representation (e.g., Three-Address Code).
-
Optimization: Transforms intermediate code for improved performance (speed, size) without changing its meaning.
-
Code Generation: Maps intermediate code to the target machine's instruction set, producing the object code.
-
Symbol Table Management: A central data structure storing information about identifiers (name, type, scope, address, etc.) accessed by multiple phases.
-
Error Handling: Detects and reports errors at various phases, attempts recovery.
[!TIP] Separation of Lexical & Syntax Analysis
- Simplicity: Reduces the complexity of the parser.
- Efficiency: Lexical analysis can use specialized, fast techniques (e.g., finite automata).
- Portability: Lexical analyzer is often the only machine-dependent part.
- Tool Reuse: Standard lexical analyzer generators (LEX/Flex) can be used.
Bootstrapping
The process of using a compiler to compile itself.
-
Cross-Compilation: Compiler on machine A generates code for machine B.
-
Bootstrapping Phases:
-
Write a simple compiler (C1) for language L in machine code or another language.
-
Use C1 to compile a more advanced compiler (C2) written in L.
-
Use C2 (or C2 compiled by C1) to compile itself, achieving self-hosting.
-
II. LEXICAL ANALYSIS
Core Concepts
-
Token: A category of lexemes (e.g.,
id,num,+,if). -
Pattern: A rule describing the lexemes of a token (specified by regular expressions).
-
Lexeme: A sequence of characters matching a pattern.
Regular Expressions (RE)
Basic operators: | (union), · (concatenation), * (Kleene star), +, ?.
Examples:
-
Identifier:
[a-zA-Z_][a-zA-Z0-9_]* -
Integer:
[0-9]+ -
Even number of
as:(b*ab*ab*)*
Finite Automata (FA)
-
NFA (Nondeterministic FA): Can have ε-transitions and multiple moves on same input.
-
DFA (Deterministic FA): Single move per state/input. No ε-transitions.
-
Conversion: RE → ε-NFA (Thompson's Construction) → Subset Construction → DFA.
-
Minimization: Merge equivalent DFA states (using partitioning algorithm).
Input Buffering
-
Two-Buffer Scheme: Divides input into two buffers (size
N). Reduces overhead of reading each character. -
Sentinel Method: Append a special character (e.g.,
#) to the end of each buffer to avoid explicit end-of-buffer checks forlexemeBeginandforwardpointers.
LEX/Flex Tool
A lexical analyzer generator.
%{
/* C declarations */
%}
%%
"if" { return IF; }
[a-z]+ { yylval.id = lookup(yytext); return ID; }
[0-9]+ { yylval.num = atoi(yytext); return NUM; }
"+" { return PLUS; }
. { return yytext[0]; } /* Catch-all */
%%
/* User code (main, yyerror, etc.) */
Lexical Errors
-
Invalid character (e.g.,
@in identifier). -
Unterminated string/comment.
-
Handling: Typically, the scanner skips the offending character and continues, reporting an error message.
III. SYNTAX ANALYSIS
Context-Free Grammars (CFG)
A 4-tuple G = (V, T, P, S):
-
V: Set of non-terminals (syntactic variables). -
T: Set of terminals (tokens). -
P: Set of productionsA → α. -
S: Start symbol.
Derivations:
-
Leftmost: Replace leftmost non-terminal at each step.
-
Rightmost: Replace rightmost non-terminal at each step.
-
Parse Tree: Graphical representation of derivation, internal nodes are non-terminals, leaves are terminals.
Ambiguity
A grammar is ambiguous if a string has more than one parse tree (or leftmost/rightmost derivation).
Example:
E → E+E | E*E | a | b | cis ambiguous fora+b*c.
Resolution: Use precedence (
*over+) and associativity (left for both) rules, or rewrite grammar.
Grammar Transformations
-
Eliminating Left Recursion:
Immediate:
A → Aα | β→A → βA',A' → αA' | εIndirect: Reorder non-terminals.
-
Left Factoring:
A → αβ1 | αβ2→A → αA',A' → β1 | β2
FIRST & FOLLOW Sets
FIRST(X): Set of terminals that can begin strings derived from X.
-
If
X → ε, includeε. -
For
X → Y1Y2...Yk, addFIRST(Y1)(excludingε). IfFIRST(Y1)containsε, addFIRST(Y2), etc. FOLLOW(A): Set of terminals that can appear immediately to the right of A in some sentential form. -
$(end-of-input) is inFOLLOW(S). -
If
A → αBβ, addFIRST(β) \ {ε}toFOLLOW(B). -
If
A → αBorA → αBβwhereFIRST(β)containsε, addFOLLOW(A)toFOLLOW(B).
[!TIP] Predictive Parsing (LL(1)) requires:
- Grammar is non-left-recursive.
- Grammar is left-factored.
- For any two productions
A → α | β,FIRST(α) ∩ FIRST(β) = ∅.
- If
α⇒*ε, thenFIRST(α) ∩ FOLLOW(A) = ∅.
Top-Down Parsing
-
Recursive Descent: A set of mutually recursive procedures, one per non-terminal. May require backtracking (not LL(1)).
-
Predictive Parsing (LL(1)): Uses a parsing table
M[A, a](A: non-terminal, a: terminal). No backtracking.Table Construction: For each production
A → α:-
For each
a ∈ FIRST(α), addA → αtoM[A, a]. -
If
ε ∈ FIRST(α), for eachb ∈ FOLLOW(A), addA → αtoM[A, b].
-
Bottom-Up Parsing (Shift-Reduce)
Builds parse tree from leaves (tokens) to root (start symbol).
-
Handle: A substring that matches the RHS of a production, whose reduction is part of a reverse rightmost derivation.
-
LR Parsers: The most powerful shift-reduce parsers, using LR(k) tables (Left-to-right scan, Rightmost derivation, k lookahead).
LR Parsing Table Construction
-
Canonical Collection of LR(0)/LR(1) Items: An LR(1) item is
[A → α·Bβ, a]whereais lookahead. -
GOTO(I, X): Set of items reachable from state
Ion symbolX. -
ACTION & GOTO Tables:
-
ACTION[s, a]:-
shift tifGOTO(s, a) = t(for terminala). -
reduce A → βif[A → α·Aβ, a]in statesandGOTO(s', A) = s'(for LR(0) items). -
acceptif[S' → S·, $]in states. -
errorotherwise.
-
-
GOTO[s, A] = tfor non-terminalA.
-
Parser Types
| Feature | SLR(1) | LALR(1) | LR(1)/CLR(1) |
|---|---|---|---|
| Lookahead | 1 | 1 | 1 |
| State Merging | No (uses FOLLOW of LHS for reduce) | Yes (merges states with same core) | No (distinct lookahead) |
| Power | Least | Medium | Most (full lookahead) |
| Table Size | Smallest | Medium | Largest |
| Conflicts | May have false conflicts due to coarse FOLLOW | Fewer than SLR | Minimal (theoretically conflict-free for unambiguous grammars) |
[!TIP] SLR vs LALR: SLR uses
FOLLOW(A)for all reductions ofA. LALR merges states with identical cores (sameA → α·) but different lookaheads, then uses the union of lookaheads for reductions. This resolves many false SLR conflicts.
Operator Precedence Parsing
A simple bottom-up parser for operator grammars (no production has two adjacent non-terminals).
-
Precedence Relations:
a < b(a yields to b),a = b(a matches b),a > b(a takes precedence over b). -
Algorithm: Maintain stack. Compare top-of-stack terminal with current input token using precedence relation.
-
Limitations: Cannot handle all CFGs (e.g., no non-adjacent non-terminals), limited to operator grammars.
Syntactic Errors & Recovery
-
Panic Mode: Skip tokens until a synchronizing token (e.g.,
;,}) is found. -
Phrase-Level Recovery: Use error productions (e.g.,
A → error) in grammar to handle common mistakes. -
Global Correction (Least-Cost): Find minimal sequence of insertions/deletions to make input valid (expensive, rarely used).
IV. SYNTAX-DIRECTED TRANSLATION & SEMANTIC ANALYSIS
Attribute Grammars
A CFG augmented with attributes and semantic rules.
-
Synthesized Attributes: Values computed from children's attributes (bottom-up).
-
Inherited Attributes: Values passed from parent or siblings (top-down).
S-Attributed Definitions
-
All attributes are synthesized.
-
Evaluated during bottom-up parsing (e.g., LR parsing).
-
Example: Computing the value of an expression.
E → E1 + T { E.val = E1.val + T.val } E → T { E.val = T.val } T → num { T.val = num.lexval }
L-Attributed Definitions
-
Attributes can be synthesized or inherited, but for any production
A → X1 X2 ... Xn:-
Inherited attributes of
Xidepend only on:-
Attributes of
A(parent). -
Attributes of
X1...X(i-1)(left siblings).
-
-
-
Can be evaluated during top-down parsing (e.g., recursive descent) or bottom-up (with careful ordering).
-
Conversion to Translation Scheme: Place semantic actions immediately before the symbol whose inherited attribute they compute or after the last symbol for synthesized attributes.
Dependency Graph
-
Nodes: attribute occurrences in a parse tree.
-
Edges: From an attribute occurrence to another if its semantic rule depends on it.
-
Evaluation: Must be in topological order (no cycles → grammar is non-circular).
Backpatching
A technique for handling boolean expressions and control flow in code generation, where target addresses are not known initially.
-
Boolean Expressions: Maintain lists of unresolved jump addresses (true-list, false-list).
B → B1 || B2 { B.truelist = merge(B1.truelist, B2.truelist); B.falselist = B2.falselist; } B → B1 && B2 { B.truelist = B2.truelist; B.falselist = merge(B1.falselist, B2.falselist); } -
Control Flow Statements:
-
if B then S1: BackpatchB.truelistwithS1.nextlist. -
if B then S1 else S2: BackpatchB.truelistwithS1.code,B.falselistwithS2.code. -
while B do S: GenerateS.nextlist→ backpatch toB's code; backpatchB.truelisttoS.code,B.falselisttonext.
-
Syntax-Directed Translation Schemes
-
Conditional (if-then-else):
if B then S1 else S2 → B.true → L1, B.false → L2 → S1.code, goto L3 L1: S2.code L3: -
Loops (while):
while B do S → L1: B.code, B.true → L2, B.false → L3 → L2: S.code, goto L1 L3: -
Switch/Case Statements:
Generate jump table based on expression value. For each
caseconstantc_i, generateif (expr == c_i) goto L_i. Default case at the end.
Type Conversion
-
Implicit (Coercion): Compiler automatically converts (e.g.,
inttofloatinfloat f = 5;). -
Explicit (Cast): Programmer-specified (e.g.,
(int)3.14). -
In Expressions: Follow a type hierarchy (e.g.,
char < short < int < long < float < double). Operands are promoted to the highest type.
V. INTERMEDIATE CODE GENERATION
Need for IR
-
Machine independence.
-
Easier to analyze and optimize.
-
Bridges high-level language and target machine.
Three-Address Code (TAC)
A sequence of statements of the form x = y op z (or x = op y, goto L, if x relop y goto L).
Example: d = (a-b) + (a-c) + (a-c)
t1 = a - b
t2 = a - c
t3 = t1 + t2
t4 = t2 + t3
d = t4
Quadruples, Triples, Indirect Triples
| Structure | Fields | Example for x = (a+b)*c |
Pros | Cons |
|---|---|---|---|---|
| Quadruple | (op, arg1, arg2, result) |
( *, +, a, b, t1 ), ( *, t1, c, x ) |
Easy to optimize (addresses in result field). | Extra space for result. |
| Triple | (op, arg1, arg2) |
( +, a, b ), ( *, (1), c ) |
No extra temp names. | Hard to update (shared refs). |
| Indirect Triple | Pointer to triple array | t1 → ( +, a, b ), t2 → ( *, t1, c ) |
Easy reordering (like quadruples). | Extra indirection. |
Directed Acyclic Graphs (DAG)
-
Nodes: Variables, constants, or results of operations.
-
Edges: From operands to operation.
-
Purpose: Represent expressions for common subexpression elimination.
-
Construction: Builds upon triples. Identical nodes (same operation, same operands) are merged.
Example: Construct DAG for p + p*(q-r) + (q-r)*s
-
Leaf nodes:
p,q,r,s. -
q-r→ noden1. -
p * n1→ noden2. -
n1 * s→ noden3. -
p + n2→ noden4. -
n4 + n3→ rootn5.-
n1(forq-r) is shared → common subexpression eliminated. -
Final code:
t1 = q - r,t2 = p * t1,t3 = t1 * s,t4 = p + t2,t5 = t4 + t3.
-
Code Generation for Statements
-
Assignments: Direct TAC generation.
-
Conditionals: Use backpatching.
if B then S1 else S2→B.true → S1.code,B.false → S2.code. -
Loops:
while B do S→L1: B.code,B.true → L2,B.false → L3;L2: S.code, goto L1; L3:. -
Switch Statements:
-
Evaluate
expr. -
Generate
if expr == c1 goto L1,if expr == c2 goto L2, etc. -
goto Ldefault. -
L1: code1; goto Lend; L2: code2; ...; Ldefault: default_code; Lend:.
-
-
Procedure Calls:
-
Evaluate arguments.
-
Generate
paramstatements. -
call p, n(n = number of params). -
return(optional value).
-
VI. SYMBOL TABLES
Purpose
-
Store information about identifiers (name, type, scope, dimension, address, etc.).
-
Support insert, lookup, delete operations.
Data Structures
-
Linear Lists:
-
Unordered: Simple, but slow lookup (O(n)).
-
Ordered (Sorted): Binary search possible (O(log n)), but insertion costly (O(n)).
-
-
Hash Tables:
-
Hash Function:
h(name) = (sum of ASCII values) mod table_size. -
Collision Resolution:
-
Chaining: Buckets with linked lists.
-
Open Addressing: Linear probing, quadratic probing, double hashing.
-
-
Average lookup/insert: O(1) if load factor low.
-
-
Binary Search Trees (BST): Average O(log n), but can degenerate.
-
Tries (Prefix Trees): Efficient for string keys, especially with common prefixes.
Scope Management (Block-Structured Languages)
-
Nested Scopes: Inner scope can hide outer scope's identifier.
-
Organization: Use a stack of hash tables. Each block entry pushes a new hash table; exit pops it.
-
Global Table: Can maintain a global hash table with a scope number or access link (static chain) to resolve non-local references.
VII. ERROR HANDLING
Types of Errors
| Phase | Examples |
|---|---|
| Lexical | Invalid character, unterminated comment/string. |
| Syntactic | Missing ;, mismatched parentheses, wrong operator. |
| Semantic | Type mismatch, undeclared variable, incompatible assignment. |
| Logical | Infinite loop, incorrect algorithm (not detectable by compiler). |
Detection & Reporting
-
Each phase detects errors in its domain.
-
Good Error Message: Should indicate location (line/column), nature of error, and possibly suggestion.
-
Error Recovery: Allows further analysis (more errors found per compilation).
Recovery Strategies
-
Panic Mode: Discard tokens until a synchronizing token (e.g.,
;,}) is found. Simple, prevents infinite loops. -
Phrase-Level Recovery: Use error productions in grammar. E.g.,
S → error ;recovers from missing semicolon. -
Global Correction (Least-Cost): Compute minimal edit script (insert/delete/replace) to make input valid. Computationally expensive (O(n³)).
VIII. RUNTIME ENVIRONMENTS
Storage Allocation Strategies
| Strategy | Lifetime | Deallocation | Fragmentation | Typical Use |
|---|---|---|---|---|
| Static | Entire program | At program end | None | Global variables, code. |
| Stack | Procedure activation | On return (LIFO) | None (contiguous) | Local variables, parameters, return addresses. |
| Heap | Dynamic (programmer-controlled) | Explicit (free) or GC |
External & internal | Dynamic data structures (malloc, new). |
Activation Record (AR) / Frame
Structure for a procedure call (varies by language/architecture):
|---------------------------|
| Actual parameters (caller) |
| Return address |
| Control link (dynamic) | → Previous AR (for nested procedures)
| Access link (static) | → Enclosing scope's AR (for non-locals)
| Saved machine registers |
| Local variables |
| Temporary variables |
|---------------------------|
-
Calling Sequence: Caller pushes args, jumps to callee. Callee pushes return addr, control link, etc.
-
Parameter Passing:
-
Call by Value: Copy argument value.
-
Call by Reference: Pass address (aliasing possible).
-
Call by Value-Result (Copy-Restore): Copy in on call, copy out on return.
-
Stack vs Heap
-
Stack: Fast allocation/deallocation (pointer move), no fragmentation, size fixed at compile-time (or grows dynamically but contiguous).
-
Heap: Flexible, but allocation/deallocation slower (search free list), prone to fragmentation.
Polymorphic Functions & Overloading
-
Polymorphism: Function works with multiple types.
-
Compile-time (Ad-hoc): Function Overloading (same name, different signatures). Resolution based on static types of arguments.
-
Runtime (Parametric/Subtype): Templates/generics (C++, Java) or inheritance (Java, C#). Resolution at runtime (dynamic dispatch).
-
-
Overloading Example:
int max(int a, int b) { ... } float max(float a, float b) { ... } max(3, 5) → int version; max(3.0f, 5.0f) → float version.
IX. CODE OPTIMIZATION
Need & Goals
-
Improve execution speed, reduce code size, lower power consumption.
-
Must preserve program semantics (preservation of correctness).
Basic Blocks
-
A sequence of statements with:
-
Single entry (first statement executes only if control enters here).
-
Single exit (last statement transfers control out).
-
-
Construction: Partition code by leader statements (first stmt, targets of jumps, statements following jumps). Each leader starts a new block.
Flow Graphs
-
Nodes = Basic Blocks.
-
Directed edges = possible control flow between blocks (fall-through and jumps).
Reducible vs Non-Reducible Flow Graphs
-
Reducible: All loops have a single entry (natural loops). Most structured programs.
-
Non-Reducible: Contains "irreducible" loops (e.g., multiple entry points from
goto). Harder to optimize.
Loop Optimization Techniques
-
Loop Detection: Find natural loops (header dominates all members, back-edge exists).
-
Invariant Code Motion: Move calculations that produce same result on every iteration outside the loop.
Before: while(i<n) { x = y*z; ... } After: t = y*z; while(i<n) { x = t; ... } -
Strength Reduction: Replace expensive operation (
*) with cheaper (+).Before: for(i=0; i<n; i++) a[i] = 10*i; After: t = 0; for(i=0; i<n; i++) { a[i] = t; t += 10; } -
Induction Variables: Variables whose values change by a constant each iteration. Can be eliminated or replaced.
-
Loop Unrolling: Replicate loop body to reduce branch overhead.
Global Optimizations
-
Common Subexpression Elimination (CSE): Reuse previously computed value (using DAGs).
-
Copy Propagation: Replace
x = ywith uses ofxbyy. -
Dead Code Elimination: Remove statements whose results are never used.
-
Constant Propagation & Folding:
-
Propagation: If
x = 5, replacexwith5in subsequent code. -
Folding: Evaluate constant expressions at compile time.
x = 3 + 5 * 2→x = 13.
-
-
Variable Propagation: Similar to copy propagation but for more complex assignments.
-
Code Motion: Move invariant code out of loops (see above).
Peephole Optimization
-
Local optimization on a small window (peephole) of target code.
-
Examples:
-
Redundant load/store elimination.
-
Unreachable code removal.
-
Algebraic simplifications (
x*1 → x). -
Jump-to-jump elimination.
-
X. ADDITIONAL TOPICS (FREQUENTLY ASKED)
Input Buffering (Detailed)
-
Problem: Reading each character from input stream is slow.
-
Two-Buffer Scheme: Two buffers of size
N.lexemeBeginpoints to start of current lexeme,forwardscans ahead. -
Sentinel Method: Append a special character (e.g.,
#) to each buffer. Allows checkingforwardagainstlexemeBegin+N-1without explicit bound check for most cases. Whenforwardhits sentinel, refill buffer.
Operator Precedence Parser (Detailed)
-
Grammar Requirement: Operator Grammar (no two adjacent non-terminals in any production).
-
Precedence Relations Table: For terminals
a, b:-
a < b:ayields tob(pushb). -
a = b:amatchesb(pop both). -
a > b:atakes precedence (popaand reduce). -
No relation: Error.
-
-
Algorithm:
-
Push
$(end marker) onto stack. -
Let
a= top terminal on stack,b= next input token. -
While
a != $$\displaystyle ` or `b != $$:-
If
a < bora = b: pushb, get next token. -
If
a > b: popaand reduce (pop matching=symbols). -
Else: error.
-
-
-
Limitations: Cannot handle non-operator grammars (e.g.,
A → (A)).
Capabilities of CFG
-
Can Express: Recursion, nested structures, most programming language constructs (e.g., balanced parentheses,
if-else, loops). -
Cannot Express: Context-sensitive features (e.g., variable must be declared before use, type compatibility). These require semantic analysis.
-
Limitation: CFGs cannot enforce constraints like "same identifier used in declaration and use" without attributes/symbol tables.
Steps to Compute FIRST & FOLLOW
FIRST(X):
-
If
Xis terminal,FIRST(X) = {X}. -
If
X → εis a production, addεtoFIRST(X). -
For
X → Y1 Y2 ... Yk:-
Add
FIRST(Y1) \ {ε}. -
If
ε ∈ FIRST(Y1), addFIRST(Y2) \ {ε}, and so on. -
If
εis in allFIRST(Yi), addεtoFIRST(X).
-
FOLLOW(A):
-
Add
$toFOLLOW(S). -
For each production
B → α A β:- Add
FIRST(β) \ {ε}toFOLLOW(A).
- Add
-
For each production
B → α AorB → α A βwhereε ∈ FIRST(β):- Add
FOLLOW(B)toFOLLOW(A).
- Add
Basic Block Construction
-
Identify Leaders:
-
First instruction.
-
Target of a
goto/branch. -
Instruction immediately following a
goto/branch.
-
-
Form Blocks: From each leader to the next leader (exclusive).
-
Compute Successors: First instruction of next block (fall-through) and all branch targets.
Dependency Graph (in Optimization)
-
Nodes: Program statements or expressions.
-
Edges: From statement
S1toS2ifS1computes a value used byS2. -
Use: Determine evaluation order, identify independent statements for parallelization, detect loops for optimization.
Backpatching (Detailed Flow)
For boolean expressions generating jumps:
-
B → B1 or B2:-
B.truelist = merge(B1.truelist, B2.truelist) -
B.falselist = B2.falselist
-
-
B → B1 and B2:-
B.truelist = B2.truelist -
B.falselist = merge(B1.falselist, B2.falselist)
-
-
B → not B1:-
B.truelist = B1.falselist -
B.falselist = B1.truelist
-
-
Backpatch(list, target): Fill all addresses in
listwithtarget.
\boxed{\text{These notes cover all high-frequency topics from past RGPV exams (Dec 2024, Jun 2025, May 2023).}}