UNIT 5: Compiler Design – Short Notes
1. Lexical Analysis
Role and Importance
-
First phase of compilation.
-
Reads input characters, groups them into lexemes, and produces tokens.
-
Removes whitespace/comments, handles error reporting for invalid characters.
-
Benefits of separation: Simplifies syntax analysis, improves compiler modularity, allows use of specialized tools (Lex/Flex).
Finite Automata (FA) for Token Recognition
-
NFA (Non-deterministic): Multiple transitions on same input, ε-transitions allowed.
-
DFA (Deterministic): Single transition per input, no ε-moves. More efficient for implementation.
-
Regular expressions are converted to NFA (Thompson's construction), then to DFA (subset construction), minimized for efficiency.
Example: Regular expression for strings with odd number of
aand odd number ofb:
$$(a(ba)^*b \mid b(ab)^*a) \cup (a(ba)^*a(ba)^*b \mid b(ab)^*b(ab)^*a)$$
A simpler approach: Odd
aand oddbimplies total length is even, and both counts odd. Can be represented as:
$$(a(ba)^*b \mid b(ab)^*a)^+$$
Regular Expressions for Language Specification
-
Used to describe token patterns (identifiers, numbers, operators).
-
Example: Identifier:
[a-zA-Z_][a-zA-Z0-9_]*
Lex/Flex Implementation
-
Structure of a Lex program:
definitions %% rules %% user subroutines -
Recognizing identifiers and keywords:
-
Define pattern for identifiers:
[a-zA-Z_][a-zA-Z0-9_]* -
Keywords listed explicitly before identifier pattern to give them precedence.
-
Example:
"if" { return IF; } "else" { return ELSE; } [a-zA-Z_][a-zA-Z0-9_]* { yylval = lookup(yytext); return IDENTIFIER; }
-
Input Buffering
-
Sentinel method: Use a special character (e.g.,
\0) to mark end of buffer, avoiding bounds checks. -
Two-buffer scheme: Split input into two equal halves; when pointer reaches end of first, reload second and switch. Handles arbitrarily long tokens efficiently.
2. Syntax Analysis
Overview of Parsing Techniques
| Technique | Direction | Example | Notes |
|---|---|---|---|
| Top-down | Start symbol → input | Recursive descent, Predictive | May require left-factoring |
| Bottom-up | Input → start symbol | LR, Operator precedence | More powerful, handles left recursion |
Predictive Parsing
-
Predictive parsing table:
M[Non-terminal, Terminal]→ production or error. -
Construction:
-
Compute FIRST and FOLLOW sets.
-
For production
A → α:-
For each terminal
ainFIRST(α), addA → αtoM[A, a]. -
If
ε ∈ FIRST(α), for eachbinFOLLOW(A), addA → αtoM[A, b].
-
-
-
Example: Grammar
A → (B) | a,B → B,B | A. Not LL(1) due to left recursion inB.
Operator Precedence Parsing
-
Define precedence relations (
<·,=,·>) between terminals based on grammar. -
Algorithm:
-
Push
$(end marker) onto stack. -
Let
a= current input token,b= top stack terminal. -
If
b <· a, shiftaonto stack. -
If
b ·> a, popband reduce by a productionA → βwhereβcontains no non-terminals. -
If
b = a, shift/reduce for matching=(e.g., parentheses).
-
-
Limitation: Only works for operator grammars (no two adjacent non-terminals in RHS).
LR Parsing Family
-
General LR:
-
States represent LR(1) items:
[A → α·Bβ, a](dot position, lookaheada). -
Canonical collection of LR(1) items via
GOTOandCLOSURE. -
Parsing table:
ACTION[i, a]= shift/reduce/accept/error;GOTO[i, A]= state.
-
SLR(1)
-
Uses FOLLOW of LHS non-terminal for reduce actions.
-
Construction:
-
Build canonical collection of LR(0) items (no lookahead).
-
For reduce item
A → α·in statei, for eachainFOLLOW(A), setACTION[i, a] = reduce A → α.
-
-
Check SLR(1) correctness: No conflicts in
ACTIONtable. Weaker than LALR/CLR.
LALR(1)
-
Merge LR(1) states with same core (same LR(0) item) but different lookaheads.
-
Combines lookaheads of merged states.
-
Comparison:
| Parser | States | Power | Example | |--------|--------|-------|---------| | SLR(1) | Fewest | Weakest | May have spurious conflicts | | LALR(1) | Moderate | Intermediate | Used in Yacc/Bison | | CLR(1) | Most | Strongest | No false conflicts |
Canonical LR(1) / CLR(1)
-
Full LR(1) items with lookaheads.
-
No state merging; most precise.
-
Larger tables but avoids LALR ambiguities.
Parse Tree for Ambiguous Grammar
-
Grammar:
E → E+E | E*E | a | b | c -
Ambiguity:
a+b*chas two parse trees (left/right associativity, precedence). -
Resolution: Use precedence (
*>+) and associativity (left for both) to get unique tree:E /|\ E + E | | a E |\ E * E | | b c
FIRST and FOLLOW Sets
-
FIRST(X):
-
If
Xis terminal,FIRST(X) = {X}. -
If
X → εis production, addεtoFIRST(X). -
If
X → Y₁...Yₖ, addFIRST(Y₁)(excludingε). If allYᵢderiveε, addε.
-
-
FOLLOW(A):
-
If
Sis start symbol, add$toFOLLOW(S). -
If
B → αAβ, addFIRST(β) \ {ε}toFOLLOW(A). -
If
B → αAorB → αAβwithε ∈ FIRST(β), addFOLLOW(B)toFOLLOW(A).
-
Error Handling in Syntax Analysis
-
Syntactic errors: Missing operators, unmatched parentheses, unexpected token.
-
Recovery strategies:
-
Panic mode: Skip tokens until synchronizing token (e.g.,
;,}). -
Phrase-level: Insert/delete/replace tokens to recover (e.g., missing
;). -
Error productions: Augment grammar with productions for common errors.
-
3. Syntax-Directed Translation & Intermediate Code
Attribute Grammars
-
S-attributed: Only synthesized attributes (computed from children).
-
Evaluated in bottom-up parsing.
-
Example:
E → E₁ + T { E.val = E₁.val + T.val }
-
-
L-attributed: Synthesized + inherited attributes (from parent/siblings).
-
Inherited attributes passed left-to-right during parsing.
-
Can be evaluated in both top-down and bottom-up.
-
Bottom-up Evaluation of Attributes (S-attributed)
-
Each parse tree node has a synthesized attribute.
-
Values computed as reduction occurs in LR parsing.
-
Example:
E → E₁ + T E.val = E₁.val + T.valWhen reducing
E₁ + TtoE, use values fromE₁andT.
Translation Schemes for Switch/Case
-
Syntax-directed translation:
switch (E) { case c₁: S₁; case c₂: S₂; ... default: S_d; } -
Intermediate code:
E.code if E ≠ c₁ goto L1 S₁.code goto L_end L1: if E ≠ c₂ goto L2 S₂.code goto L_end ... L_d: S_d.code L_end:
Intermediate Code Forms
| Form | Structure | Pros | Cons |
|---|---|---|---|
| Quadruples | (op, arg1, arg2, result) |
Fixed format, easy optimization | Temporary names needed |
| Triples | (op, arg1, arg2) |
No temporaries | Ambiguous reference (indirect) |
| Indirect triples | (index, pointer) |
Efficient for optimization | Extra indirection |
Directed Acyclic Graph (DAG)
-
Definition: DAG for expression where common subexpressions share nodes.
-
Construction:
-
Leaf nodes for variables/constants.
-
Interior nodes for operators; if identical operator and operands exist, reuse node.
-
-
Example:
a + a*(b-c) + (b-c)*dStep 1: t1 = b - c Step 2: t2 = a * t1 Step 3: t3 = t1 * d Step 4: t4 = a + t2 Step 5: t5 = t4 + t3DAG: Node for
t1used twice;aused twice but as different operands.
Backpatching
-
For boolean expressions and flow-of-control (if, while).
-
Boolean expressions generate jumps with unfilled addresses.
-
Backpatching list: Maintain lists of quadruple indices needing addresses.
-
Example for
if (B) S:-
B.codegenerates(jtrue, B.true, _, _)and(jfalse, B.false, _, _). -
S.codegenerated. -
Backpatch
B.truelist to start ofS.code. -
B.falselist points to next instruction afterS.
-
Type Conversion
-
Implicit/explicit conversions in intermediate code.
-
Example:
int i; float f; f = i + 2.5;- Generate TAC:
t1 = inttofloat(i),t2 = 2.5,t3 = t1 + t2,f = t3.
- Generate TAC:
4. Runtime Environments & Storage Allocation
Symbol Tables
-
Purpose: Store identifier attributes (type, scope, address, etc.).
-
Scope management: Nested scopes via stack of symbol tables or static parent links.
-
Data structures:
| Structure | Search Time | Insert Time | Notes | |-----------|-------------|-------------|-------| | Linear list | O(n) | O(1) | Simple, slow for large | | Binary Search Tree | O(log n) | O(log n) | Ordered | | Hash table | O(1) avg | O(1) avg | Most common, collisions handled |
Activation Records (AR)
-
Structure:
[Return address] [Dynamic link (old AR pointer)] [Parameters] [Local variables] [Temporaries] -
Example: Nested procedure
PcallsQ:AR for P: ... AR for Q: return addr dynamic link → AR P params locals
Storage Allocation Strategies
| Strategy | Lifetime | Example | Management |
|---|---|---|---|
| Static | Entire program | Global variables | Fixed addresses at compile time |
| Stack | Procedure calls | Local variables, parameters | Push/pop on call/return |
| Heap | Dynamic allocation | malloc, new |
Manual/automatic (GC) |
Parameter Passing Mechanisms
-
Call-by-value: Copy argument value.
-
Call-by-reference: Pass address; callee modifies caller's variable.
-
Call-by-value-result (copy-in copy-out): Like value, but copy back on return.
Polymorphic Functions & Overloading
-
Polymorphic: Function works with multiple types (e.g.,
print(int),print(float)). -
Overloading: Same name, different parameter types.
-
Resolution: At compile time based on argument types (static binding).
5. Code Optimization
Basic Blocks
-
Definition: Sequence of statements with single entry (first) and single exit (last). No jumps inside except at end.
-
Construction:
-
Identify leaders (first statement, target of jump, statement after jump).
-
Form block from leader to next leader-1.
-
-
Flow graph: Nodes = basic blocks; edges = possible control flow.
-
Reducible flow graph: Can be constructed by repeatedly splitting edges; most programming languages yield reducible graphs.
Local Optimizations (within a basic block)
| Optimization | Description | Example |
|---|---|---|
| Constant folding | Evaluate constant expressions at compile time | x = 3 * 4 → x = 12 |
| Common subexpression elimination (CSE) | Reuse computed value | t1 = a + b; ... t2 = a + b → reuse t1 |
| Copy propagation | Replace variable with its assigned value | x = y; ... a = x + 5 → a = y + 5 |
| Dead code elimination | Remove unused assignments | x = 5; (if x never used) |
Loop Optimizations
-
Code motion (loop-invariant code motion):
-
Move statements that compute same value each iteration outside loop.
-
Example:
for (i=0; i<n; i++) { x = y * z; // invariant if y,z not changed in loop a[i] = x + i; }→ Move
x = y*zbefore loop.
-
-
Induction variable simplification:
-
Replace induction variables with linear functions of loop counter.
-
Example:
j = i + 1inside loop; replace uses ofjwithi+1.
-
-
Strength reduction:
-
Replace expensive operations (multiplication) with cheaper (addition).
-
Example:
x = i * 8→x = i << 3(bit shift).
-
Peephole Optimization
-
Principle: Examine small window (peephole) of target code for redundant/inefficient patterns.
-
Examples:
-
Redundant load/store:
MOV R1, R2; MOV R2, R1→ delete. -
Algebraic:
x = x * 1→ delete. -
Jump chaining:
goto L1; L1: goto L2→goto L2.
-
Global Data Flow Analysis (Brief)
-
Purpose: Gather information across basic blocks for global optimizations (e.g., reaching definitions, live variables).
-
Equations: For each node
n,IN[n] = ∪ OUT[p]for predecessorsp;OUT[n] = f(IN[n], n). -
Used for CSE, dead code elimination across blocks.
6. Error Handling
Lexical Phase Errors
-
Examples:
-
Invalid character:
@in identifier. -
Unterminated string:
"hello(no closing"). -
Illegal escape sequence:
\xin string.
-
-
Recovery: Delete/replace invalid char, report line/column.
Syntactic Phase Errors
-
Examples:
-
Missing operator:
a b + c(expected*or+betweenaandb). -
Unmatched parentheses:
(a + b(missing)). -
Unexpected token:
if x then y else(missing statement afterelse).
-
-
Recovery: Panic mode (skip to
;or}), error productions (e.g.,S → error).
7. Additional Specialized Topics
Bootstrapping
-
Definition: Using a compiler to compile itself.
-
Methods:
-
Cross-compilation: Compile on machine A for machine B.
-
Bootstrap: Write simple compiler (in machine code) for language L, then use it to compile more advanced compiler written in L.
-
Dependency Graphs in Optimization
-
Nodes = statements; edge
S₁ → S₂ifS₂uses variable defined inS₁. -
Used for code motion: A statement can be moved out of loop if all its dependencies are loop-invariant and it dominates all uses.
Input Buffering (Detailed)
-
Two-buffer scheme:
-
Input divided into two halves of size
N. -
lexemeBeginandforwardpointers. -
When
forwardreaches end of first half, reload second half and swap. -
Sentinel: Special char (e.g.,
\0) at buffer end to avoid checking bounds on each character read.
-
Operator Precedence Parser (Detailed Algorithm)
-
Initialize stack with
$. -
While input not
$:-
Let
a= current input token,b= top stack terminal. -
If
b <· a: shiftaonto stack. -
If
b ·> a: popband reduce by productionA → βwhereβis string of terminals popped. -
If
b = a: shift/reduce for matching=(e.g., parentheses). -
Else: syntax error.
-
-
Accept when stack =
$$\displaystyle ` and input = ` $$.
Exam Tips:
- Regular expressions: Practice converting odd/even constraints.
- LR tables: Always compute canonical collection first; SLR uses FOLLOW, LALR/CLR use lookaheads.
- DAG construction: Identify common subexpressions by matching operator and operands (order matters for non-commutative).
- Backpatching: Maintain lists for
true/falsein boolean expressions; forif, backpatchtruelist to then-part.
- Activation record: Know layout for nested procedures (static vs dynamic links).
- Loop optimization: Code motion requires loop-invariant check (no definitions in loop, no uses before statement).