GATE/Compiler Design/Semantic Analysis & Intermediate Code Generation
Medium16 min readCompiler Design

Semantic Analysis & Intermediate Code Generation

Semantic analysis checks meaning (types, scopes, declarations). Intermediate code generation produces an abstract representation that is easier to optimise and translate to machine code.

Key Points

  • ·Semantic analysis: type checking, scope resolution, declaration checking
  • ·Symbol table: maps identifiers to type, scope, memory location; supports nested scopes
  • ·Type checking: static (compile time) vs dynamic (runtime)
  • ·Three-address code (TAC): at most 3 operands per instruction — x = y op z
  • ·Quadruples: (op, arg1, arg2, result) — 4-tuple representation of TAC
  • ·Triples: (op, arg1, arg2) — result referenced by position number (no temp names)
  • ·DAG for expressions: detects common subexpressions; nodes shared for same value
  • ·Syntax-Directed Translation (SDT): attach actions to grammar rules; compute attributes

Semantic Analysis — Meaning Beyond Syntax

Analogy: You can write a grammatically correct sentence like "The colour seven drinks blue" — syntactically valid (subject, verb, object) but semantically NONSENSE. Semantic analysis catches these errors for programs.

Syntactically valid but semantically wrong:
  int x = "hello";       // type error: cannot assign string to int
  y = x + 1;             // undeclared variable y
  int arr[5]; arr[10] = 1; // array out of bounds (runtime)

Symbol Table — The Compiler's Encyclopedia

The symbol table maps: name → attributes

Attributes stored:
  name:     "count"
  type:     int
  scope:    function scope "main"
  address:  stack offset or register
  for function: parameter types, return type

Operations:
  insert(name, attributes)  ← at declaration
  lookup(name)              ← at each use
  delete(scope)             ← when scope exits

Implementation: hash table (O(1) average) or balanced BST

Scoping — Nested Scopes:

int x = 5;           // x in global scope
void foo() {
  float x = 3.14;    // x in foo scope (shadows global x)
  {
    int y = 1;       // y in inner block
    // can use x (float, 3.14) and y here
  }
  // y is gone, x is still float 3.14
}
// x is int 5 again

Implementation: STACK of scopes
  Enter scope  → push new hash table
  Exit scope   → pop hash table
  Lookup       → search from top of stack down

Type Checking

Static type checking: done at COMPILE TIME
  int x; x = 3.14;    // compile error: type mismatch
  Pros: catch errors early, no runtime overhead

Dynamic type checking: done at RUNTIME
  Python, JavaScript — types checked when code runs
  Pros: flexible; Cons: errors only at runtime

Coercion (implicit type conversion):
  int x = 3.14;    // C: float 3.14 → int 3 (truncation)
  double d = 5;    // C: int 5 → double 5.0

Three-Address Code (TAC) — The Universal IR

Each instruction has at most 3 operands (one result, two sources).

Forms:
  x = y op z    (binary operation)
  x = op y      (unary operation)
  x = y         (copy)
  goto L        (unconditional jump)
  if x relop y goto L   (conditional jump)
  x = y[i]      (array read)
  x[i] = y      (array write)
  param x       (push parameter)
  call f, n     (call function with n params)
  return x      (return value)

Example: Translate a = b + c * d - e

t1 = c * d
t2 = b + t1
t3 = t2 - e
a  = t3

Or more compactly:
t1 = c * d
t2 = b + t1
a  = t2 - e

Quadruples vs Triples

Quadruples — explicit temporary names:

(op,    arg1, arg2, result)
(  *,   c,    d,    t1    )
(  +,   b,    t1,   t2    )
(  -,   t2,   e,    a     )

Advantage: easy to reorder instructions during optimisation
           (result name can be changed without updating references)

Triples — no explicit names, use instruction number:

(op,  arg1,  arg2)
0: (*, c,    d   )
1: (+, b,    (0) )  ← arg2 = result of instruction 0
2: (-, (1),  e   )  ← arg1 = result of instruction 1
a = result of instruction 2

Advantage: saves space (no temp names)
Disadvantage: hard to reorder (all references use instruction numbers)

DAG — Detect Common Subexpressions

Expression: a + a * (b - c) + (b - c) * d

Build DAG:
     +
    / \
   +   *
  / \ / \
 a   *   d
    / \
   a  sub
       / \
      b   c

(b-c) node is SHARED — computed only once!
(a) also shared.

DAG detects and eliminates common subexpressions at the expression level.

Syntax-Directed Translation (SDT)

Attach semantic ACTIONS to grammar productions:

Grammar rule        │ Semantic action
────────────────────┼─────────────────────────────────────
E → E₁ + T          │ E.val = E₁.val + T.val
E → T               │ E.val = T.val
T → T₁ * F          │ T.val = T₁.val * F.val
T → F               │ T.val = F.val
F → ( E )           │ F.val = E.val
F → digit           │ F.val = digit.lexval

Synthesised attribute: computed from children (bottom-up)
Inherited attribute:   computed from parent or left siblings (top-down)

S-attributed: only synthesised attributes → evaluate in single bottom-up pass
L-attributed: inherited from left siblings + synthesised → left-to-right pass

Quick Check

Q1. Convert: x = (a + b) * (a + b) - c to TAC. Optimise with common subexpressions.

Naive:
  t1 = a + b
  t2 = a + b    ← same as t1!
  t3 = t1 * t2
  x  = t3 - c

Optimised (CSE):
  t1 = a + b
  t2 = t1 * t1  ← reuse t1 instead of recomputing
  x  = t2 - c

Q2. What is the difference between quadruples and triples? Answer: Quadruples have 4 fields including an explicit result name (temp variable). Triples have 3 fields; result is referenced by instruction index. Quadruples are easier to optimise (reorder without renumbering); triples are more compact.

Q3. What is an L-attributed grammar? Answer: A grammar where attributes can be inherited from parent or from LEFT siblings in a parse tree, plus synthesised attributes. Evaluatable in a single left-to-right pass (during LL parsing or similar). Contrasted with S-attributed (synthesised only, evaluated bottom-up).

Key Formulas

  • TAC form: x = y op z (at most 3 operands/instruction)
  • Quadruple: (operator, arg1, arg2, result) — 4 fields

GATE Exam Tips

  • S-attributed: only synthesised — evaluate in single bottom-up pass (good for LR parsing).
  • L-attributed: synthesised + inherited from left siblings — one left-to-right pass.
  • DAG detects common subexpressions at expression level — computes shared nodes once.
  • GATE: convert expression to TAC or quadruples — always respect operator precedence.

Finished reading this topic?

Mark it complete to track your study progress.