UNIT 3: Compiler Design (Based on Past Exam Questions)
1. Introduction to Compilers
1.1 Definition and Purpose
A compiler is a program that translates source code written in a high-level language (source language) into an equivalent target language (usually machine code or assembly). Its primary purpose is to detect and report errors in the source program and generate efficient executable code.
1.2 Phases of a Compiler
A typical compiler is partitioned into a sequence of phases, each with a specific function:
| Phase | Primary Function | Key Output |
|---|---|---|
| 1. Lexical Analysis | Scans source characters, groups them into tokens (lexemes). | Stream of tokens. |
| 2. Syntax Analysis | Parses token stream to check grammatical structure using CFG. | Parse tree / Syntax tree. |
| 3. Semantic Analysis | Checks meaning (type compatibility, etc.). Uses syntax-directed translation. | Annotated syntax tree / Intermediate code. |
| 4. Intermediate Code Generation | Produces a platform-independent intermediate representation (IR). | IR (e.g., Quadruples). |
| 5. Optimization | Improves IR for speed/size (local & global). | Optimized IR. |
| 6. Code Generation | Maps optimized IR to target machine code. | Target assembly/machine code. |
| 7. Symbol Table Management | 贯穿全程: Stores info about identifiers (name, type, scope, address). | Updated symbol table entries. |
1.3 Bootstrapping
-
Concept: The process of using a simpler/older version of a compiler (or an interpreter) to compile a more advanced version of itself.
-
Process: A minimal compiler (often written in assembly) for language L is used to compile a more feature-rich compiler (written in L). This new compiler can then compile even more advanced versions. It's essential for porting compilers to new architectures.
Exam Tip: Be ready to explain the "bootstrap loader" concept – a tiny program that loads the initial compiler.
2. Lexical Analysis
2.1 Role & Separation from Syntax Analysis
-
Role: Simplifies syntax analysis by removing low-level details (whitespace, comments) and grouping characters into meaningful tokens (keywords, identifiers, operators).
-
Reasons for Separation:
-
Simplicity: Removes tedious, non-semantic details from syntax analyzer.
-
Efficiency: Token recognition can be highly optimized (e.g., using Finite Automata).
-
Portability: Device-dependent I/O and character set handling are isolated.
-
Language Independence: Lexical rules change less often than syntactic rules.
-
2.2 Finite Automata & Regular Expressions
-
Regular Expressions (RE): Concise notation for describing token patterns (e.g.,
id = letter (letter | digit)*). -
Finite Automata (FA): Abstract machines (DFA/NFA) that recognize REs. The lexical analyzer is essentially a DFA simulator.
-
Transition Diagram: Visual representation of FA states and transitions.
-
Transition Table: Tabular representation used in implementation.
-
2.3 LEX Tool
-
Structure: A LEX program has three sections:
-
Definitions: (REs, C code declarations).
-
Rules:
Pattern { Action }(e.g.,letter(letter|digit)* { return ID; }). -
User Code: (C functions,
main()).
-
-
How it Works: LEX generates a C program (
lex.yy.c) containing a DFA-based scanner (yylex()function). This function reads input, matches patterns, and executes associated actions.
Example Rule for Identifier & Arithmetic Operators:
%{
#include "y.tab.h" // Token definitions from parser
%}
%%
[a-zA-Z][a-zA-Z0-9]* { yylval = atoi(yytext); return ID; }
"+" { return PLUS; }
"-" { return MINUS; }
"*" { return MUL; }
"/" { return DIV; }
[ \t\n] ; // Ignore whitespace
. { return yytext[0]; } // Catch-all
%%
3. Syntax Analysis
3.1 Context-Free Grammars (CFGs)
-
Definition: A formal grammar
G = (N, T, P, S)whereN= non-terminals,T= terminals,P= productions,S= start symbol. -
Capabilities: Precisely describes nested, recursive structures (matching parentheses, nested
if-else). -
Limitations: Cannot express context-sensitive constraints (e.g., "variable must be declared before use").
3.2 Parsing Techniques: Top-down vs Bottom-up
| Feature | Top-Down (e.g., LL) | Bottom-Up (e.g., LR) |
|---|---|---|
| Direction | Start symbol → Input string | Input string → Start symbol |
| Process | Leftmost derivation. Predicts production based on FIRST/FOLLOW. | Rightmost derivation (in reverse). Reduces handle (substring matching RHS). |
| Power | Less powerful (cannot handle left-recursion directly). | More powerful (handles left-recursion, wide class of CFGs). |
| Error Recovery | Generally easier. | More complex. |
3.3 Operator Precedence Parsing
-
Principle: Uses precedence relations (
<·,=,·>) between adjacent terminals to find handles. Based on the fact that operators have inherent precedence. -
Steps:
-
Insert
#(end marker) at start/end. -
Scan stack top
aand inputb. -
Apply relation: if
a <· b, shiftb. Ifa ·> b, reduce by someA → αwhereα's RHS ends withaand starts with something·>b.
-
-
Limitation: Only works for operator grammars (no two adjacent non-terminals on RHS of any production).
3.4 Parser Types: LL, SLR, LR, LALR, CLR
| Type | Lookahead | Table Size | Power | Construction |
|---|---|---|---|---|
| LL(k) | k tokens |
Small | Weakest | Predictive, uses FIRST/FOLLOW. |
| SLR(1) | 1 token | Small | Moderate | Uses FOLLOW of LHS for reductions. May have spurious conflicts. |
| LR(1) | 1 token | Very Large | Most powerful | State includes lookahead. Canonical LR(1). |
| LALR(1) | 1 token | Medium | High (near LR(1)) | Merges LR(1) states with identical cores. More efficient than LR(1). |
| CLR(1) | 1 token | Very Large | Most powerful | Synonym for Canonical LR(1). |
3.5 Comparison: SLR vs LALR Parsers
| Aspect | SLR(1) | LALR(1) |
|---|---|---|
| Parsing Table Construction | Uses FOLLOW(LHS) for all reductions. | Uses lookaheads computed from LR(1) items (more precise). |
| State Size | Smaller (no lookahead in state). | Larger (states have lookahead sets). |
| Conflict Handling | May report spurious conflicts (false positives) because FOLLOW is too broad. | Resolves many SLR conflicts by using more precise lookaheads. |
| Power | Subset of LALR(1) grammars. | Superset of SLR(1); subset of LR(1). |
| Example | Grammar S → L = R | R, L → *R | id, R → L is SLR(1) correct but often NOT LALR(1) due to shift-reduce conflict on id. |
3.6 FIRST & FOLLOW Sets: Computation
-
FIRST(X): Set of terminals that can begin any string derived from
X.-
Rules:
-
If
X → aα, adda. -
If
X → ε, addε. -
If
X → Y1Y2...Yk, addFIRST(Y1)(ifε ∉ FIRST(Y1)stop). If allYideriveε, addε.
-
-
-
FOLLOW(X): Set of terminals that can appear immediately to the right of
Xin some sentential form.$(end marker) is inFOLLOW(S).-
Rules:
-
If
A → αXβ, addFIRST(β) - {ε}. -
If
A → αXorA → αXβwhereε ∈ FIRST(β), addFOLLOW(A). -
Repeat until no change.
-
-
3.7 Parsing Table Construction
For SLR(1):
-
Build LR(0) items (productions with
·indicating parser position). -
Construct canonical collection of LR(0) items (states).
-
ACTION table:
-
[Si, a](shift): if[A → α·aβ]in stateiand goto onaleads to statej. -
[rj](reduce byA → α): if[A → α·]in stateianda ∈ FOLLOW(A). -
accept: if[S' → S·]in stateiand input is$. -
error: otherwise.
-
-
GOTO table:
[i, A] = jif goto on non-terminalAfrom stateileads toj.
For CLR(1) / LR(1):
-
Items are
[A → α·β, a]whereais lookahead. -
Closure and goto operations propagate lookaheads.
-
Reduction
[A → α·, a]is placed only ifais in lookahead set. More precise, avoids SLR conflicts.
3.8 Parse Tree Construction & Ambiguity Elimination
-
Ambiguous Grammar: A grammar where a string has multiple parse trees (e.g.,
E → E+E \| E*E \| idfora+b*c). -
Elimination: Use precedence & associativity rules to rewrite grammar or use parser with precedence.
-
Example: For
a+b*c, standard math precedence (*>+) and left associativity. -
Unambiguous Grammar:
E → E + T \| T T → T * F \| F F → ( E ) \| id -
Parse Tree:
E(root) →E + T→T + T→id + T*F→id + id*id.
-
3.9 Checking Grammar for SLR(1) Correctness
-
Build canonical collection of LR(0) items.
-
Construct SLR(1) parsing table.
-
Check for conflicts:
-
Shift-Reduce (S/R): If an entry
ACTION[i, a]would contain bothSiandrj(wherea ∈ FOLLOW(A)for reductionA → α), conflict exists. -
Reduce-Reduce (R/R): If
ACTION[i, a]would containrjandrkfor differentA → αandB → β, conflict exists.
-
-
If no conflicts in any entry → Grammar is SLR(1).
4. Semantic Analysis
4.1 Syntax-Directed Definitions (SDDs)
-
Definition: A CFG augmented with semantic rules/actions attached to productions. Defines attribute values for grammar symbols.
-
Purpose: To evaluate semantic information (type, value, code) during parsing.
4.2 Attribute Types
-
Synthesized Attributes: Values computed from children's attributes in the parse tree. Flow upward.
- Example:
E.valinE → E1 + TwhereE.val = E1.val + T.val.
- Example:
-
Inherited Attributes: Values computed from parent and/or siblings. Flow downward/sideways.
- Example:
L.in(type environment) inL → L1 , idwhereL.in = L1.inandid.type = L1.in.lookup(id.name).
- Example:
4.3 S-attributed vs L-attributed Definitions
| Feature | S-attributed | L-attributed |
|---|---|---|
| Attributes | Only synthesized. | Both synthesized & inherited (but inherited must depend only on parent's inherited & left siblings' synthesized). |
| Evaluation | Can be evaluated in any bottom-up order (e.g., during LR parsing). | Can be evaluated during top-down or bottom-up parsing with restricted dependencies. |
| Example Use | Computing expression values, symbol table insertion. | Type checking in block-structured languages, passing inherited scopes. |
4.4 Dependency Graphs
-
Definition: A directed graph where nodes represent attribute instances in a parse tree. An edge
A → Bmeans value ofAis needed to computeB. -
Construction: For each semantic rule
B = f(A1, A2, ...), add edgesA1 → B,A2 → B, etc. -
Purpose: To check for circular dependencies (ill-formed SDD). Acyclic graph → attributes can be evaluated.
Example: For
S → L = Rwith rulesS.code = L.code || R.code || '='andL.place = R.place, edges:L.code → S.code,R.code → S.code,R.place → L.place.
4.5 Bottom-up Evaluation of Attributes
-
S-attributes: Trivial. When a reduction by
A → αoccurs, computeA's synthesized attributes fromα's attributes (already on stack). PushA's values onto stack. -
L-attributes: More complex. Requires stack to hold inherited attributes of non-terminals. When reducing
A → X Y Z:-
Inherited attributes of
Xare computed beforeXis reduced (using parent's inherited &Y,Z's synthesized). -
After
Xis reduced, its synthesized attributes are available. -
Repeat for
Y, thenZ.
-
4.6 Translation Schemes: Converting L-attributed Grammars
-
Translation Scheme: CFG with embedded semantic actions (in curly braces
{}) within RHS. -
Conversion Rule: Place inherited attribute computations as early as possible (before the non-terminal needing them is parsed).
- Example:
L → {L1.in = L.in;} L1 , idbecomesL → L1 {L1.in = L.in;} , idin a top-down parser.
- Example:
-
Result: Scheme can be parsed top-down (LL) or bottom-up (LR) while evaluating attributes correctly.
4.7 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 conditional branches.
-
exprevaluated once, result saved. -
For each
case, compareexpr.valwithconst.val. If equal, gotostmt.label. -
If no match, goto
default(if exists) or error.
-
-
Backpatching: Used to fill in jump addresses after code generation.
4.8 Backpatching
-
Purpose: To generate unresolved jumps (
goto L) during intermediate code generation and fill in actual addresses later. -
Mechanism:
-
Maintain a list of pending jump locations (quadruple indices) for each boolean expression or control structure.
-
When the target label's address is known, backpatch the list to that address.
-
-
Example: For
if (a == b) goto L1; ... L1: ..., initiallyifgenerates(if a == b goto -)and returns list[-]. Later, whenL1is defined,makelist(nextquad)is used andbackpatch(list, L1.quad)fills-withL1's quad number.
5. Intermediate Code Generation
5.1 Forms of Intermediate Representation
| Form | Structure | Advantages | Disadvantages |
|---|---|---|---|
| Quadruples | (op, arg1, arg2, result) |
Simple, fixed format. Easy to generate & optimize. | Temporary names (t1, t2) clutter. |
| Triples | (op, arg1, arg2) (no result field). References by position. |
No extra temporaries. | Harder to optimize (changing order affects references). |
| Indirect Triples | List of triple references. (op, arg1, arg2) stored separately; instruction list holds pointers. |
Easy to reorder for optimization without changing code. | Extra level of indirection. |
5.2 Generating Intermediate Code for Expressions
-
Example:
d = (a-b) + (a-c) + (a-c) -
Quadruples (using grammar
E → E+T \| T,T → T*F \| F,F → (E) \| id):1: - a b t1 2: - a c t2 3: + t1 t2 t3 4: - a c t4 5: + t3 t4 t5 6: = t5 - d(Note:
(a-c)computed twice → common subexpression for optimization).
6. Symbol Table Management
6.1 Purpose & Operations
-
Purpose: Central repository for identifier information (name, type, scope, address, size, line number).
-
Operations:
-
insert(name, attributes): Add new entry. -
lookup(name): Find entry. -
delete(name)/delete_scope(scope): Remove entries (for scope exit).
-
6.2 Data Structures for Implementation
| Structure | Organization | Lookup/Insert | Pros | Cons |
|---|---|---|---|---|
| Linear List | Unordered / Sorted array. | O(n) linear search. | Simple. | Slow for large tables. |
| Hash Table | Hash function → bucket (list/AVL). | Average O(1), worst O(n). | Fast, efficient. | Collision handling needed. |
| Binary Search Tree (BST) | Ordered by name (lexical). | O(log n) average. | Ordered traversal, dynamic size. | Unbalanced tree → O(n). |
| Self-Organizing List | Move-to-front on access. | O(n) but fast for locality. | Good for temporal locality (repeated use). | Still linear worst-case. |
Exam Tip: Hash tables are most common in practice due to speed. Symbol table often uses multiple scopes (nested hash tables or stack of hash tables).
7. Runtime Storage Organization
7.1 Activation Records (AR) / Stack Frames
-
Structure: (Typically grows downward on stack)
↑ | Actual parameters |
| Return address | | Control link (FP) | | Access link (for non-local vars) | | Local variables | | Temporaries | | ... |
↓ (Stack Pointer SP)
```
-
Components:
-
Local Data: Variables, temporaries.
-
Machine Status: Return address, saved registers.
-
Control Link: Pointer to caller's AR (old FP).
-
Access Link: Pointer to lexical parent's AR (for nested scopes).
-
Parameters: Passed by caller.
-
7.2 Storage Allocation Strategies
| Strategy | Allocation Time | Deallocation Time | Typical Use |
|---|---|---|---|
| Static | Compile time | Program termination | Global variables, code. |
| Dynamic (Stack) | Procedure call | Procedure return | Local variables (no recursion? → stack). |
| Dynamic (Heap) | malloc/new |
free/delete (explicit) |
Dynamic data structures (linked lists, objects). |
7.3 Heap Storage Allocation
-
Strategies:
-
First-fit: Allocate first hole large enough.
-
Best-fit: Allocate smallest hole large enough.
-
Worst-fit: Allocate largest hole.
-
-
Management Issues:
-
Fragmentation: External (holes between allocated blocks), Internal (allocated block larger than requested).
-
Compaction: Moving allocated blocks to gather free space (costly, requires updating pointers).
-
Garbage Collection: Automatic reclamation of unreachable heap objects.
-
8. Code Optimization
8.1 Basic Blocks & Flow Graphs
-
Basic Block: A maximal sequence of consecutive statements with:
-
Single entry (first statement only).
-
Single exit (last statement only, a jump).
-
-
Construction:
-
Identify leaders (first statement, target of jump, statement after jump).
-
Start new block at each leader.
-
Include following statements until next leader.
-
-
Flow Graph: Nodes = basic blocks. Edges = possible control flow between blocks (fall-through & jumps).
8.2 Reducible vs Non-Reducible Flow Graphs
| Property | Reducible Flow Graph | Non-Rducible Flow Graph |
|---|---|---|
| Definition | Can be reduced to a single node by repeatedly removing: <br> a) Self-loop (node → itself). <br> b) Node with single predecessor (merge). | Contains a "loop" that cannot be broken by above rules (e.g., goto into a loop from outside). |
| Parser Relation | Generated by structured programs (well-nested loops, no goto into loops). |
Generated by unstructured programs (arbitrary goto). |
| Optimization | Dominators and loops easily identified. | Difficult to analyze; may require complex algorithms. |
| Example | for, while, if-else structures. |
goto jumping into the middle of a loop. |
8.3 Optimization Techniques
-
Loop Optimization:
-
Code Motion: Move loop-invariant computations (same value each iteration) outside the loop.
- Example:
for(i=0;i<n;i++) x = a*b + c[i];→ movea*bout.
- Example:
-
Induction Variable Elimination: Replace induction variables (change by constant each iteration) with a single variable.
- Example:
iandj = 2*i→ use onlyi, computejas needed or eliminate.
- Example:
-
-
Common Subexpression Elimination (CSE): Compute expression once, reuse result.
- Example:
t1 = a+b; ... t2 = a+b;→ replacet2witht1.
- Example:
-
Variable Propagation (Copy Propagation): Replace uses of a variable with its defined constant or another variable.
- Example:
x = 5; ... y = x+2;→y = 5+2.
- Example:
-
Strength Reduction: Replace expensive operations (
*,/) with cheaper ones (+,-).- Example:
x = i*8;→x = i<<3;or loopi*4→ maintainxand add4each iteration.
- Example:
8.4 Optimization of Basic Blocks (Local)
-
Performed within a single basic block (no control flow).
-
Techniques:
-
Constant Folding: Evaluate constant expressions at compile time (
2+3→5). -
Algebraic Simplifications:
x*1 → x,x+0 → x. -
Dead Code Elimination: Remove statements whose results are never used.
-
CSE & Copy Propagation (as above, within block).
-
8.5 Data Flow Analysis Concepts
-
Goal: Gather global information about how values flow through the program.
-
Key Concepts:
-
Data Flow Equations: Define how information propagates along edges (
IN[n] = union(OUT[p])for predecessorsp). -
Direction: Forward (e.g., reaching definitions) vs Backward (e.g., live variables).
-
Fixed Point Iteration: Repeatedly apply equations until no change (convergence).
-
Meet Operator:
∩(intersection) for must problems,∪(union) for may problems.
-
-
Examples:
-
Reaching Definitions: Set of defs that may reach a point (forward, may).
-
Live Variables: Variables that may be used before next definition (backward, may).
-
Available Expressions: Expressions whose values are already computed and not killed (forward, must). Used for CSE.
-
Exam Tip: Be able to draw a flow graph from code, identify loops (using dominators), and apply local optimizations to a basic block. Know the difference between must and may analysis.