IT-603(C) Embedded Systems - Unit 1: Compiler Design (Based on Past Exam Analysis)
I. Introduction & Compiler Structure
A compiler is a program that translates source code written in a high-level language into an equivalent target language (usually machine code).
Phases of a Compiler (with Role):
| Phase | Primary Role | Key Output |
|---|---|---|
| 1. Lexical Analysis | Reads input stream, groups characters into tokens (lexemes). | Stream of tokens. |
| 2. Syntax Analysis | Organizes tokens into a hierarchical structure (parse tree/abstract syntax tree) based on grammar rules. | Parse tree / AST. |
| 3. Semantic Analysis | Checks for semantic consistency (type checking), gathers type information. | Annotated AST / intermediate representation. |
| 4. Intermediate Code Generation | Produces a machine-independent, low-level representation (e.g., quadruples). | Intermediate code. |
| 5. Code Optimization | Transforms intermediate code for efficiency (speed, size). | Optimized intermediate code. |
| 6. Code Generation | Maps optimized intermediate code to target machine instructions. | Target machine code. |
| 7. Symbol Table | Central repository accessed by all phases to store and retrieve identifier info (name, type, scope, address). | Symbol table entries. |
Bootstrapping: The process of using a simple, limited version of a compiler (written in another language) to compile a more sophisticated version of itself, eventually leading to a full compiler for its own source language. Purpose: To create a self-hosting compiler without needing an existing compiler for the new language.
II. Lexical Analysis
Reasons for Separating Lexical Analysis from Syntax Analysis:
-
Simplicity: Removes low-level character-level details (whitespace, comments) from syntax analysis.
-
Efficiency: Lexical analyzer can be optimized (e.g., using finite automata) for fast token recognition.
-
Portability: Device-dependent I/O and character encoding handling is isolated.
-
Modularity: Clear interface (token stream) between phases.
Role of Finite Automata (FA):
-
Deterministic Finite Automaton (DFA) is constructed from regular expressions (patterns for tokens).
-
DFA acts as the recognizer: it reads the input character-by-character and accepts/rejects strings based on whether they match a token pattern.
-
The DFA's accepting states correspond to token types.
LEX Tool: Structure and Working
A Lex program has three sections:
%{
/* C declarations (global vars, functions) */
%}
/* Regular expressions (patterns) */
%%
pattern1 { action1 C code }
pattern2 { action2 C code }
...
%%
/* Additional C code (main, helper functions) */
Working: Lex reads the specifications, generates a lex.yy.c file containing a yylex() function. This function implements a DFA derived from the regex patterns. When yylex() is called, it scans input, matches the longest possible prefix to a pattern, and executes the corresponding action (usually returning a token).
Lexical Analysis Example: Identifiers & Arithmetic Operators
%{
#include "y.tab.h" /* Token definitions from parser */
%}
%%
"+" { return PLUS; }
"-" { return MINUS; }
"*" { return MUL; }
"/" { return DIV; }
"=" { return ASSIGN; }
[a-zA-Z][a-zA-Z0-9]* { yylval.id = strdup(yytext); return ID; }
[ \t\n] { /* ignore whitespace */ }
. { return INVALID; } /* any other char */
%%
- For input
x = a + b * c, tokens returned:ID('x'),ASSIGN,ID('a'),PLUS,ID('b'),MUL,ID('c').
III. Syntax Analysis (Parsing)
Top-down vs. Bottom-up Parsing (Overview):
| Feature | Top-down (e.g., Recursive Descent, LL) | Bottom-up (e.g., LR, LALR, SLR) |
|---|---|---|
| Construction | Starts from start symbol, applies productions to derive string. | Starts from input string, applies reductions to reach start symbol. |
| Direction | Leftmost derivation (usually). | Rightmost derivation in reverse. |
| Error Detection | Early, but may have infinite loops on left-recursion. | Late, but more powerful (handles left-recursion). |
| Power | LL(k) grammars (subset of CFG). | LR(k) grammars (superset of LL). |
Operator Precedence Parsing:
-
Concept: Uses precedence relations (
<·,=·,·>) between operators to guide parsing without a full parse tree. -
Algorithm: Constructs a precedence parsing table from grammar (after removing non-operator symbols). Parses by shifting tokens onto a stack and using relations to decide when to reduce.
-
Limitation: Only works for operator-precedence grammars (no two non-terminals adjacent in any production).
SLR(1) Parsing:
-
Uses LR(0) items to build canonical collection of LR(0) states.
-
Parsing Table Construction:
-
Build LR(0) automaton (states as sets of items).
-
ACTION Table:
-
shifton[A → α·aβ]for terminala. -
reduce A → αon[A → α·]if·is at end, only ifFOLLOW(A)does not contain any terminal that also causes a shift-reduce conflict. -
accepton[S' → S·]. -
errorotherwise.
-
-
GOTO Table:
goto[state, non-terminal] = next_state.
-
-
Checking SLR(1) Correctness: Grammar is SLR(1) if no reduce-shift or reduce-reduce conflicts exist in the ACTION table after applying the FOLLOW set condition.
LALR Parsers:
-
Comparison with SLR:
| Feature | SLR | LALR | | :--- | :--- | :--- | | State Construction | Uses FOLLOW sets of LHS non-terminal for reductions in all states. | Merges LR(0) states with identical cores (items without lookahead). Uses lookaheads propagated during merge. | | Power | Less powerful (more likely to have conflicts). | More powerful than SLR, but less than full LR(1). | | Table Size | Smaller. | Larger than SLR, smaller than LR(1). |
CLR(1)/LR(1) Parsing:
-
Uses LR(1) items:
[A → α·β, a]whereais lookahead. -
Construction:
-
Build canonical collection of LR(1) items (closure and goto operations consider lookaheads).
-
ACTION Table:
reduce A → αonly on[A → α·, a]for lookaheada. -
GOTO Table: Same as SLR but on LR(1) states.
-
-
Result: Largest class of grammars parsable by deterministic shift-reduce parser (all deterministic CFLs).
Parse Tree Construction for Ambiguous Grammars:
-
Ambiguous grammar (e.g.,
E → E+E | E*E | id) allows multiple parse trees fora+b*c. -
Elimination: Use precedence and associativity rules to rewrite grammar unambiguously.
E → E + T | T T → T * F | F F → (E) | idNow,
a+b*chas a unique parse tree reflecting*higher precedence than+.
Context-Free Grammars (CFG):
-
Capabilities: Describe nested, hierarchical structures (matching parentheses, if-then-else, loops). Express recursion.
-
Limitations: Cannot express context-sensitive constraints (e.g., variable must be declared before use, type compatibility). Cannot count (e.g., equal number of a's and b's).
FIRST and FOLLOW Sets:
-
FIRST(α) for string α: Set of terminals that can begin any string derived from α. If α ⇒* ε, include ε.
-
FOLLOW(A) for non-terminal A: Set of terminals that can appear immediately to the right of A in some sentential form. If A is at end, include
$(end-of-input). -
Step-by-Step Computation:
-
FIRST:
-
If
Xis terminal,FIRST(X) = {X}. -
If
X → εis production, addεtoFIRST(X). -
For
X → Y1 Y2 ... Yk:-
Add
FIRST(Y1) \ {ε}toFIRST(X). -
If
ε ∈ FIRST(Y1), addFIRST(Y2) \ {ε}, and so on. -
If
εin allFIRST(Yi), addεtoFIRST(X).
-
-
-
FOLLOW:
-
Add
$toFOLLOW(S)whereSis start symbol. -
For production
A → α B β:- Add
FIRST(β) \ {ε}toFOLLOW(B).
- Add
-
For production
A → α BorA → α B βwhereε ∈ FIRST(β):- Add
FOLLOW(A)toFOLLOW(B).
- Add
-
-
IV. Syntax-Directed Translation & Semantic Analysis
Attribute Grammars:
-
Annotate grammar with attributes (values associated with grammar symbols).
-
S-attributed Definitions:
-
Attributes are synthesized only (computed from children's attributes).
-
Bottom-up evaluation: Attributes computed during reductions in a bottom-up parser.
-
Example: Evaluating arithmetic expressions:
E → E1 + T { E.val = E1.val + T.val } E → T { E.val = T.val } T → int { T.val = int.lexval }
-
-
L-attributed Definitions:
-
Attributes can be synthesized or inherited.
-
Inherited attributes are computed from parent and left siblings (not right siblings).
-
Allows top-down flow of information (e.g., symbol table scope).
-
Conversion to Translation Scheme: Embed semantic actions within productions at positions respecting L-attributed constraints (actions before/after symbols).
S → { push new scope } A { pop scope } A → D L L → ; D L | ε
-
-
Differentiation:
| Feature | S-attributed | L-attributed | | :--- | :--- | :--- | | Attribute Types | Only synthesized. | Synthesized + Inherited (from left siblings/parent). | | Evaluation Order | Pure bottom-up (post-order). | Can be evaluated in both top-down and bottom-up passes. | | Use Case | Simple expression evaluation. | Context-sensitive info (e.g., scope, type checking). |
Translation Schemes:
-
Grammar with embedded semantic actions (in
{ }). -
For Case Statements:
stmt → case expr { gen('t1', '=', expr.place) } of case_list end case_list → case_list ; case_item | case_item case_item → const_list : stmt { gen('goto', '-') }Actions generate code for expression evaluation and conditional jumps.
Dependency Graphs:
-
Directed graph representing dependencies between attribute values.
-
Nodes: Attribute instances.
-
Edges:
X.i → Y.jif value ofY.jdepends onX.i. -
Construction: For each semantic rule
X.a = f(Y1.b1, ..., Yk.bk), add edgesY1.b1 → X.a, ...,Yk.bk → X.a. -
Evaluation: Topological sort of graph gives order for attribute evaluation. Cycles indicate circular definitions (semantic error).
Backpatching:
-
Concept: Technique to handle forward jumps (unresolved addresses) in code generation for boolean expressions and control flow.
-
Use: Generate code with temporary labels (placeholders). Maintain lists of incomplete jumps (list of instruction addresses needing a target). When target label is known, backpatch the list with the actual address.
-
Example: For
if (a < b) goto L1; ... L1: ..., generateif a < b goto -and add this instruction's address to a list. WhenL1's address is known, fill in all addresses in the list.
V. Intermediate Code Generation
Forms of Intermediate Code:
| Form | Structure | Advantages | Disadvantages |
|---|---|---|---|
| Quadruples | 4-tuple: (op, arg1, arg2, result) |
Easy to generate, optimize (no temporary renaming). | Uses temporary names; occupies more space. |
| Triples | 3-tuple: (op, arg1, arg2); result implied by position. |
No extra temporaries; saves space. | Hard to optimize (address changes affect all references). |
| Indirect Triples | Triple table + instruction pointer list. | Easy optimization (reorder list without changing triples). | Extra level of indirection. |
Generation Example: d = (a-b) + (a-c) + (a-c)
-
Quadruples:
1: (-, a, b, t1) 2: (-, a, c, t2) 3: (+, t1, t2, t3) 4: (-, a, c, t4) // Common subexpression reused? 5: (+, t3, t4, t5) 6: (=, t5, -, d) -
Triples:
1: (-, a, b) 2: (-, a, c) 3: (+, 1, 2) 4: (-, a, c) // Same as 2, but new triple 5: (+, 3, 4) 6: (=, 5, d) -
Indirect Triples:
Triple Table: 1: (-, a, b) 2: (-, a, c) 3: (+, 1, 2) 4: (-, a, c) // Duplicate 5: (+, 3, 4) 6: (=, 5, d) Pointer List: [1, 2, 3, 4, 5, 6] // Can reorder for optimization
VI. Symbol Tables
Purpose:
-
Store information about identifiers (variables, functions, constants).
-
Support insertion, lookup, and scope management.
-
Used by all phases (lexical, syntax, semantic, code generation).
Data Structures for Implementation:
| Structure | Description | Pros | Cons |
|---|---|---|---|
| Linear List | Array/List of entries. | Simple, fast for small tables. | O(n) lookup/insertion (inefficient for large programs). |
| Binary Search Tree (BST) | Ordered tree (e.g., by name). | O(log n) average lookup/insertion. | No worst-case guarantee; unbalanced tree degrades to O(n). |
| Hash Table | Hash function maps name → bucket (linked list/AVL tree). | O(1) average lookup/insertion. | Hash collisions; need good hash function & collision resolution. |
| Lexical-Level Tree | Tree where each node represents a scope (block). | Efficient scope handling (nested blocks). | More complex; lookup may traverse up tree. |
Common Choice: Hash Table with separate chaining is most common due to average constant-time performance.
VII. Runtime Environment & Storage Management
Static vs. Dynamic Storage Allocation:
| Feature | Static Allocation | Dynamic Allocation |
|---|---|---|
| When | Compile-time. | Run-time. |
| Memory | Fixed locations (data/bss segments). | Heap (malloc/free) or stack (activation records). |
| Size | Must be known at compile time. | Can grow/shrink at runtime. |
| Access | Direct (fast). | Indirect (via pointers). |
| Use Case | Global/static variables. | Local variables (stack), dynamic objects (heap). |
Heap Storage Allocation Strategy:
-
Manages dynamic memory (e.g.,
mallocin C,newin C++). -
Algorithms:
-
First-fit: Allocate first block of sufficient size.
-
Best-fit: Allocate smallest sufficient block (reduces fragmentation).
-
Worst-fit: Allocate largest block (tries to leave large free blocks).
-
-
Fragmentation:
-
External: Free memory exists but is non-contiguous.
-
Internal: Allocated block is larger than requested (wasted space inside).
-
-
Compaction: Moving allocated blocks to create larger free space (costly, requires updating pointers).
Activation Record (AR) / Stack Frame:
Structure pushed onto call stack for each function call:
|------------------------| ↑ Higher Addresses
| Actual Parameters | (if any, pushed by caller)
|------------------------|
| Return Address | (saved PC)
|------------------------|
| Dynamic Link (FP) | (pointer to caller's AR)
|------------------------| ← Frame Pointer (FP)
| Local Variables | (including temporaries)
|------------------------|
| Saved Registers | (if needed)
|------------------------|
| ... |
|------------------------| ↓ Lower Addresses (Stack Pointer SP)
VIII. Code Optimization
Basic Blocks:
-
Definition: A sequence of statements with:
-
Single entry (first statement executed only if control enters here).
-
Single exit (last statement causes all control to leave).
-
-
Construction:
-
Identify leaders (first statement, target of jump, statement after jump).
-
Each leader starts a new basic block.
-
Block includes all statements up to (but not including) next leader.
-
-
Example:
if (a<b) x=0; else x=1; y=x+1;-
Leaders: 1 (
if), 2 (x=0), 4 (x=1), 5 (y=x+1). -
Blocks:
B1: 1,B2: 2,B3: 4,B4: 5.
-
Optimization of Basic Blocks:
-
Constant Folding: Evaluate constant expressions at compile time.
-
Constant Propagation: Replace variables with known constant values.
-
Common Subexpression Elimination (CSE): Reuse computed value if same expression reappears.
-
Dead Code Elimination: Remove statements whose results are never used.
-
Algebraic Simplifications:
x*1 → x,x+0 → x.
Flow Graphs:
-
Nodes = Basic Blocks.
-
Edges = Possible control flow between blocks (fall-through and jumps).
-
Entry node = block containing first statement.
Reducible vs. Non-Reducible Flow Graphs:
| Property | Reducible Flow Graph | Non-Reducible Flow Graph |
|---|---|---|
| Definition | Can be reduced to a single node by repeatedly removing edges from loops (back edges in depth-first spanning tree). | Contains irreducible loops (multiple entry points). |
| Construction | Formed by structured programming (well-nested loops, no goto into loops). |
Contains arbitrary goto (especially into loops). |
| Analysis | Allows efficient data-flow analysis (e.g., reaching definitions). | Complicates data-flow analysis; may require iterative methods. |
| Example | for, while, if-else (properly nested). |
goto jumping into middle of a loop from outside. |
Optimization Techniques (with Example):
| Technique | Concept | Example |
|---|---|---|
| Common Subexpression Elimination | Compute expression once, reuse result. | t1 = a*b; ...; t2 = a*b; → t1 = a*b; ...; t2 = t1; |
| Code Motion (Loop-Invariant) | Move computation out of loop if result same in all iterations. | for(i) { x = y*z; a[i] = x+1; } → x = y*z; for(i) { a[i] = x+1; } |
| Variable Propagation (Copy) | Replace variable with its assigned value. | x = y; ...; z = x+1; → z = y+1; (after x redef.) |
| Strength Reduction | Replace expensive op with cheaper one (e.g., mult by constant → shift/add). | x = i*8; → x = i << 3; |
| Loop Optimization | Apply above techniques within loops; unrolling, fusion, fission. | Unroll loop: for(i=0;i<4;i++) a[i]=0; → a[0]=0; a[1]=0; a[2]=0; a[3]=0; |
Exam Tip: For basic block optimization, always draw the block, apply transformations step-by-step, and show the final optimized block.