UNIT 4: Compiler Design - Short Notes
I. Introduction and Compiler Overview
Phases of a Compiler
A compiler operates as a sequence of phases, each transforming the source program representation.
| Phase | Primary Role | Key Output |
|---|---|---|
| 1. Lexical Analysis | Reads input stream, groups characters into tokens (lexemes), removes whitespace/comments. | Stream of tokens (e.g., id, =, (, id, /, id, ), *, ...). |
| 2. Syntax Analysis | Parses token stream using a Context-Free Grammar (CFG) to build a parse tree/syntax tree. Checks grammatical structure. | Parse Tree / Abstract Syntax Tree (AST). |
| 3. Semantic Analysis | Checks semantic consistency (type checking, type conversion). Annotates AST with attributes (e.g., type info). | Annotated Syntax Tree. |
| 4. Intermediate Code Generation | Translates annotated AST into a machine-independent, low-level representation (e.g., Three-Address Code). | Intermediate Code (TAC). |
| 5. Optimization | Transforms intermediate code to improve execution efficiency (speed, memory) without changing behavior. | Optimized Intermediate Code. |
| 6. Code Generation | Maps optimized intermediate code to target machine code (register allocation, instruction selection). | Target Machine Code (assembly/object code). |
| 7. Symbol Table | Central repository maintained across phases. Stores identifier info (name, type, scope, address). | Data structure (hash table, tree) with entries. |
| 8. Error Handling | Detects & reports errors at each phase. Attempts recovery to continue analysis. | Error messages, diagnostics. |
Separation of Lexical & Syntax Analysis
-
Simplicity: Lexical analysis deals with regular patterns (tokens), syntax analysis with context-free structures. Separation simplifies each phase's design.
-
Efficiency: Lexical analyzer can use fast, table-driven techniques (finite automata). Syntax analysis is more complex.
-
Portability: Token definitions & lexical rules are often language-specific, while parser logic is more general. Separating them aids in retargeting.
-
Ignoring Whitespace/Comments: Lexical phase removes these non-significant characters, simplifying the parser's token stream.
Bootstrapping
-
Concept: The process of using a compiler to compile itself. Often involves writing the initial compiler in a different language (or machine code), then using that compiler to compile a more advanced version of itself written in its own source language.
-
Significance: Essential for self-hosting compilers. Enables the compiler's source code to be written in the very language it compiles, improving maintainability and portability.
-
Types:
-
Simple Bootstrapping: Write compiler C₁ for language L in language M. Use C₁ (in M) to compile C₂ (in L) to get C₃ (in machine code).
-
Cross-Compilation: Compiler runs on machine A but generates code for machine B. First step in bootstrapping for a new platform.
-
Comprehensive Example: Compilation of a = (x/y) * (y + x - z)
-
Lexical Analysis:
[id(a), =, (, id(x), /, id(y), ), *, (, id(y), +, id(x), -, id(z), )] -
Syntax Analysis: Builds parse tree based on grammar (e.g.,
E → E * E | E / E | E + E | E - E | (E) | id). -
Semantic Analysis: Checks types (assume all
idareint). No type errors. Annotates nodes with typeint. -
Intermediate Code (TAC):
t1 = x / y t2 = y + x t3 = t2 - z t4 = t1 * t3 a = t4 -
Optimization: (Minimal here). Could potentially combine
t2andt3if no reuse. -
Code Generation (x86-like):
mov eax, x cdq idiv y ; eax = x/y mov ebx, eax ; save t1 in ebx mov eax, y add eax, x ; eax = y+x sub eax, z ; eax = (y+x)-z imul ebx ; eax = t1 * t3 mov a, eax
II. Lexical Analysis
Role & Responsibilities
-
Tokenization: Scan source characters, form lexemes, and output corresponding tokens (
<token-name, attribute-pointer>). -
Remove Noise: Strip whitespace and comments.
-
Error Detection: Report illegal characters or malformed lexemes (e.g.,
12.3.4). -
Correlation with Symbol Table: For tokens like
idandliteral, often inserts into symbol table and returns a pointer to the entry as the attribute.
Finite Automata & Regular Expressions
-
Regular Expressions (RE): Algebraic notation to describe regular languages (sets of strings).
- Example: Strings with odd number of
aand odd number ofb:
- Example: Strings with odd number of
$$ ( (a(ba)^*b) + (b(ab)^*a) ) (a|b)^* $$
* `(a(ba)*b)`: Starts/ends with `a...b`, odd `a`, odd `b`.
* `(b(ab)*a)`: Starts/ends with `b...a`, odd `a`, odd `b`.
* `(a|b)*`: Any suffix doesn't change parity.
-
Finite Automata (FA): Machine (states, transitions) that accepts/rejects strings. RE → NFA (Thompson's construction) → DFA (subset construction) → Minimized DFA.
-
Lexical Spec → RE → DFA → Code: Standard implementation path.
LEX/FLEX
-
Structure of a Lex Program:
%{ /* C declarations (global vars, functions) */ %} %% /* Rules Section: Pattern { Action } */ [a-zA-Z_][a-zA-Z0-9_]* { return ID; } /* Recognizes identifiers */ "+"|"-"|"*"|"/" { return *yytext; } /* Returns operator char */ [ \t\n] ; /* Skip whitespace */ . { fprintf(stderr, "Illegal char: %c\n", *yytext); } %% /* User Code Section (main, yyerror, etc.) */ int main() { yylex(); return 0; } -
How it works: Lex generates
lex.yy.ccontaining a table-driven DFA scanner.yylex()function drives the DFA.
Token Recognition: Identifiers & Keywords
-
Challenge: Keywords (
if,while) and identifiers share the same lexical pattern ([a-zA-Z_][a-zA-Z0-9_]*). -
Solution:
-
Lexical analyzer recognizes the lexeme as an
idpattern. -
Looks up the lexeme in the symbol table.
-
If found and marked as keyword, returns the keyword token.
-
If not found (or found as ordinary identifier), returns
idtoken and possibly inserts new entry.
-
-
Example: Lexeme
"while"→ lookup → found as keyword → return tokenWHILE.
Input Buffering
-
Need: To look ahead several characters to recognize the longest possible lexeme (maximal munch).
-
Technique: Sentinel Method
-
Use two buffers (
lexemeBegin,forwardpointers) of sizeN. -
End of each buffer marked by a sentinel character (e.g.,
EOF). -
When
forwardreaches sentinel, refill that buffer. Avoids checkingforwardagainst buffer end on every character read. -
Benefit: Reduces per-character branching overhead, speeding up scanning.
-
[!TIP] Exam Focus: Be prepared to write a simple Lex program for identifiers/operators and explain the sentinel method. The odd
a/bRE is a classic.
III. Syntax Analysis
Role of the Parser
-
Receives token stream from lexical analyzer.
-
Constructs a parse tree (or AST) to represent the grammatical structure.
-
Detects syntax errors (missing
;, mismatched()). -
Reports errors meaningfully and attempts recovery to find more errors.
Context-Free Grammars (CFG)
-
Components:
G = (V, T, P, S)-
V: Set of non-terminals (syntactic variables, e.g.,E,T,F). -
T: Set of terminals (tokens, e.g.,id,+,*). -
P: Set of productions (rules), e.g.,E → E + T. -
S: Start symbol.
-
-
Derivation: Applying productions to replace a non-terminal.
-
Leftmost Derivation: Always replace leftmost non-terminal.
-
Rightmost Derivation: Always replace rightmost non-terminal.
-
-
Parse Tree: Graphical representation of derivation. Interior nodes = non-terminals, leaves = terminals.
-
Ambiguity: A grammar is ambiguous if a string has >1 distinct parse tree (or leftmost/rightmost derivation).
-
Example:
E → E + E | E * E | a | b | cfor stringa+b*c.-
Tree 1 (
+at root):(a+b) * c -
Tree 2 (
*at root):a + (b*c)
-
-
Solution: Use precedence rules or rewrite grammar to be unambiguous:
E → E + T | T T → T * F | F F → a | b | c | (E)
-
Top-Down Parsing
Starts from start symbol, attempts to derive the input string.
-
Recursive Descent: Write a procedure for each non-terminal. Backtracking may be needed (inefficient).
-
Predictive Parsing (Non-Recursive): Uses a parsing table and a stack. No backtracking.
-
FIRST(X): Set of terminals that can begin any string derived from
X. -
FOLLOW(X): Set of terminals that can appear immediately to the right of
Xin some sentential form.$(EOF) is inFOLLOW(S). -
Algorithm for FIRST/FOLLOW: Standard procedures (see below).
-
Predictive Parsing Table
M[A, a]:-
If
a ∈ FIRST(α), thenA → αinM[A, a]. -
If
ε ∈ FIRST(α)anda ∈ FOLLOW(A), thenA → αinM[A, a]. -
If
ε ∈ FIRST(α)and$ ∈ FOLLOW(A), thenA → αinM[A, $]. -
Else error.
-
-
Example Grammar:
A → (B) | a,B → B,B | A-
FIRST:
FIRST(A) = {(, a},FIRST(B) = {(, a} -
FOLLOW:
FOLLOW(A) = {), ,},FOLLOW(B) = {), ,} -
Table: (Construct using rules above. Grammar is LL(1) if no conflicts).
-
-
Bottom-Up Parsing
Starts from input string, reduces to start symbol using productions in reverse.
-
Shift-Reduce Parsing: Uses a stack and input buffer.
-
Shift: Move next input token onto stack.
-
Reduce: Pop RHS of production from stack, push LHS.
-
-
LR Parsing Family: Most powerful deterministic bottom-up parsers.
-
LR(k):
L= left-to-right scan,R= rightmost derivation (in reverse),k= lookahead tokens. -
Comparison:
| Parser | Construction | Table Size | Power | | :--- | :--- | :--- | :--- | | SLR(1) | FOLLOW sets of LHS | Smallest | Least (may have spurious conflicts) | | LALR(1) | Merged LR(1) states | Medium | Between SLR & LR | | LR(1)/CLR(1) | Full LR(1) states | Largest | Most powerful (no conflicts if grammar is LR(1)) |
-
Example Grammar:
S → L = R | R,R → L,L → *R | id- CLR(1) Table Construction: (Standard algorithm: build LR(1) items, compute
GOTO/ACTION). Result has no conflicts → grammar is LR(1).
- CLR(1) Table Construction: (Standard algorithm: build LR(1) items, compute
-
Example Grammar for SLR:
S → S + A | A,A → A B | B,B → B * | a | b-
Compute
FOLLOW(S)={$$\displaystyle , +}`, `FOLLOW(A)={+, $$},FOLLOW(B)={*, +, $}. -
Build state machine, then SLR table using FOLLOW for reductions. Check for shift-reduce/reduce-reduce conflicts.
-
-
Operator Precedence Parser
-
Theory: For a subset of CFGs (operator grammars) where no production has two adjacent non-terminals on RHS.
-
Method: Define precedence relations (
<·,=·,·>) between terminals based on grammar. -
Parsing: Uses a stack. Compare top-of-stack terminal with next input terminal using precedence table to decide shift/reduce.
-
Limitation: Less powerful than LR, cannot handle all constructs (e.g.,
if-then-elseambiguity).
[!TIP] Exam Focus: Constructing predictive, SLR, and CLR(1) tables is very frequent. Practice step-by-step: compute FIRST/FOLLOW → build canonical collection → fill ACTION/GOTO. Know differences between SLR/LALR/LR.
IV. Syntax-Directed Translation & Semantic Analysis
Syntax-Directed Definitions (SDD)
Associates attributes with grammar symbols and semantic rules with productions.
-
Attributes:
-
Synthesized: Computed from children's attributes in parse tree. Flows upward.
-
Inherited: Computed from parent/siblings' attributes. Flows downward/ sideways.
-
-
Types of SDDs:
-
S-attributed: Only synthesized attributes. Evaluated in bottom-up (LR) parsing.
-
L-attributed: Inherited attributes restricted:
X → Y₁ Y₂ ... Yₖ, inherited attrs ofYᵢcan depend on:-
Attributes of
X(parent). -
Attributes of
Y₁...Yᵢ₋₁(left siblings). -
Not on
Yᵢ₊₁...Yₖ(right siblings) or their descendants.
- Can be evaluated in top-down (LL) or bottom-up (LR) order.
-
-
-
Dependency Graph: For a parse tree node, draw edges from each attribute's defining occurrence to its used occurrences. A cycle → circular definition (error).
- Example: For
A → B C, with inheritedB.i = A.symand synthesizedA.s = B.s + C.s, edges:A.sym → B.i,B.s → A.s,C.s → A.s.
- Example: For
Translation Schemes
SDD with embedded semantic actions placed within production RHS. Shows order of evaluation.
-
Converting L-attributed to Scheme: Place inherited attribute calculations before the symbol they belong to. Place synthesized after.
- Example:
A → {B.in = A.in;} B {A.s = B.s + 1;}.
- Example:
-
Case/Switch Statement Scheme:
switch E { case 1: S1; break; case 2: S2; break; ... default: Sd; }Translation:
E → E1 { gen("if E1.val == 1 goto L1"); ... gen("goto Ldefault"); } | E2 { gen("if E2.val == 2 goto L2"); ... } | ... | Ed { gen("goto Ldefault"); }(Simplified: generate code to test each case value and jump).
Semantic Analysis
-
Type Systems:
-
Type Checking: Verify operator-operand compatibility (e.g.,
int + boolerror). -
Type Conversion (Coercion): Implicit conversion to expected type.
-
Widening (safe):
int → float. -
Narrowing (may lose info):
float → int(often requires explicit cast).
-
-
Example:
float f; int i; f = i;→ implicit widenitofloat.
-
-
Polymorphic Functions & Overloading:
-
Overloading: Same name, different parameter types (e.g.,
print(int),print(float)). Resolved at compile-time by signature matching. -
Polymorphism: Same name, same interface but different implementations (e.g.,
virtualfunctions in C++). Often resolved at runtime (dynamic dispatch). -
Compiler's Task: For overloading, build symbol table entries for each signature. At call site, match argument types to select correct function.
-
[!TIP] Exam Focus: Distinguish S- vs L-attributed. Draw dependency graphs. Write translation scheme for
switch. Explain type conversion with examples.
V. Intermediate Code Generation
Three-Address Code (TAC)
-
Definition: Sequence of simple statements with at most one operator per statement.
-
General Form:
x = y op z(orx = op y,goto L,if x relop y goto L,param,call,return). -
Why "Three-Address"? Each statement has three addresses (operands/result), though some have fewer.
Forms of TAC
-
Quadruples:
(op, arg1, arg2, result).-
Example:
a + a*(b-c) + (b-c)*d( -, b, c, t1 ) ( *, a, t1, t2 ) ( -, b, c, t3 ) // Common subexpr not yet eliminated ( *, t3, d, t4 ) ( +, t2, t4, t5 ) ( =, t5, -, a )
-
-
Triples:
(op, arg1, arg2); results referenced by position (no separate result field). Can be explicit (list of triples) or implicit (referenced by pointer).-
Example: Same expression.
1: ( -, b, c ) 2: ( *, a, 1 ) 3: ( -, b, c ) // Duplicate! 4: ( *, 3, d ) 5: ( +, 2, 4 ) a = 5 -
Problem: Duplicate computation (triples 1 & 3). DAG solves this.
-
-
Indirect Triples: List of pointers to triples. Allows optimization by reordering pointer list without moving triple data.
[ &t1, &t2, &t3, &t4, &t5 ]wheret1=( -, b, c ), etc.
Directed Acyclic Graphs (DAGs)
-
Definition: A DAG for an expression is a directed graph where:
-
Leaf nodes = identifiers/constants.
-
Interior nodes = operators.
-
Each node represents a value.
-
Key Property: Common subexpressions are represented by a single node (shared).
-
-
Construction: Process expression (postorder). For each node:
-
If operand is leaf, create node.
-
If operator, check if node
(op, left-child, right-child)already exists in current node list.-
Yes: Use existing node (common subexpr found).
-
No: Create new node.
-
-
-
Examples:
-
a + a*(b-c) + (b-c)*d+ / \ + d / \ * a / \ a - / \ b c-
(b-c)appears once (shared node). -
TAC from DAG:
t1 = b - c; t2 = a * t1; t3 = t2 + t1*d; a = t3 + a? Wait, careful: -
Correct DAG:
+ / \ * + / \ / \ a - a d / \ b c -
Nodes:
-(b,c) shared by both*and+? No,(b-c)*dusesd, nota. Let's build:-
b-c→ node N1. -
a * N1→ node N2. -
N1 * d→ node N3. -
N2 + N3→ node N4. -
N4 + a→ node N5 (butais leaf). -
Final DAG root is
+with childrenN4anda.
-
-
TAC:
t1 = b - c; t2 = a * t1; t3 = t1 * d; t4 = t2 + t3; a = t4 + a? That's not the original expression. Original:a + (a*(b-c)) + ((b-c)*d). -
Correct:
a + t2 + t3→(a + t2) + t3. -
DAG:
+ / \ + t3 / \ a t2 | * / \ a t1 | - / \ b c -
TAC:
t1 = b - c; t2 = a * t1; t3 = t1 * d; t4 = a + t2; a = t4 + t3.
-
-
p + p*(q-r) + (q-r)*s-
q-r→ node N1 (shared). -
p * N1→ node N2. -
N1 * s→ node N3. -
p + N2→ node N4. -
N4 + N3→ node N5. -
DAG root N5.
-
TAC:
t1 = q - r; t2 = p * t1; t3 = t1 * s; t4 = p + t2; p = t4 + t3(assuming result inp).
-
-
a*b + (c/(d/e))*d-
d/e→ N1. -
c / N1→ N2. -
N2 * d→ N3. -
a * b→ N4. -
N4 + N3→ N5. -
TAC:
t1 = d / e; t2 = c / t1; t3 = t2 * d; t4 = a * b; a = t4 + t3.
-
-
Backpatching
-
Purpose: Handle jumps in boolean expressions and control flow (
if,while) where target address is unknown until later. -
Mechanism: For a jump
if x relop y goto _, generate a placeholder and maintain a list of pending jumps that need this target. -
Lists:
-
makelist(i): Returns list containing just indexiof a quadruple. -
merge(p1, p2): Concatenates two lists. -
backpatch(list, target): Fillstargetfield of all quadruples inlist.
-
-
Example for
if (a < b) S else T:-
a < b→ generatesif a < b goto _→M1 = makelist(nextquad). -
Scode generated. -
Sends withgoto _→M2 = makelist(nextquad). -
backpatch(M1, quad(S)+1)(jump toTif false). -
Tcode generated. -
backpatch(M2, nextquad)(jump afterT).
-
Different Intermediate Forms for d = (a-b) + (a-c) + (a-c)
-
Naive TAC (no CSE):
t1 = a - b t2 = a - c t3 = t1 + t2 t4 = a - c // Duplicate! t5 = t3 + t4 d = t5 -
With CSE (using DAG):
t1 = a - c // Compute once t2 = a - b t3 = t2 + t1 d = t3 + t1 -
Quadruples:
(-, a, b, t1) (-, a, c, t2) // t2 is common subexpr (+, t1, t2, t3) (+, t3, t2, t4) (=, t4, -, d) -
Triples:
1: (-, a, b) 2: (-, a, c) 3: (+, 1, 2) 4: (+, 3, 2) d = 4
[!TIP] Exam Focus: Constructing DAGs for given expressions is extremely frequent (seen in 3 papers). Practice step-by-step: identify common subexpressions, build DAG, then generate TAC. Understand backpatching lists for control flow.
VI. Symbol Tables
Purpose & Operations
-
Purpose: Store information about identifiers (name, type, scope, line number, size, address/register, etc.) for use by all phases.
-
Operations:
-
insert(name, type, ...): Add new entry (often at declaration). -
lookup(name): Find entry forname(at use). -
delete(name)ordelete_scope(scope_id): Remove entries (e.g., end of scope). -
print_table(): For debugging/error messages.
-
Data Structures for Implementation
| Structure | Description | Pros | Cons | Typical Use |
|---|---|---|---|---|
| Linear List | Array/record of entries. insert at end, lookup sequential search. |
Simple, space efficient. | lookup O(n) slow for large programs. |
Small scopes, teaching. |
| Binary Search Tree (BST) | Entries ordered by name. insert/lookup O(log n) average. |
Faster than list, ordered traversal. | Unbalanced tree → O(n). | Medium-sized programs. |
| Hash Table | Hash function on name → bucket index. Collision resolution (chaining/open addressing). | Average O(1) for insert/lookup. |
Hash function design, collisions, resizing cost. | Most common in real compilers. |
Lexical Scoping: Often implemented as stack of hash tables (one per scope). lookup searches from top (current scope) down. insert adds to top. delete pops top table. |
Example: Symbol Table for Sample Program
int x; // Global scope
void f(int y) { // Scope f
int z;
x = y + z; // x: global, y: param, z: local
}
Hash Table (per scope):
-
Global Scope Table:
| Name | Type | Scope | Addr | | :--- | :--- | :--- | :--- | |
x|int| global |R1| |f|func| global |addr_f| -
Scope
fTable (on top of stack):| Name | Type | Scope | Addr | | :--- | :--- | :--- | :--- | |
y|int|f(param) |R2| |z|int|f(local) |R3| -
Lookup
xinf: Searchftable → not found → search global → found atR1.
[!TIP] Exam Focus: Compare data structures (time/space). Explain scope management with stack of tables. Be ready to draw symbol table for a given nested program.
VII. Error Handling
Lexical vs. Syntactic Errors
| Aspect | Lexical Error | Syntactic Error |
|---|---|---|
| Definition | Invalid character sequence that doesn't match any token pattern. | Token sequence violates grammar rules. |
| Detection Phase | Lexical Analyzer. | Parser (Syntax Analysis). |
| Examples | @ in C program, 12.3.4 (invalid float), "unclosed string. |
Missing ;, if without then, mismatched {}, else without if. |
| Recovery | Panic mode: Skip characters until valid token (e.g., whitespace). | Panic mode: Discard tokens until one in FOLLOW(current_nonterminal). Phrase-level: Use error productions. |
Error Recovery Strategies
-
Panic Mode: Simplest. On error, discard input tokens until a synchronizing token (e.g.,
;,}) is found. Loses some input, but guarantees progress. -
Phrase-Level Recovery: Enumerate common errors at a point and provide corrective actions (insert/delete/replace tokens). Requires knowledge of common mistakes.
-
Error Productions: Add augmented productions to grammar for common errors (e.g.,
missing_semicolon → S). Parser uses these to parse erroneous input and report specific error. -
Global Correction: (Theoretical) Find minimal number of changes (insert/delete/replace) to make input valid. Uses Levenshtein distance. Impractical for real compilers due to cost.
Error Handling Phase in Compiler
-
Not a separate phase. Error detection/recovery is integrated into each phase.
-
Lexical: Report illegal character; use panic mode to skip.
-
Syntax: Parser uses error routines on parsing table conflict. Reports line/column, expected vs. found token.
-
Semantic: Type checker reports type mismatch; may insert conversion or flag error.
-
Goal: Maximize number of errors detected per compilation, minimize cascading errors (one error causing many false positives).
[!TIP] Exam Focus: Differentiate lexical vs. syntactic errors with examples. List recovery strategies (panic mode most common). Explain how errors are reported (line numbers, expected tokens).
VIII. Runtime Environments
Activation Records (AR)
-
Definition: The storage layout for a single procedure invocation (call). Also called stack frame.
-
Typical Layout (grows downwards):
------------------- <- SP (Stack Pointer) after call | Actual Params | (passed by caller) ------------------- | Return Address | (where to jump after return) ------------------- | Control Link | (pointer to caller's AR, for non-local access) ------------------- | Access Link | (pointer to AR of lexically enclosing scope, for nested procedures) ------------------- | Saved Registers | (caller-saved/callee-saved) ------------------- | Local Variables | (including temporaries) ------------------- | ... | ------------------- <- FP (Frame Pointer) points here (fixed during call) -
Example (Nested):
procedure A; var x: int; procedure B; var y: int; begin ... end; begin ... end;- Call to
BfromA:B's AR has Access Link pointing toA's AR (to accessx). Control Link points toA's AR (for return).
- Call to
Storage Allocation Strategies
| Strategy | Description | When Used | Management |
|---|---|---|---|
| Static Allocation | Compile-time fixed addresses. Global/static variables. | Global variables, code. | Simple, no runtime overhead. |
| Stack Allocation | LIFO allocation. For procedure calls, local variables. | Local variables, parameters, return addresses. | SP moves on call/return. Efficient, supports recursion. |
| Heap Allocation | Dynamic, arbitrary order. malloc/new. |
Dynamic data structures (linked lists, objects). | Garbage Collection or manual free/delete. Complex, fragmentation possible. |
Procedure Calls
-
Parameter Passing:
-
Call-by-Value: Copy value of actual param to formal param. Changes in callee not visible to caller.
-
Call-by-Reference: Pass address of actual param. Callee accesses original variable. Changes visible to caller. (
int &xin C++,varin Pascal). -
Call-by-Value-Result (Copy-in/Copy-out): Copy in at start, copy out at end. Like value, but changes visible (but tricky with aliases).
-
-
Return Values: Typically returned in a designated register (e.g.,
eaxin x86) or via pointer parameter. -
Stack Unwinding: On
return, restore saved registers, pop AR (reset SP to Control Link), jump to return address.
[!TIP] Exam Focus: Draw activation record for nested procedures (show access/control links). Compare stack vs heap allocation. Explain parameter passing methods with examples.
IX. Optimization
Basic Blocks & Flow Graphs
-
Basic Block: A sequence of consecutive statements with:
-
Single entry (first statement only).
-
Single exit (last statement only, a jump).
-
No jumps into or out of the middle.
-
-
Construction: Partition TAC into blocks by leaders (first statement, target of jump, statement after jump).
-
Flow Graph: Nodes = basic blocks. Edges = possible control flow (fall-through or jump).
-
Characteristics of Basic Blocks: Used for local optimization (within block). All statements execute if entry executed.
-
Reducible vs. Non-Reducible Flow Graphs:
-
Reducible: Can be constructed by structured constructs (
if,while,for). Has no jumps into loops from outside. Most programming languages produce reducible graphs. Easier for analysis. -
Non-Reducible: Contains arbitrary jumps (e.g.,
gotointo loops). Harder to analyze, may require more complex algorithms.
-
Loop Optimization
-
Invariant Code Motion (Code Motion): Move loop-invariant statements (compute same value each iteration) outside the loop.
-
Condition: Statement
Sis inside loopL, and for all paths throughL,S's operands are unchanged beforeS. -
Example:
for(i=0; i<n; i++) { x = y * z; // Invariant if y,z not changed in loop a[i] = x + i; }Optimized:
x = y * z; for(i=0; i<n; i++) { a[i] = x + i; }
-
-
Induction Variables: Variable
xis induction if it changes by a constant each iteration (e.g.,iinforloop). -
Strength Reduction: Replace expensive operations (multiplication) with cheaper ones (addition) using induction variables.
-
Example:
x = i * 8in loop whereiincrements by 1.- Introduce
t = 0; each iteration:t = t + 8,x = t. Replaces*8with+8.
- Introduce
-
Global Optimizations
-
Constant Folding: Evaluate constant expressions at compile time.
3 * 4→12,'A' + 1→'B'.
-
Constant Propagation: If
x = 5, then replace uses ofxwith5. -
Common Subexpression Elimination (CSE): Eliminate duplicate computations of same expression. Use DAGs or value numbering.
-
Variable Propagation (Copy Propagation): Replace
y = xfollowed by... x ...with... y .... Simplifies expressions. -
Dead Code Elimination: Remove statements whose computed values are never used (e.g.,
x = 5;wherexnot used later). -
Strength Reduction: As above, but applied globally (e.g.,
x*2→x+x,x*4→x<<2).
Peephole Optimization
-
Local optimization on a small sliding window (peephole) of target code (e.g., 3-5 instructions).
-
Examples:
-
Redundant load/store elimination:
mov ax, x; mov x, ax→ delete second. -
Branch optimization:
jmp L1; L1: ...→ deletejmpifL1immediately follows. -
Algebraic simplifications:
x = x * 1→ delete. -
Sequence reduction:
push a; push b; add→push (a+b)if possible.
-
-
Pros: Simple, fast, effective.
-
Cons: Local view may miss global opportunities.
Dependency Graphs for Optimization
-
Data Dependencies: Between statements
SᵢandSⱼ.-
Flow Dependence (True):
Sⱼuses value defined bySᵢ. (Sᵢ → Sⱼ) -
Anti-Dependence:
Sⱼdefines variable thatSᵢuses. (Sⱼ → Sᵢ) -
Output Dependence: Both
SᵢandSⱼdefine same variable. (Sᵢ ↔ Sⱼ)
-
-
Instruction Scheduling: Use dependency graph to reorder independent instructions to avoid stalls (e.g., fill delay slots). Topological sort of graph gives valid orderings.
[!TIP] Exam Focus: Loop optimization (invariant code motion, strength reduction) with step-by-step example is crucial. Know definitions of all global optimizations (CSE, constant folding/propagation, dead code). Explain peephole with examples. Draw dependency graph for small code fragment.
X. Advanced Topics (Brief Notes)
Operator Precedence Parser
-
For: Operator grammars (no two adjacent non-terminals in RHS).
-
Method: Define precedence relations (
<·,=·,·>) between terminals.-
a <· +ifahas higher precedence than+. -
+ =· +if+is left-associative. -
+ ·> aif+has lower precedence thana.
-
-
Parsing: Stack holds terminals. Compare
stack-topwithnext-inputusing precedence table.-
stack-top <· next→ shift. -
stack-top ·> next→ reduce by some productionA → αwhereα's terminals match stack top. -
=→ equal (for productions likeE → E + E,+relates to+).
-
-
Example: Grammar
E → E + E | E * E | id. Precedence:* > +, both left-assoc. Relations:id <· +,+ <· *,+ =· +,* =· *,* ·> id, etc.
Bootstrapping
-
Self-Compilation: Writing a compiler C for language L in language L itself.
-
Process:
-
Write interpreter for L in machine code or another language M.
-
Write compiler C₁ for L in M (or a subset of L).
-
Use C₁ to compile compiler C₂ (written in L) to machine code.
-
Now C₂ is a native compiler for L.
-
-
Cross-Compilation: Compiler runs on host A but generates code for target B. First step when no native compiler exists for B. Uses bootstrapping eventually.
Polymorphic Functions & Overloading
-
Overloading: Same function name, different parameter types. Resolved at compile-time by matching argument types to function signatures in symbol table.
- Example:
max(int, int),max(float, float).
- Example:
-
Polymorphism (Parametric): Function works for any type (e.g.,
template <typename T> T max(T a, T b)). Type is instantiated at call site. -
Compiler's Role: For overloading, build signature table. At call, perform type matching (best fit). For polymorphism, generate type-specific code (instantiation) or use generic representations.
KEY FORMULAS & ALGORITHMS
FIRST Set Computation
FIRST(X):
if X is terminal: FIRST(X) = {X}
if X is non-terminal:
for each production X → Y1 Y2 ... Yk:
add FIRST(Y1) to FIRST(X)
if ε ∈ FIRST(Y1) then add FIRST(Y2), ... until non-nullable
if all Y1..Yk nullable, add ε
if X → ε is a production, add ε
FOLLOW Set Computation
FOLLOW(S): {$} (S is start symbol)
FOLLOW(A) for non-terminal A:
for each production ... B → ... A α ...:
add FIRST(α) \ {ε} to FOLLOW(A)
if ε ∈ FIRST(α) or α is empty:
add FOLLOW(B) to FOLLOW(A)
LR(1) Item & Canonical Collection
-
LR(1) Item:
[A → α·β, a]whereais lookahead terminal. Means: we have parsedα, expectingβ, and if we reduceA→α, the next token should bea. -
Closure(I): Add items for non-terminals after
·with all possible lookaheads (fromFIRSTof remaining string). -
GOTO(I, X): Move
·pastXin all items ofI, then take closure. -
Canonical Collection: Start with
closure({[S' → ·S, $]}). Repeatedly applyGOTOfor all symbols until no new states.
Three-Address Code Forms Summary
| Form | Structure | Pros | Cons |
|---|---|---|---|
| Quadruples | (op, arg1, arg2, result) |
Simple, fixed fields. | Temporary names for results; renaming hard. |
| Triples | (op, arg1, arg2) |
No temporaries; refer by position. | Duplicate subexprs; hard to optimize (no result field). |
| Indirect Triples | [pointer to triple] |
Easy to reorder/optimize; no duplication. | Extra indirection; pointer management. |
DAG Construction Algorithm (for expression)
function create_node(op, left, right):
if node(op, left, right) exists in node list:
return existing node
else:
create new node, add to list, return it
function leaf(value):
if value is identifier/constant:
if leaf node exists: return it
else: create new leaf, return it
- Process expression in postorder (children before parent).
COMMON PITFALLS & EXAM TIPS
[!TIP] Parsing Tables: The #1 exam trap is incorrect FIRST/FOLLOW leading to wrong table. Double-check:
FIRST(A)includes terminals only (not ε initially).
FOLLOW(A)includes terminals only (not ε, but$).
- For
A → α, ifε ∈ FIRST(α), useFOLLOW(A)for reductions in SLR/LALR.
- SLR uses
FOLLOWfor all reductions → may have spurious conflicts.
- CLR(1) uses specific lookaheads from LR(1) items → more precise.
[!TIP] DAGs: Students often forget to share nodes for identical subexpressions. Remember: same operator, same children → same node. Draw DAG clearly with shared nodes.
[!TIP] Activation Record: Know the purpose of each field. Access Link vs Control Link is a common question. Access Link = lexical parent (for non-local variable access). Control Link = dynamic caller (for return).
[!TIP] Loop Optimization: For invariant code motion, prove invariance: all operands must be unchanged in the loop. If an operand is a local variable that is assigned inside the loop, it's not invariant.
[!TIP] Lex vs Parser: Lex handles patterns (RE → DFA). Parser handles structure (CFG → parse tree). Lex returns tokens; parser builds tree. They communicate via symbol table for
id/keywords.
[!TIP] S- vs L-attributed: If all attributes are synthesized → S-attributed (bottom-up). If inherited allowed but only from parent/left siblings → L-attributed (can do top-down or bottom-up). L-attributed covers most language constructs (e.g., inherited
typein declarations).
Final Note: This unit is highly practical. For exams, be prepared to construct (parsing tables, DAGs, TAC, symbol tables) and explain concepts with examples. Always trace a small expression (like a + a*(b-c) + (b-c)*d) through all relevant phases—it appears repeatedly in past papers.