UNIT 5: Compiler Design (Based on Past Exam Questions)
1. Compiler Phases and Overview
A compiler is a program that translates source code (high-level language) into target code (machine/assembly language). The process is divided into distinct, sequential phases.
List of Compiler Phases:
-
Lexical Analysis (Scanning)
-
Syntax Analysis (Parsing)
-
Semantic Analysis
-
Intermediate Code Generation
-
Code Optimization
-
Code Generation
-
Symbol Table Management (่ดฏ็ฉฟ all phases)
-
Error Handling (่ดฏ็ฉฟ all phases)
Role of Each Phase:
| Phase | Primary Role | Key Output |
|---|---|---|
| Lexical Analysis | Reads input stream, groups characters into tokens (lexemes), removes whitespace/comments. | Stream of tokens (e.g., id, +, =). |
| Syntax Analysis | Checks if token stream conforms to the language's syntax (grammar). Builds a parse tree. | Hierarchical parse tree / syntax tree. |
| Semantic Analysis | Checks for semantic errors (type mismatch, undeclared variables). Annotates parse tree with type info. | Annotated syntax tree. |
| Intermediate Code Generation | Translates annotated syntax tree into a machine-independent intermediate representation (IR). | IR (e.g., quadruples, triples). |
| Code Optimization | Transforms IR for improved performance (speed, size) without changing meaning. | Optimized IR. |
| Code Generation | Maps optimized IR to target machine code, allocating registers/memory. | Target assembly/object code. |
[!TIP] Exam Focus: Be prepared to list phases in order and state the key output/input of each. Questions often link phases (e.g., "What does syntax analysis receive from lexical analysis?").
2. Lexical Analysis
Importance of Separation from Syntax Analysis:
-
Simplicity: Separates low-level character processing from high-level syntax rules.
-
Efficiency: Lexical analyzer can be optimized separately (e.g., using finite automata).
-
Portability: Machine-dependent aspects (like character encoding) are isolated.
-
Implementation: Allows use of specialized tools (like LEX).
Finite Automata (FA) for Token Recognition:
-
Regular Expressions (pattern for a token class) are converted to a Non-deterministic FA (NFA).
-
NFA is converted to a Deterministic FA (DFA).
-
DFA is implemented as a program (transition table) that scans input characters and recognizes token boundaries.
-
Example: Identifier pattern
[a-zA-Z][a-zA-Z0-9]*โ DFA with states for start letter, subsequent alphanumeric chars.
LEX Tool:
-
Structure: A LEX program (
.lfile) has three sections:%{ /* C declarations (global vars, functions) */ %} /* Regular expressions (definitions) */ %% /* Rules: Pattern { Action (C code) } */ %% /* User code (main, auxiliary functions) */ -
Example for
idand arithmetic operators:%{ #include "y.tab.h" /* Token definitions from parser */ %} digit [0-9] letter [a-zA-Z] %% {letter}({letter}|{digit})* { yylval = strdup(yytext); return ID; } "+" { return PLUS; } "-" { return MINUS; } "*" { return MUL; } "/" { return DIV; } "=" { return ASSIGN; } [ \t\n] ; /* Skip whitespace */ . { return yytext[0]; } /* Catch-all */ %% int yywrap() { return 1; }-
yylvalpasses token value (e.g., string forid) to parser. -
yytextholds matched lexeme.
-
LEX Compiler Overview:
lex source.l โ generates lex.yy.c (C program with yylex() function) โ compile with GCC โ a.out (scanner executable). The scanner reads stdin, uses DFA to find longest matching pattern, executes associated action.
[!TIP] Common Pitfall: LEX uses maximal munch (longest match). If
==and=are both patterns,==is recognized as one token, not two=.
3. Syntax Analysis (Parsing)
Parsing Techniques:
| Top-Down | Bottom-Up |
|---|---|
| Starts from start symbol, derives string. | Starts from input string, reduces to start symbol. |
| Recursive Descent, LL(k). | Shift-Reduce, LR(k) (SLR, LALR, CLR(1)). |
| May require left-factoring & left-recursion elimination. | More powerful, handles all LR(k) grammars. |
Operator Precedence Parsing:
-
A simple shift-reduce parser for expressions.
-
Based on precedence relations (
<ยท,=ยท,ยท>) between terminals. -
No need for grammar to be in a specific form (unlike LR).
-
Limitation: Cannot handle non-operator tokens or ambiguous grammars well. Only for operator grammars (no two adjacent non-terminals on RHS).
-
Example: For
E -> E+E | E*E | (E) | id, precedence:id <ยท +,+ ยท> *,* <ยท id, etc.
LR Parsers:
-
SLR(1): Uses FOLLOW sets for reduce actions. May have conflicts if a production's FOLLOW set intersects with a shift action's lookahead.
-
LALR(1): Merges LR(1) states with identical core (items without lookahead). More precise than SLR, fewer conflicts. Used in YACC/BISON.
-
CLR(1) / LR(1): Uses full LR(1) items (production + dot position + lookahead). No unnecessary reductions. Most powerful, largest tables.
-
Comparison: SLR vs LALR:
| Feature | SLR(1) | LALR(1) | | :--- | :--- | :--- | | Lookahead | FOLLOW(A) for reduction of
A -> ฮฑ. | Precise lookahead from merged states. | | Power | Weakest (may reject valid LR(1) grammars). | Stronger (accepts all SLR(1) + more). | | Table Size | Smallest. | Larger than SLR, smaller than full LR(1). | | Conflicts | More likely. | Fewer conflicts than SLR. |
CLR(1) Parsing Table Construction (Example Grammar):
Given: S -> L = R | R, L -> *R | id, R -> L
-
Augment Grammar: Add
S' -> S. -
Compute LR(1) Items: For each production
A -> ฮฑยทBฮฒ, a, find all productionsB -> ฮณand compute FIRST(ฮฒa) for lookaheadb. -
Build Canonical Collection of LR(1) Items: Use
GOTOon states. -
Construct Table:
-
ACTION[state, terminal]:
-
If item
[A -> ฮฑยทaฮฒ, b]โshiftona. -
If item
[A -> ฮฑยท, a](andA != S') โreduce A -> ฮฑ. -
If item
[S' -> Sยท, $]โaccept.
-
-
GOTO[state, non-terminal]: State after shifting
A.
-
Parse Tree Construction & Ambiguity Elimination:
-
Ambiguous Grammar: Allows multiple parse trees for same string (e.g.,
E -> E+E | E*E | idfora+b*c). -
Elimination: Use precedence and associativity rules to rewrite grammar.
-
Example:
E -> E+T | T,T -> T*F | F,F -> (E) | id. -
Now
a+b*chas unique tree:E(E(a)+T(T(b)*F(c))).
-
Context-Free Grammars (CFG):
-
Capabilities: Describe nested structures (matching parentheses, if-then-else), recursion.
-
Limitations: Cannot express context-sensitive constraints (e.g., variable must be declared before use, type matching). Need semantic analysis for this.
FIRST and FOLLOW Sets:
-
FIRST(X): Set of terminals that can begin any string derived from
X.- Rules: If
X -> aฮฑ,a โ FIRST(X). IfX -> ฮต,ฮต โ FIRST(X). IfX -> Yฮฑ,FIRST(Y) - {ฮต} โ FIRST(X); ifฮต โ FIRST(Y), also addFIRST(ฮฑ).
- Rules: If
-
FOLLOW(A): Set of terminals that can appear immediately to the right of
Ain some sentential form.$(EOF) is inFOLLOW(S).- Rules: If
S -> ฮฑAฮฒ, thenFIRST(ฮฒ) - {ฮต} โ FOLLOW(A). IfS -> ฮฑAorS -> ฮฑAฮฒwhereฮต โ FIRST(ฮฒ), thenFOLLOW(S) โ FOLLOW(A).
- Rules: If
-
Computation Steps: Iteratively apply rules until no new symbols added.
[!TIP] Exam Trap: For
FOLLOW(A), you addFIRST(ฮฒ)only ifฮฒis present afterA. IfAis at end (S -> ฮฑA), addFOLLOW(S).
4. Syntax-Directed Translation
Attribute Grammars:
-
S-attributed: All attributes are synthesized. Evaluated in bottom-up (post-order) traversal.
- Example:
E -> E1 + T { E.val = E1.val + T.val }
- Example:
-
L-attributed: Attributes can be synthesized or inherited, but inherited attributes from left siblings only. Can be evaluated in top-down (pre-order) traversal.
- Example:
D -> T LwhereLinherits type fromT.
- Example:
Bottom-up Evaluation of S-Attributes:
-
When reducing
A -> ฮฑin a parse, computeA's synthesized attributes from attributes of symbols inฮฑ. -
Example Grammar:
E -> E1 + T { E.val = E1.val + T.val } E -> T { E.val = T.val } T -> int { T.val = int.lexval } -
For input
int + int:-
Reduce
int -> T:T.val = int.lexval(say 5). -
Reduce
int -> T:T.val = 2. -
Reduce
E -> T:E.val = T.val = 5. -
Shift
+. -
Reduce
E -> E1 + T:E.val = E1.val + T.val = 5 + 2 = 7.
-
Converting L-attributed Grammar to Translation Scheme:
-
For each production
A -> X1 X2 ... Xn, place semantic actions between symbols where inherited attributes are needed. -
Inherited attribute of
Xifrom left siblings is computed by an action beforeXi. -
Inherited attribute from
Ais computed by an action at the leftmost position. -
Synthesized attributes are computed by an action at the rightmost position.
-
Example:
D -> T LwithL.in = T.type.- Scheme:
D -> T { L.in = T.type } L
- Scheme:
Syntax-Directed Translation for Case Statements:
-
Grammar:
stmt -> case expr of case_list end case_list -> case_list case | case case -> const ':' stmt -
Translation: Generate jump tables or if-else chains.
-
Backpatching: For each
case, generate conditional jump tostmtifexpr == const. Use backpatching lists to fill addresses after all cases are known.
5. Intermediate Code Generation
Forms of Intermediate Code:
| Quadruple | Triple | Indirect Triple |
|---|---|---|
(op, arg1, arg2, result) |
(op, arg1, arg2) |
Array of pointers to triples. |
| Result is a temp variable. | Result is implicit (position). | Allows reordering without changing triples. |
| Easy for optimization (common subexpr). | Compact, but harder to optimize (no names). | Combines benefits: compact + optimizable. |
Example: d = a - b โ (-, a, b, t1) |
(-, a, b) |
[ref to triple1] |
Generation for Arithmetic Expressions:
-
Expression:
d = (a-b) + (a-c) + (a-c) -
Quadruples:
(-, a, b, t1) (-, a, c, t2) (+, t1, t2, t3) (-, a, c, t4) // Common subexpr not yet eliminated (+, t3, t4, t5) (=, t5, -, d) -
Triples:
(-, a, b) (-, a, c) (+, 1, 2) (-, a, c) // Duplicate triple (+, 3, 4) (=, 5, d) -
Indirect Triple:
Triple[1] = (-, a, b) Triple[2] = (-, a, c) Triple[3] = (+, 1, 2) Triple[4] = (-, a, c) // Same as 2 Triple[5] = (+, 3, 4) Triple[6] = (=, 5, d) Indirect[] = {1,2,3,4,5,6}
Backpatching for Boolean Expressions:
-
Used for conditional statements (
if,while) and boolean expressions that generate jumps. -
Concept: Generate code with unfilled jumps (
(if_false, a, -)). Maintain a list of quadruple indices needing the same target address. -
Example:
if (a < b) stmt1 else stmt2-
a < bโ(<, a, b, -)โ generateif_falsejump, add its index to listL1. -
Code for
stmt1. -
Generate unconditional
gototostmt2, indexj1. -
backpatch(L1, nextInstr)โ fills allif_falsejumps to point tostmt2. -
Code for
stmt2.
-
[!TIP] Key:
makelist(i)creates list{i}.merge(L1, L2)concatenates lists.backpatch(L, addr)fills all jumps inLwithaddr.
6. Symbol Table Management
Purpose:
-
Store information about identifiers (name, type, scope, address, line number).
-
Support insertion (on declaration), lookup (on use), and scope management.
Operations:
-
insert(name, attributes) -
lookup(name)โ returns entry orNULL. -
delete(name)ordelete_scope(scope_level).
Data Structures:
| Structure | Description | Pros | Cons |
|---|---|---|---|
| Linear List | Array or linked list of entries. | Simple. | Slow lookup O(n). |
| Binary Search Tree | Ordered tree. | Faster lookup O(log n). | Unbalanced tree โ O(n). |
| Hash Table | Hash function on name โ bucket. | Fast average lookup O(1). | Collisions, fixed size. |
| Lexical Scoping | Stack of hash tables (one per scope). | Easy scope enter/exit. | Lookup may traverse stack. |
[!TIP] Most Common: Hash table with buckets (chaining) is standard for its speed. For nested scopes, use a stack of hash tables.
7. Storage Allocation
Static vs Dynamic Allocation:
| Static | Dynamic | |
|---|---|---|
| When | Compile-time. | Run-time. |
| Memory | Data segment (global/static). | Stack (local vars), Heap (malloc/new). |
| Size | Fixed. | Variable. |
| Access | Direct (absolute address). | Indirect (via pointers/registers). |
| Example | int global; static int x; |
int local; int *p = malloc(...); |
Heap Storage Allocation Strategy:
-
Manages unbounded memory requests at runtime.
-
Strategies:
-
First-fit: Allocate first block large enough.
-
Best-fit: Allocate smallest block that fits (minimizes waste).
-
Worst-fit: Allocate largest block (leaves large leftover).
-
-
Fragmentation:
-
External: Free memory exists but not contiguous.
-
Internal: Allocated block larger than requested (wasted inside).
-
-
Garbage Collection: Reclaims unreachable heap objects (e.g., mark-sweep).
Activation Record (AR) / Stack Frame:
Structure for a procedure/function call:
|---------------------------|
| Actual Parameters | (caller pushes)
|---------------------------|
| Return Address |
|---------------------------|
| Control Link (Dynamic) | (pointer to caller's AR)
|---------------------------|
| Access Link (Static) | (pointer to non-local scope)
|---------------------------|
| Local Variables |
|---------------------------|
| Temporary Variables |
|---------------------------| <-- SP (Stack Pointer)
- Management:
CALLpushes AR, sets new SP.RETURNpops AR, restores SP.
8. Code Optimization
Basic Blocks and Flow Graphs:
-
Basic Block: Sequence of statements with single entry (first stmt) and single exit (last stmt). No jumps inside except at end.
-
Construction: Identify leaders (first stmt, target of jump, after jump). Statements from leader to next leader-1 form a block.
-
Flow Graph: Nodes = basic blocks. Edges = possible control flow (jumps, fall-through).
Optimization of Basic Blocks:
-
Common Subexpression Elimination (CSE): Recompute value only once.
t1 = a * b; ... t2 = a * b;โt1 = a * b; ... t2 = t1;
-
Dead Code Elimination: Remove statements whose results are never used.
-
Constant Folding: Evaluate constant expressions at compile time.
x = 3 * 4 + 5;โx = 17;
-
Algebraic Simplifications:
x * 1 โ x,x + 0 โ x.
Loop Optimization Techniques:
-
Code Motion: Move invariant computations out of loop.
while (i < n) { x = y * z; ... }โt = y * z; while (...) { x = t; ... }
-
Induction Variable Elimination: Replace multiple induction vars with one.
i = 0; while (i < n) { j = i * 4; ... i = i+1; }โj = 0; incr = 4; while (...) { ... j = j + incr; }
-
Strength Reduction: Replace expensive op with cheaper one.
-
x * 8โx << 3(multiplication โ shift). -
j = i * 8โj = i << 3.
-
Variable Propagation:
-
Replace uses of a variable with its known constant value.
-
Example:
a = 5; b = a + 2;โb = 5 + 2โb = 7(after constant folding).
Reducible vs Non-Reducible Flow Graphs:
-
Reducible Flow Graph: Can be reduced to a single node by repeatedly contracting edges (removing loops, merging nodes). All structured programs (with
if,while,forwithoutgoto) produce reducible graphs. -
Non-Reducible: Contains irreducible loops (multiple entry points, e.g., from
goto). Harder for data-flow analysis. -
Importance: Most optimizations (like reaching definitions) assume reducible graphs.
Dependency Graphs:
-
Directed graph where nodes = statements or expressions, edges = data/control dependencies.
-
Data Dependency:
S2uses result ofS1โ edgeS1 โ S2. -
Control Dependency:
S2executes only if condition inS1is true/false. -
Use: Detect parallelism, schedule instructions, identify optimization opportunities (e.g., if no dependency, reorder).
[!TIP] Loop Optimization: Always look for invariant code first (code motion), then induction variables, then strength reduction.
9. Additional Topics
Bootstrapping in Compiler Construction:
-
Problem: How to compile a compiler written in language X when no X compiler exists?
-
Solution: Bootstrap โ compile in stages.
-
Write compiler C1 for X in a lower-level language L (e.g., assembly, or another existing language).
-
Use L compiler to compile C1 โ produces X compiler C2 (written in X).
-
Now C2 can compile X programs directly. Can also recompile C2 with itself (C3) to ensure purity.
-
-
Example: First C compiler written in assembly. Once
cc1exists, it can compile future C compilers written in C.
[!TIP] Analogy: Like "pulling yourself up by your bootstraps" โ using an initial simple version to build a better version of itself.