UNIT 4: Compiler Design - High-Impact Short Notes
Based on RGPV CS-603 Past Paper Analysis
I. Introduction and Compiler Structure
Phases of a Compiler (with block diagram):
-
Lexical Analysis: Scans source, produces tokens.
-
Syntax Analysis: Parses tokens into parse tree using CFG.
-
Semantic Analysis: Checks type consistency, annotates parse tree.
-
Intermediate Code Generation: Produces machine-independent IR (e.g., TAC).
-
Optimization: Improves IR for efficiency.
-
Code Generation: Maps IR to target machine code.
-
Symbol Table Management:贯穿 all phases.
-
Error Handling:贯穿 all phases.
Block Diagram Flow:
Source Program → Lexical Analyzer → Syntax Analyzer → Semantic Analyzer → Intermediate Code → Optimizer → Code Generator → Target Code
(Symbol Table & Error Handler interact with all phases)
Compiler vs Interpreter:
| Aspect | Compiler | Interpreter |
|---|---|---|
| Translation | Entire program → object code | Line-by-line execution |
| Memory | Higher (stores object code) | Lower (no separate object code) |
| Speed | Faster execution (after compilation) | Slower (repeated analysis) |
| Error Detection | All errors at compile time | Errors at runtime |
| Portability | Machine-dependent object code | Source portable, interpreter needed |
Frontend-Backend Model:
-
Frontend: Language-dependent (lexical, syntax, semantic analysis). Output: IR (e.g., TAC).
-
Backend: Machine-dependent (optimization, code generation). Input: IR.
-
Advantages: Modularity, retargetability (same frontend for multiple machines), reusability.
-
Disadvantages: Interface complexity, potential inefficiency from abstraction.
Pre-processing and Input Buffering:
-
Pre-processing: Macro expansion, file inclusion, conditional compilation (e.g.,
#include,#definein C). -
Input Buffering: Double buffering (two buffers) to reduce I/O overhead; allows lookahead for token recognition.
Cross-compiler: Compiler running on machine A generates code for machine B (different ISA/OS).
Example: Compiling Android apps on x86 host for ARM target.
II. Lexical Analysis
Role: Read source characters, group into tokens, remove whitespace/comments, handle lexical errors, populate symbol table for identifiers.
Token Specification (using regex):
-
Identifier:
[a-zA-Z_][a-zA-Z0-9_]* -
Keyword:
if,while, etc. (fixed strings) -
Constant:
[0-9]+(integer),[0-9]*\.[0-9]+(float) -
Operator:
+,-,*,/,=, etc. -
Punctuation:
;,,,(,)
Regular Expressions for Tokens:
-
Integer:
[0-9]+ -
Identifier:
[a-zA-Z_][a-zA-Z0-9_]* -
Whitespace:
[ \t\n]+(skipped) -
Comment:
//.*or/*.**/
Finite Automata (DFA/NFA):
-
DFA: Single next state per input; efficient for implementation.
-
NFA: Multiple next states possible; can have ε-transitions.
-
Limitations: Cannot count or match nested structures (e.g., balanced parentheses) → need CFG.
LEX Tool:
-
Specification sections:
-
Definitions: regex macros (e.g.,
DIGIT [0-9]). -
Rules:
pattern { action }(e.g.,{DIGIT}+ { yylval.num = atoi(yytext); return NUMBER; }). -
User Code: C functions (e.g.,
main()).
-
-
Generates lexical analyzer in C.
Error Handling in Lexical Analysis:
-
Panic mode: Skip characters until valid delimiter (e.g.,
;,}). -
Recovery: Insert missing character, delete extraneous character, replace with correct one.
-
Lexical phase errors: Invalid characters, unterminated comments, malformed numbers.
III. Syntax Analysis (Parsing)
Parser Role: Check syntax using CFG, produce parse tree/abstract syntax tree.
Top-down vs Bottom-up Parsing:
| Aspect | Top-down | Bottom-up |
|---|---|---|
| Direction | Start symbol → input | Input → start symbol |
| Strategy | Leftmost derivation | Rightmost derivation (reverse) |
| Examples | Recursive descent, LL(1) | LR(0), SLR, LALR, LR(1) |
| Power | Less (LL(k) grammars) | More (LR(k) grammars) |
| Error Recovery | Easier | Harder |
Context-Free Grammars (CFG):
-
Productions:
A → α(A non-terminal, α string of terminals/non-terminals). -
Derivations:
-
Leftmost: Replace leftmost non-terminal first.
-
Rightmost: Replace rightmost non-terminal first.
-
-
Parse Tree: Graphical representation of derivation.
-
Ambiguity: String has >1 parse tree (e.g.,
if-elsedangling else).Example:
S → if S then S | if S then S else S | a→if a then if a then a else aambiguous.
Left Recursion & Elimination:
-
Immediate left recursion:
A → Aα | β→ eliminate:A → βA' A' → αA' | ε -
Indirect left recursion: Reorder non-terminals to eliminate.
Left Factoring:
-
When multiple productions share common prefix:
A → αβ1 | αβ2→A → αA' A' → β1 | β2
Top-down Parsing:
-
Recursive Descent with Backtracking: Try productions sequentially; may be exponential.
-
Predictive Parsing (LL(1)): Use parsing table (no backtracking).
-
Properties:
-
For
A → α | β,FIRST(α) ∩ FIRST(β) = ∅. -
If
ε ∈ FIRST(α), thenFIRST(α) ∩ FOLLOW(A) = ∅.
-
-
Parsing Table Construction:
-
For
A → α, for eacha ∈ FIRST(α), setM[A,a] = A→α. -
If
ε ∈ FIRST(α), for eachb ∈ FOLLOW(A), setM[A,b] = A→α. -
Else error.
-
-
Bottom-up Parsing (LR Parsing):
-
LR(k): Left-to-right scan, Rightmost derivation (reverse), k lookahead.
-
LR(0) Items: Production with dot
·(e.g.,S → L · = R). -
States: Sets of items (closure + goto).
-
Parsing Tables: ACTION (shift/reduce/accept/error) and GOTO (non-terminal transitions).
Comparison of SLR, LALR, LR:
| Parser | Items | Reduce Action | Power | Table Size |
|---|---|---|---|---|
| SLR(1) | LR(0) items | Use FOLLOW(A) for reduce on A → α |
Weakest | Smallest |
| LALR(1) | LR(0) items merged | Use lookahead from LR(1) items (merged) | Intermediate | Medium |
| LR(1) | LR(1) items | Full lookahead per item | Strongest | Largest |
[!TIP]
- SLR may have spurious conflicts because FOLLOW(A) may contain terminals not in FIRST(α).
- LALR merges states with same core (LR(0) items), losing some lookahead precision.
- LR(1) has largest tables but no false conflicts.
First & Follow Sets:
-
FIRST(X): Set of terminals that begin strings derived from X.
- If
X → ε, include ε.
- If
-
FOLLOW(A): Set of terminals that appear immediately after A in some sentential form. Include
$for start symbol. -
Computation: Standard algorithms (see textbooks).
Error Detection & Recovery:
-
Syntactic errors: Missing/extra tokens, mismatched parentheses.
-
Recovery:
-
Panic mode: Skip tokens until synchronizing token (e.g.,
;,}). -
Phrase level: Insert/delete/replace tokens to continue parsing.
-
Error productions: Add productions for common errors.
-
IV. Semantic Analysis
Role: Check semantic consistency (type, scope), enrich AST with types, attributes, generate intermediate code via SDT.
Attribute Grammars:
-
Attributes: Values associated with grammar symbols.
-
Synthesized: Computed from children (bottom-up). Example:
E.value = E1.value + T.value. -
Inherited: Computed from parent/siblings (top-down). Example: array index type from parent.
-
Differences:
| Synthesized | Inherited |
|---|---|
| Bottom-up evaluation | Top-down evaluation |
| From children | From parent/siblings |
| Used in semantic actions | Used for context-sensitive info |
S-attributed Definitions: Only synthesized attributes. Evaluated in bottom-up order (e.g., LR parsing).
L-attributed Definitions: Inherited attributes from left siblings and parent only. Allows top-down evaluation (e.g., LL parsing).
Example:
S → L = R { L.place = newtemp(); R.place = L.place; }
L → * R { L.place = R.place; }
L → id { L.place = id.place; }
Here L.place is inherited from parent S and left sibling? Actually in S → L = R, L.place is set by S? Wait, better example:
A → B C D
B.inh = A.inh // inherited from parent
C.inh = B.synth // from left sibling
L-attributed: C.inh from left sibling B.synth allowed.
Dependency Graph & Annotated Parse Tree:
-
Dependency graph: Nodes = attribute instances; edges = dependency (if attribute
X.adepends onY.b). -
Annotated parse tree: Parse tree with attribute values filled after evaluation.
-
Evaluation order: Topological sort of dependency graph (must be acyclic for well-defined grammar).
Type Checking:
-
Type expressions: Basic types (
int,float), arrays ([10]int), functions ((int)→float), products ((int, float)). -
Type equivalence:
-
Name equivalence: Same declaration (e.g., two
typedefnames). -
Structural equivalence: Same structure (e.g.,
[10]int≡[10]int).
-
-
Type conversion:
-
Implicit (coercion):
int→floatin assignment. -
Explicit (cast):
(float) i.
-
-
Polymorphic functions: Operate on multiple types (e.g.,
length(list<T>)). -
Overloading: Same name, different parameter types (e.g.,
print(int),print(string)).
V. Intermediate Code Generation
Three-Address Code (TAC):
-
Form:
x = y op z(orx = op y,goto L,if x relop y goto L). -
Forms:
| Quadruples | Triples | Indirect Triples | |----------------------|----------------------|---------------------------| |
(op, arg1, arg2, result)|(op, arg1, arg2)| List of pointers to triples | | Explicit temporaries | No result field; refer by position | Allows easy rearrangement | | Preferred in optimizing compilers (easy renaming, optimization) | Harder to optimize | Used with DAG |
TAC Generation Examples:
-
Expression
a = b + c * d:t1 = c * d a = b + t1 -
Control Structures:
-
if (B) S1 else S2:B.true = ?, B.false = ? code for B if B.false goto L2 code for S1 goto L1 L2: code for S2 L1: -
while (B) S:L1: code for B if B.false goto L2 code for S goto L1 L2:
-
Directed Acyclic Graph (DAG):
-
Nodes: Operations (internal), variables/constants (leaves).
-
Construction Algorithm (for basic block):
-
Initialize empty DAG.
-
For each statement
x = y op zin order:-
Get/create node for
y,z. -
If node
nwith sameopand children exists, usen; else create new node. -
Associate
xwith that node (may have multiple names).
-
-
-
Applications:
-
Common subexpression elimination (shared nodes).
-
Dead code elimination (unreferenced nodes).
-
Efficient code generation (evaluate leaves once, propagate values).
-
-
Example: For
a = b + c; d = b + c;→ single+node with two parentsaandd.
Backpatching:
-
Purpose: Fill addresses of jump instructions for boolean expressions and flow-of-control statements.
-
Mechanism: Maintain lists of unfilled jumps (instruction indices).
-
Boolean Expressions:
-
B → B1 or B2:-
B1.falsemerged withB2.false→B.false. -
B.true = B1.true(afterB1code, backpatchB1.falsetoB2code).
-
-
B → not B1: swap true/false lists.
-
-
Flow-of-control Statements:
-
if B then S1 else S2:-
Generate
Bwithtrue_list,false_list. -
Backpatch
true_listtoS1start. -
Backpatch
false_listtoS2start. -
S.next = merge(S1.next, S2.next).
-
-
while B do S:L1: code for B if B.false goto L2 code for S goto L1 L2:B.truebackpatched toSstart;B.falseto after loop.
-
Flow Graphs & Basic Blocks:
-
Basic Block: Sequence of statements with:
-
Single entry (first statement executed only from start).
-
Single exit (last statement only way to exit).
-
Straight-line code (no jumps inside except at end).
-
-
Construction:
-
Identify leaders: First statement, target of jump, statement after jump.
-
Each leader starts a new block; block ends before next leader.
-
-
Flow Graph: Nodes = basic blocks; edges = possible control transfers (jumps).
-
Characteristics: Used for optimization (data flow analysis), register allocation.
VI. Code Optimization
Goals & Types:
-
Goals: Improve execution speed, reduce memory, power efficiency.
-
Types:
-
Machine-independent: On IR (TAC, DAG) – constant folding, CSE.
-
Machine-dependent: On target code – peephole, register allocation.
-
-
Local vs Global:
-
Local: Within a basic block (e.g., CSE, constant propagation).
-
Global: Across blocks (e.g., dead code elimination, loop optimization).
-
-
Loop Optimization: Most impactful (loops execute repeatedly).
Optimization Techniques (with examples):
| Technique | Description | Example |
|---|---|---|
| Constant Folding | Evaluate constant expressions at compile time. | x = 3 * 4 → x = 12 |
| Constant Propagation | Replace variables with constant values. | a=5; b=a+2 → b=7 |
| Common Subexpression Elimination (CSE) | Reuse previously computed value if same expression. | t1=b*c; t2=b*c → t1=b*c; t2=t1 |
| Copy Propagation | Replace x=y with uses of y instead of x. |
x=y; z=x+1 → z=y+1 |
| Dead Code Elimination | Remove code whose results never used. | x=y; z=5; (if x unused) → remove x=y |
| Loop Invariant Code Motion | Move computations that don’t change in loop outside. | for(i=0;i<n;i++) x=a+b; → t=a+b; for(...) x=t; |
| Peephole Optimization | Examine small window of instructions (e.g., 3-5) for improvements. | LDA R1, x; ADD R1, R1 → redundant → remove |
| Strength Reduction | Replace expensive op with cheaper (e.g., mult by const → shifts/adds). | x = y * 8 → x = y << 3 |
Use of DAG in Optimization:
-
Construct DAG for basic block → eliminate CSE automatically (shared nodes).
-
Generate code from DAG in reverse postorder (evaluate leaves first).
-
Example: For
a=b+c; d=b+c; e=a*d;→ DAG has one+node forb+c, shared byaandd.
Data Flow Analysis (Brief):
-
Computes information at program points (e.g., reaching definitions, live variables).
-
Equations:
IN[n] = ∪ OUT[p]for predecessorsp;OUT[n] = gen[n] ∪ (IN[n] - kill[n]). -
Used for global optimizations (constant propagation, dead code).
VII. Code Generation
Issues:
-
Instruction selection: Choose target instructions for IR.
-
Register allocation: Assign variables to limited registers.
-
Evaluation order: Minimize registers, avoid hazards.
Register Allocation & Assignment:
-
Graph Coloring Algorithm:
-
Build interference graph: nodes = variables, edge if two variables live simultaneously.
-
Color graph with
kcolors (registers). -
If not
k-colorable, spill variable (store in memory) and retry.
-
-
Linear Scan Algorithm:
-
Sort variables by live range start.
-
Allocate registers linearly, freeing when live range ends.
-
Spill when no register free.
- Faster but less optimal than graph coloring.
-
Code Generation from TAC/DAG:
-
Expressions: Generate load/store, arithmetic instructions. Use registers for temporaries.
-
Control Structures: Use backpatching to fill jump addresses.
-
Example: For
if (a<b) x=y; else x=z;if a < b goto L1 x = z goto L2 L1: x = y L2:
Instruction Selection Strategies:
-
Tree rewriting: Match IR tree to machine instruction patterns.
-
Pattern matching: Use templates for common sequences.
VIII. Runtime Environment
Storage Organization:
| Strategy | Description | Merits | Demerits |
|---|---|---|---|
| Static | Global variables at fixed addresses. | Fast access, simple. | No recursion, waste space. |
| Stack | Activation records (AR) for procedures. | Supports recursion, efficient. | No dynamic allocation. |
| Heap | Dynamic allocation (malloc, new). |
Flexible, arbitrary lifetimes. | Fragmentation, slower. |
Activation Record (AR):
-
Model/Format (typical for stack allocation):
[Actual parameters] // from caller [Return address] // where to continue after return [Control link] // pointer to caller's AR (dynamic chain) [Access link] // pointer to enclosing AR (for nested scopes) [Saved registers] // if needed [Local variables] // including temporaries [Temporaries] // intermediate values -
Procedure Call/Return:
-
Call: Push AR, set control/access links, jump.
-
Return: Restore registers, pop AR, jump to return address.
-
Symbol Table Management:
-
Organization:
-
Linear list: Simple, slow lookup (O(n)).
-
Hash table: Fast average lookup (O(1)), handles collisions.
-
Tree structures (BST, B-tree): Ordered, efficient range queries.
-
-
Operations:
-
insert(name, attributes) -
lookup(name)→ attribute entry -
delete(name)(for scopes)
-
-
Scope Handling: Nested scopes → use access links (static chain) or display (array of AR pointers).
IX. Additional & Peripheral Topics
Properties of Optimizing Compilers:
-
Preserve semantics: Output program must be equivalent.
-
Improve performance: Most cases faster/smaller.
-
Efficient compilation: Not too slow.
-
Modular: Separate analysis and transformation.
Tools for Code-Improving Transformations:
-
DAG: For local optimization (CSE).
-
Data flow equations: For global analysis (reaching definitions, live variables).
-
Static Single Assignment (SSA): Simplify data flow analysis.
-
Machine descriptions: For instruction selection (e.g.,
codegeninlcc).
Closure Properties of CFG:
-
CFLs closed under: Union, Concatenation, Kleene star.
-
Not closed under: Intersection, Complement (except with additional constraints).
Theory of Computation (Peripheral):
-
Recursive languages closed under complement (unlike CFLs).
-
Automata equivalence: DFA ≡ NFA (in language recognition power).
Key Formulas & Algorithms
First & Follow (Algorithm Sketch):
-
FIRST(X):-
If
Xis terminal,FIRST(X) = {X}. -
If
X → ε, add ε. -
If
X → Y1...Yk, addFIRST(Y1)(excluding ε); if allYiderive ε, add ε.
-
-
FOLLOW(A):-
If
Sstart symbol, add$toFOLLOW(S). -
For
B → αAβ, addFIRST(β) \ {ε}toFOLLOW(A). -
If
β ⇒* ε, addFOLLOW(B)toFOLLOW(A).
-
DAG Construction Algorithm:
for each statement x = y op z in order:
node_y = get_node(y) // create if not exists
node_z = get_node(z)
if node n with op and children (node_y, node_z) exists:
x_node = n
else:
x_node = new node(op, node_y, node_z)
associate x with x_node
Backpatching for if B then S1 else S2:
B.code with true_list, false_list
backpatch(B.true_list, S1.instr)
backpatch(B.false_list, S2.instr)
S.next = merge(S1.next, S2.next)
Loop Invariant Code Motion:
-
Identify statements in loop where all definitions are loop-invariant (no definition changes within loop).
-
Move such statements before loop (ensure no side effects, and moved code executed only once).
[!EXAM TIPS]
- DAG vs TAC: DAG eliminates CSE automatically; TAC is linear.
- SLR vs LALR: SLR uses FOLLOW for reductions → may have conflicts; LALR uses precise lookahead → fewer conflicts.
- S vs L-attributed: L-attributed allows inherited from left siblings → more flexible for top-down.
- Backpatching: Always maintain lists; patch after generating code for substatements.
- Register Allocation: Graph coloring optimal but expensive; linear scan faster for JITs.
- Activation Record: Access link for non-local variables in nested procedures; control link for dynamic chain.
- Type Checking: Structural equivalence more flexible than name equivalence.
- Constant Folding vs Propagation: Folding evaluates expressions; propagation replaces variables with constants.
- Peephole: Look for redundant loads/stores, jumps to jumps, algebraic simplifications.
- LEX: Patterns are regex; actions are C code;
yytextholds matched string.