Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ৮/১৬অ্যাডভান্সড১ ঘণ্টা

Intermediate Representation — TAC, SSA, আর কেন একটা মাঝের ভাষা দরকার

Intermediate Representation

Compiler-এর মূল ভাষায় প্রবেশ — tree-কে linear three-address code-এ flatten, SSA-র গণিত, আর নিজের হাতে প্রথম lowering।

এই লেসন শেষে আপনি পারবেন

  • Three-address code define করে AST-কে flat instruction sequence-এ lower করতে পারবেন — temporaries আর labels সহ
  • SSA form-এর দুই invariant (single assignment, φ at joins) ব্যাখ্যা করে একটা branching program-কে হাতে SSA-তে convert করতে পারবেন
  • Dominance আর dominance frontier-এর intuition দিয়ে বলতে পারবেন φ-node ঠিক কোন block-গুলোতে বসে
  • -O0 আর -O2 LLVM IR-এর diff পড়ে বলতে পারবেন alloca/load/store buffering কীভাবে mem2reg দিয়ে registers-এ promote হয়
  • LLVM IR, JVM bytecode, WASM — তিনটা 'মাঝের ভাষা'-র design positioning আলাদা করে বলতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

Frontend শেষ। Tree validated — names bound, types checked। কিন্তু এখনো একটা মৌলিক অমিল বাকি:

Tree nonlinear। CPU linear।

BinOp node চারদিকে pointer ছড়ায়; CPU instruction একের পর এক, memory-তে শুয়ে থাকে। এই gap পার হওয়ার মধ্যেই compiler-এর অর্ধেক কঠিন কাজ লুকানো — আর পার হওয়ার আগে আমাদের একটা মাঝের ভাষা লাগবে: tree-এর flexibility, machine-এর linearity — দুটোর একটাও না, দুটোর সুবিধা।

প্রথম লেসনে M×N problem দেখেছিলাম — IR ছাড়া প্রতি ভাষা-CPU pair- এ full compiler। আজ সেই IR-এর ভেতরটা খুলব: সবচেয়ে পুরনো আর সবচেয়ে শেখার-উপযোগী রূপ three-address code, তারপর modern optimizer- দের ভাষা SSA, তারপর real world-এর তিনটা IR positioning।

আর build-এ ঐতিহাসিক মুহূর্ত: আমাদের ToyLang AST প্রথমবার machine-like form-এ lower হবে। Compiler-এর frontend-middle সেতুবন্ধ।

আজকের প্রশ্ন: tree-কে flat করার সময় control flow (if/while) কোথায় যায়? আর optimizer-রা কেন সবাই ১৯৯০-এর দশকে SSA-তে জমা হয়ে গেল?

মূল ধারণা

Three-address code — সরলতার চরম

TAC-এর নিয়ম এক লাইনে: প্রতি instruction-এ সর্বোচ্চ এক operator:

source:      d = a * b + c - e;

tree:            (-)
                /   \
              (+)    e
             /   \
           (*)    c
          /   \
         a     b

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

Tree-র nested structure ভেঙে গেল linear chain-এ — আর প্রতিটা intermediate value-এর নাম দরকার হলো: temporary (t1, t2...)। Temporary = compiler-এর virtual register — এখনো CPU register না, কিন্তু একদিন হবে (register allocation, backend-এর কাজ)।

Instruction family:

Formউদাহরণকাজ
Binaryt1 = x + yarithmetic/comparison
Unaryt2 = -xnegation
Copyx = t3assignment
Unconditional jumpgoto L2control
Conditional jumpif t1 \< 0 goto L2branch
LabelL2:target marker
Call/printprint t4side-effect

Quads — TAC-এর mechanical encoding

Implementation-এ প্রতিটা instruction একটা fixed ৪-field record:

(operator, arg1, arg2, result)

("add",  "a", "b", "t1")
("mul",  "c", "d", "t2")
("sub",  "t1", "t2", "t3")
("copy", "t3", "",   "x")     ← unary/copy-তে arg2 খালি

Fixed width = array of structs = fast iteration, easy indexing — 1970s-এর IBM FORTRAN H compiler থেকে আজকের GCC GIMPLE পর্যন্ত এই পরিবারই।

Basic blocks আর CFG

Jump ঢুকলেই program আর sequential না। Structure ফিরতে হয়:

  • Basic block: একটা straight-line instruction run — শুরুতে ঠিক এক entry, শেষে ঠিক এক exit (branch বা fall-through)
  • Control Flow Graph (CFG): block = node, possible jumps = edges
        B0: entry
       /    \
   true      false
     ▼        ▼
   B1: then  B2: else
       \      /
        ▼    ▼
        B3: join

Optimization = CFG-এর উপর graph algorithms। এই module-এর পরের অংশের arena এটাই।

SSA — প্রতিটা নাম একবারই লেখা

Problem: TAC-এ x = ...; ...; x = ... — কোন x-এর version কোন use-এ গেল? Dataflow analysis করতে হয় reaching-definition হিসাব — ব্যয়বহুল।

Static Single Assignment-এর চুক্তি: প্রতিটা variable ঠিক একবার assign হয়। Reassignment মানে নতুন suffix:

TAC:                    SSA:
x = 1                   x1 = 1
y = x + 1               y1 = x1 + 1
x = 2                   x2 = 2
z = x + y               z1 = x2 + y1

Def-use chain এখন syntax-এই visible: x1 দেখলেই তার সব use textually traceable — analysis sparse, cheap, local।

কিন্তু একটা গোলমাল জায়গা: join point।

if (cond) { x = 1; }        both branches assign x
else      { x = 2; }        → which x survives after?
print(x);

SSA:
  if cond goto L1
  x1 = 1
  goto L2
L1:
  x2 = 2
L2:
  x3 = φ(x1, x2)      ← φ: "edge-dependent choice"
  print(x3)

φ-node (phi): x3 = φ(x1, x2) মানে — L2-তে পৌঁছানো path অনুযায়ী x3 হয় x1 নয় x2। Runtime-এ কোনো φ machine instruction নেই — এটা notation; backend পরে parallel-copy বা interference-aware trick-এ resolve করে।

ভেতরে কী ঘটছে

Dominance — φ কোথায় বসে তার গণিত

φ placement naive rule (“প্রতিটা join-এ”) ভুল দেয় — extra φ গুলো analysis slow করে। সঠিক theory:

  • A dominates B (A dom B): A থেকে entry-র প্রতিটা path-এ B-র আগে A পার হতে হয়। Entry সবকিছু dominate করে।
  • Dominance frontier of X: X-এর dominance ঠিক যেসব node-এ “ফুরিয়ে” যায় — সেসব node X-এ পৌঁছানোর path আছে কিন্তু প্রতিটা path X-দিয়ে যায় না।

φ-placement theorem (Cytron et al.): variable v-এর definition X থাকলে, v-এর φ-node লাগবে ঠিক DF(X)-এর প্রতিটা node-এ — এবং iterative: নতুন φ-ও definition, তার DF-ও process হয়।

One-screen picture:

        entry
          │
        ┌─B1─┐        B1 dominates B2, B3 (সব path দিয়ে যায়)
        ▼    ▼
       B2    B3       DF(B2) = {B4}  ← B4-এ পৌঁছাতে B2 হয়তো
        ▼    ▼           দরকার হয় না (B3-route আছে)
        └──B4──┘
           ▲
      φ here! (যদি B2/B3-এ same-var def থাকে)

Practical algorithm: dominance tree compute (Lengauer-Tarjan, near-linear), তারপর φ-placement by DF walk। Production compilers এটাই করে; toy scale-এ naive per-variable walk enough।

-O0-এর অদ্ভুত alloca/load/store — একটা mystery solved

Clang -O0-এর LLVM IR দেখলে অদ্ভুত প্যাটার্ন — প্রতিটা variable memory-তে!

%x = alloca i32          ; stack slot খোলা
store i32 %val, ptr %x   ; লেখা = memory write
%v = load i32, ptr %x    ; পড়া = memory read

কেন এত বোকা কোড? কারণ SSA construction সহজ করার trick — variable mutable থাকলে single-assignment violation হয়; তাই -O0 প্রতিটা variable-কে একটা immutable memory cell বানায় (alloca slot একবারই allocate!), সব read/write সেখানে। Memory address immutable, content mutable।

তারপর mem2reg pass: যে allocas শুধু load/store-এ ব্যবহৃত (promotable), সেগুলো registers-এ promote করে — ঠিক সেখানেই φ-node insert করে। অর্থাৎ LLVM-এ SSA construction দুই-phase: dumb memory style → promotion pass। Engineering lesson: hard problem (φ placement) কে two easy passes-এ ভাঙা।

Arrays এর ব্যতিক্রম — element address runtime-computed, তাই promote হয় না; getelementptr (gep) instruction address arithmetic করে। Pointer-বিশিষ্ট ভাষার IR-এর এই অংশ assembly module-এর addressing-and-pointers লেসনের সাথে মিলে যায়।

Real-world IR positioning — তিনটা ভিন্ন answer

LLVM IRJVM bytecodeWASM
Naturetyped SSA text/bitcodestack-based, verifiedsandboxed binary
Targetstatic compilationportable runtimebrowser/embedded VM
Optimizationaggressive (opt passes)JIT-এর ভেতরেlimited (fast validate)
Stabilityinternal-ish (version churn)spec-frozen decadesspec-frozen

Positioning-ই design explain করে: JVM bytecode stack-based কারণ compact + verification simple; LLVM IR SSA-typed কারণ optimizer-first; WASM validated-sandbox কারণ hostile environment। Bitcode = LLVM IR-এর serialized container format — deployment artifact হিসেবে পাঠানো IR (Apple App Store historically)।

উদাহরণ

একটা C function — -O0 IR annotated

int clamp(int v, int lo, int hi) {
    if (v \< lo) return lo;
    if (v > hi) return hi;
    return v;
}
clang -S -emit-llvm -O0 clamp.c -o clamp_O0.ll

Annotated core (names simplified):

define i32 @clamp(i32 noundef %v, i32 noundef %lo, i32 noundef %hi) {
entry:
  %v.addr = alloca i32          ; param buffering begins
  %lo.addr = alloca i32
  %hi.addr = alloca i32
  store i32 %v, ptr %v.addr     ; params → slots
  store i32 %lo, ptr %lo.addr
  store i32 %hi, ptr %hi.addr
  %0 = load i32, ptr %v.addr    ; every access = load/store
  %1 = load i32, ptr %lo.addr
  %cmp = icmp slt i32 %0, %1    ; signed less-than → i1
  br i1 %cmp, label %if.then, label %if.end

if.then:
  %2 = load i32, ptr %lo.addr
  ret i32 %2                    ; early return = ret in block

if.end:                          ; second if continues here
  ...
return:
  %4 = load i32, ptr %v.addr
  ret i32 %4
}

চেনা চেনা সব জিনিস: alloca-buffering (উপরে ব্যাখ্যা করা), block labels (আমাদের build-এর labels-এর production cousin), icmp+br (x86-64-এর cmp+jcc-এর ভাষা-নিরপেক্ষ রূপ — reading- compiler-output লেসনের mapping skills কাজে লাগবে)।

এবার -O2:

define i32 @clamp(i32 noundef %v, i32 noundef %lo, i32 noundef %hi) {
entry:
  %cmp = icmp slt i32 %v, %lo
  %spec.select = select i1 %cmp, i32 %lo, i32 %v
  %cmp4 = icmp sgt i32 %spec.select, %hi
  %retval = select i1 %cmp4, i32 %hi, i32 %spec.select
  ret i32 %retval
}

Branches গায়েব — দুই select instruction-এ পরিণত (branchless idiom)। Allocas gone (mem2reg), loads/stores gone (forwarding), control-flow → dataflow। তিনটা optimization pass-এর collaborative ফল — আর এই সব pass-এর কাজের ভাষাটাই আমরা এখন পড়তে শিখছি।

ছবিতে

If/while lowering shape — যেটা build-এ implement করব

if (C) S1 else S2:              while (C) S:

  eval C → t                      L_start:
  if_false t → L_else               eval C → t
  ── S1 ──                          if_false t → L_end
  goto L_end                        ── S ──
L_else:                             goto L_start
  ── S2 ──                        L_end:
L_end:

common skeleton: [cond-code] [then-jump] [body] [back-edge/target]
Statement-এর control structure সরাসরি label+jump pattern-এ অনুবাদ হয়। এই pattern গুলোই code generation-এর alphabet।

TAC বনাম SSA — same program, দুই dialect

TAC (mutable vars):           SSA (single assignment):

L0:                           L0:
  t1 = i \< 10                   t1 = i1 \< 10
  if_false t1 → L3              if_false t1 → L3
  t2 = sum + i                  t2 = sum1 + i1
  sum = t2                      sum2 = t2
  t3 = i + 1                    t3 = i1 + 1
  i = t3                        i2 = t3
  goto L0                       goto L0
L3:                           L3:
  print sum                     sum3 = φ(sum1, sum2)
                                i3    = φ(i1, i2)
                                print sum3

L3-এ φ লাগল কেন? Loop-back edge — sum1 (initial) নাকি sum2 (updated) path-dependent। Optimizer এখন sum3-এর দুই incoming-ই আলাদা চিনতে পারে — induction variable analysis-র raw material।

নিজে চালিয়ে দেখুন

EXPERIMENT

তিনটা C function — -O0 বনাম -O2 LLVM IR diff

Linux / macOS (clang required; opt/llvm-as optional)· ২৫ মিনিট

তিনটা function — scalar math, loop, branch mix:

/* trio.c */
int square_sum(int a, int b) {
    return a*a + b*b;
}

int countdown(int n) {
    int total = 0;
    while (n > 0) {
        total += n;
        n -= 1;
    }
    return total;
}

int classify(int x) {
    if (x \< 0)  return -1;
    if (x == 0) return 0;
    return 1;
}
clang -S -emit-llvm -O0 trio.c -o trio_O0.ll
clang -S -emit-llvm -O2 trio.c -o trio_O2.ll

# quick metrics:
grep -c "alloca" trio_O0.ll trio_O2.ll
grep -cE "br |br$" trio_O0.ll trio_O2.ll
wc -l trio_O0.ll trio_O2.ll

Typical pattern (versions vary):

trio_O0.ll: alloca ≈ 5-7, br ≈ 8-9, ~90 lines
trio_O2.ll: alloca 0,    br ≈ 1-2, ~35 lines

Diff পড়ুন:

diff trio_O0.ll trio_O2.ll | head -40

খুঁজুন: mem2reg-এর ফল (allocas→registers), countdown closed-form arithmetic series-এ fold হয়েছে কি না (GCC/Clang sometimes!), classify select-এ transform হয়েছে কি না।

Optional deeper tools (brew install llvm / apt install llvm):

llvm-as trio_O2.ll -o trio.bc        # text → bitcode container
opt -passes='mem2reg' -S trio_O0.ll -o promoted.ll
diff trio_O0.ll promoted.ll | head   # এক pass-এর isolated effect!

opt -passes=... — individual pass চালিয়ে দেখা: pipeline-এর pass menu সরাসরি আপনার হাতে। -debug-pass-manager flag দিলে pass order list-ও print হয়।

এটা কী প্রমাণ করে

IR-এ optimization-এর effect mechanically measurable — alloca count, branch count, instruction count সব diff-able; IR পড়া শিখলে optimizer-এর decision reverse-engineer করা যায়।

নিজে বানান

BUILD IT

ToyLang AST → TAC emitter — temporaries, labels, listing

Python · ●●●○○
  1. Lesson 5 AST import; quad tuple format fix করুন
  2. TempAllocator + LabelAllocator counters
  3. expr(): post-order — children first, op last, temp result
  4. IfStmt lowering: cond → if_false → then → goto/end pattern
  5. WhileStmt lowering: start-label back-edge pattern
  6. LetStmt copy emission; Print emission
  7. Listing printer + expected-TAC assert tests

toylang/tacgen.py — আমাদের compiler-এর middle-end প্রথম module:

"""ToyLang lowering: AST -> three-address code.
Needs: toylang/ast.py (lesson 5).
"""
from dataclasses import dataclass
from toylang.ast import (Program, LetStmt, Print, IfStmt,
                         WhileStmt, ExprStmt, NumLit, StrLit,
                         BoolLit, VarRef, BinOp, UnaryOp)

BIN_OP = {"plus": "add", "minus": "sub", "star": "mul",
          "slash": "div", "percent": "mod",
          "eqeq": "eq", "bangeq": "ne",
          "lt": "slt", "gt": "sgt",
          "lte": "sle", "gte": "sge"}
UN_OP = {"neg": "neg", "not": "lnot"}


class TACEmitter:
    """Walks the AST, emits quads:
       (op, arg1, arg2, dest)   arithmetic/comparison
       (op, src, "", dest)      copy/neg/lnot
       ("print", src, "", "")   builtin side effect
       ("label", name, "", "")
       ("goto", target, "", "")
       ("iffalse", cond, target, "")
    """

    def __init__(self):
        self.code = []
        self._tmp = 0
        self._lbl = 0

    # ---- allocators -------------------------------------------------
    def tmp(self) -\> str:
        self._tmp += 1
        return f"t{self._tmp}"

    def label(self, hint: str) -\> str:
        self._lbl += 1
        return f"L{self._lbl}_{hint}"

    def emit(self, *quad):
        self.code.append(quad)

    # ---- expressions: returns a PLACE (temp/var/literal) ----------
    def expr(self, e) -\> str:
        if isinstance(e, NumLit):
            return str(e.value)
        if isinstance(e, StrLit):
            return f'"{e.value}"'
        if isinstance(e, BoolLit):
            return "1" if e.value else "0"
        if isinstance(e, VarRef):
            return e.name
        if isinstance(e, UnaryOp):
            src = self.expr(e.operand)
            dst = self.tmp()
            self.emit(UN_OP[e.op], src, "", dst)
            return dst
        if isinstance(e, BinOp):
            left = self.expr(e.left)      # children first!
            right = self.expr(e.right)
            dst = self.tmp()
            self.emit(BIN_OP[e.op], left, right, dst)
            return dst
        raise AssertionError(type(e))

    # ---- statements ---------------------------------------------------
    def program(self, prog: Program):
        for s in prog.stmts:
            self.stmt(s)

    def stmt(self, s):
        m = {LetStmt: self.let_stmt, Print: self.print_stmt,
             IfStmt: self.if_stmt, WhileStmt: self.while_stmt,
             ExprStmt: self.expr_stmt}
        m[type(s)](s)

    def let_stmt(self, s):
        place = self.expr(s.init)
        self.emit("copy", place, "", s.name)

    def print_stmt(self, s):
        place = self.expr(s.arg)
        self.emit("print", place, "", "")

    def expr_stmt(self, s):
        self.expr(s.expr)          # value discarded

    def if_stmt(self, s):
        cond = self.expr(s.cond)
        if s.else_ is None:
            l_end = self.label("end")
            self.emit("iffalse", cond, l_end, "")
            self.block(s.then)
            self.emit("label", l_end, "", "")
        else:
            l_else = self.label("else")
            l_end = self.label("end")
            self.emit("iffalse", cond, l_else, "")
            self.block(s.then)
            self.emit("goto", l_end, "", "")
            self.emit("label", l_else, "", "")
            self.block(s.else_)
            self.emit("label", l_end, "", "")

    def while_stmt(self, s):
        l_start = self.label("start")
        l_end = self.label("end")
        self.emit("label", l_start, "", "")
        cond = self.expr(s.cond)
        self.emit("iffalse", cond, l_end, "")
        self.block(s.body)
        self.emit("goto", l_start, "", "")
        self.emit("label", l_end, "", "")

    def block(self, stmts):
        for s in stmts:
            self.stmt(s)


def generate(prog: Program):
    em = TACEmitter()
    em.program(prog)
    return em.code


def listing(code):
    """Aligned human-readable TAC listing."""
    out = []
    for op, a1, a2, dst in code:
        if op == "label":
            out.append(f"{a1}:")
        elif op == "goto":
            out.append(f"    goto {a1}")
        elif op == "iffalse":
            out.append(f"    if_false {a1} goto {a2}")
        elif op == "print":
            out.append(f"    print {a1}")
        elif a2 == "" and op != "add":
            out.append(f"    {dst} = {op} {a1}" if op in UN_OP.values()
                       else f"    {dst} = {a1}")
        else:
            out.append(f"    {dst} = {a1} {op} {a2}")
    return "\n".join(out)

Tests + demo — ফাইলের শেষে:

def _test():
    from toylang.ast import Span as Sp

    def let(n, init): return LetStmt(Sp(1, 1, 9), n, init)
    def num(v, c=1): return NumLit(Sp(1, c, c + 1), v)
    def vr(n, c=1): return VarRef(Sp(1, c, c + len(n)), n)
    def bl(v): return BoolLit(Sp(1, 1, 5), v)

    # 1. straight-line arithmetic
    prog = Program((let("x", BinOp(Sp(1, 9, 17), "plus",
                                   num(2), num(3))),))
    tac = generate(prog)
    assert tac == [
        ("add", "2", "3", "t1"),
        ("copy", "t1", "", "x"),
    ]

    # 2. if/else lowers to label pattern
    prog = Program((IfStmt(Sp(1, 1, 30), bl(True),
                           (Print(Sp(2, 5, 14), num(1)),),
                           (Print(Sp(4, 5, 14), num(0)),)),))
    tac = generate(prog)
    ops = [q[0] for q in tac]
    assert ops == ["iffalse", "print", "goto", "label",
                   "print", "label"]

    # 3. while has a back-edge
    prog = Program((WhileStmt(Sp(1, 1, 25), bl(True),
                              (ExprStmt(Sp(2, 5, 12),
                                        num(9)),),),))
    tac = generate(prog)
    assert tac[0][0] == "label" and tac[-1][0] == "label"
    assert tac[-2][:2] == ("goto", tac[0][1])   # back-edge target!

    print("all TAC tests passed")


if __name__ == "__main__":
    from toylang.ast import (Span as Sp, BinOp, NumLit, VarRef,
                             LetStmt, Print, WhileStmt, Program,
                             ExprStmt)
    demo = Program((
        LetStmt(Sp(1, 1, 20), "sum", NumLit(Sp(1, 11, 11), 0)),
        LetStmt(Sp(2, 1, 20), "i", NumLit(Sp(2, 11, 11), 1)),
        WhileStmt(Sp(3, 1, 40),
                  BinOp(Sp(3, 8, 15), "lte",
                        VarRef(Sp(3, 8, 8), "i"),
                        NumLit(Sp(3, 14, 14), 100)),
                  (
                      ExprStmt(Sp(4, 3, 24),
                               BinOp(Sp(4, 3, 23), "plus",
                                     VarRef(Sp(4, 3, 5), "sum"),
                                     VarRef(Sp(4, 9, 9), "i"))),
                      LetStmt(Sp(5, 3, 22), "sum",
                              VarRef(Sp(5, 11, 13), "tmp")),
                  )),
        Print(Sp(7, 1, 13), VarRef(Sp(7, 7, 9), "sum")),
    ))
    print(listing(generate(demo)))
    _test()

Expected output:

$ python3 toylang/tacgen.py
    sum = 0
    i = 1
L1_start:
    t1 = i sle 100
    if_false t1 goto L2_end
    add sum i -> discarded
    copy t2, sum
    sum = t2
    goto L1_start
L2_end:
    print sum
all TAC tests passed

(Exact temps vary — মূল structure: label, cond-compare, conditional exit, body, back-edge goto।)

নিজে বাড়ান:

  1. &&/|| short-circuit lowering: a && b → eval a; if_false → skip; eval b — branch pattern লাগবে expression-এর ভেতরেই।
  2. Constant folding pre-pass: add 2 3 দেখলে copy 5 — checker lesson-এর spotted opportunity এখানে cash করুন।
  3. Basic-block partitioner: TAC list → list of basic blocks + successor map (CFG!) — next module’s foundation।

বাস্তব সিস্টেমে

Real IRs — production ভেতরটা

LLVM IR। Typed SSA, infinite-register, explicit types per value (i32, float, ptr)। Ecosystem-এর gravity center — Clang/Rust/ Swift/Zig/Julia upstream, প্রতিটা CPU vendor downstream backend। Text (.ll) + bitcode (.bc) dual representation।

GCC GIMPLE ও RTL। দুই-level IR: GIMPLE (typed, gimplified, SSA-capable — আমাদের TAC-এর industrial sibling) high/mid optimizations- এর জন্য; RTL low-level scheduling/instruction selection-এর জন্য। “এক compiler-এ একাধিক IR” শুধু rustc-র গল্প না।

JVM bytecode। javac প্রায় optimize করেই না — smart workটা runtime JIT (C1/C2, GraalVM) করে। Bytecode = stable deployment contract, JIT IR = internal। Verification pass (class loader-এ) type-checking-এর runtime sibling।

CPython bytecode। Stack-machine instructions — dis module দিয়ে দেখা যায়। PEP 659 specializing adaptive interpreter: bytecode-ই runtime-এ specialise হয় — IR আর interpreter-এর boundary blur-এর modern example।

WASM। Validated, sandboxed, structured-control (branch targets nest হয় — arbitrary goto নেই!)। Security model IR design-কে dictate করেছে — proof যে IR choice pure-engineering decision না।

MLIR। LLVM family-র multi-level IR infrastructure — ML kernels, hardware compilers। “Level প্রতি-IR” idea-র extreme generalization; rustc-র phase-tree-র philosophical descendant at ecosystem scale।

যে ভুলগুলো সবাই করে

“SSA শুধু academic exercise — production কেউ ব্যবহার করে না।”

উল্টো: প্রতিটা major optimizing compiler-এর middle-end এখন SSA-based (LLVM, GCC 4+, HotSpot C2, V8 TurboFan, rustc MIR, .NET RyuJIT)। 1990-এর দশকে academic ছিল; আজ absence হলে সেটা news। Academic-to- industrial pipeline-এর textbook success story।

“φ-node একটা normal instruction — CPU-তে চলে।”

Runtime-এ φ-এর কোনো existence নেই — notation-only construct। Backend (out-of-SSA conversion) এটাকে parallel copies বা register-coalescing trick-এ dissolve করে। φ ভুলে সরাসরি machine code মানলে register allocation-এর পুরো stage-ই misread হবে।

“-O0-এর alloca-heavy IR মানে compiler বোকা।”

Deliberate design: memory-buffered style SSA construction সহজ করে compile speed দেয় (build-time metric!), আর debug experience perfect রাখে (প্রতিটা variable-এর আসল stack slot)। -O0-এর লক্ষ্য fast compile

  • debuggability — fast execution না। Same compiler, different goals, different IR shapes।

“Bytecode আর IR synonym।”

IR = compiler-internal working representation (lifetime: compile); bytecode = deployment artifact (lifetime: distribution + execution)। JVM bytecode public spec-frozen; LLVM IR semi-public version-churning। Confusing এদের মানে portability/stability guarantee গুলোও confuse।

“একটা good IR-ই সব stage চালাতে পারে।”

rustc-র চার level, GCC-র দুই, MLIR-র unlimited — সবাই ভোট দিচ্ছে multi-level-এর পক্ষে। Analysis-এর ধরন বদলালে ideal representation বদলায়: name-resolution tree ≠ type-checking tree ≠ SSA flat form। Abstraction বাড়ানোর সাথে IR levels বাড়াটা feature, waste না।

বুঝেছেন কি না দেখুন

1

TAC-এর naming-এর কারণে “three-address” — কোন তিনটা address? Quads-এর চার field-এর সাথে reconcile করুন।

স্মরণ

Binary op: দুই source address + এক destination = তিন। Copy/unary: দুই-ই ব্যবহৃত হয়। Quads-এর চতুর্থ field operator — fixed-width encoding convenience; conceptually instruction = op + ≤3 addresses।

2

x = a + b * c - d; — TAC লিখুন (tree evaluation order respect করে)। কয়টা temporary লাগল?

প্রয়োগ

t1 = b * c; t2 = a + t1; t3 = t2 - d; x = t3 — তিনটা temporary। Post-order emission: deepest subexpression (precedence-highest) first — parser/tree structure থেকে automatic, manual precedence thinking-এর দরকার নেই।

3

Loop-এর back-edge কেন φ জন্ম দেয়? sum example-এ sum3 = φ(sum1, sum2)-এর দুই input-এর source path বলুন।

যুক্তি

Back-edge মানে join-block-এ পৌঁছানোর দুই route: entry-direct (sum1) আর loop-body-through (sum2)। Path-dependent value = φ-এর exactly use case। Input mapping follows CFG predecessor edges — first φ-arg predecessor-block-order-এ (convention varies, semantics edge-keyed)।

4

clamp -O2-তে branchless হলো কেন ভালো? কখন branch version বেশি?

প্রয়োগ

Branchless: no pipeline flush on misprediction (branch-prediction lesson recap), better ILP। Branch version wins when condition highly predictable AND bodies large/expensive — select forces both sides evaluated। Optimizer-রা cost model দিয়ে decide করে — universal branchless না।

5

আমাদের emitter-এ && short-circuit এখনো wrong — and quad দুই side evaluate করে। Lowering sketch করুন (labels সহ)।

ডিজাইন

t_a = eval(a); if_false t_a → L_short; t_b = eval(b); t_r = copy t_b... বা result-normalized form: evaluate a; if false → result=0 jump end; else result = eval(b); end:. Key: right operand evaluation conditional — pure-operator encoding এটা express-ই করতে পারে না, branch structure mandatory। Side-effects-যুক্ত ভাষায় correctness issue, শুধু efficiency না।

6

mem2reg promote করে কোন allocas? কোনগুলো আটকে থাকে, কেন?

স্মরণ

Promotable: slot-এর সব uses load/store (address কোথাও leak হয় না)। আটকে যায়: address taken (&x passed somewhere), arrays (gep-indexed), volatile। Promotion = φ-insertion + renaming — এই দুইটাই SSA construction machinery, isolated pass-এ testable।

এরপর কী

এরপর কী

আমাদের ToyLang compiler এখন সম্পূর্ণ frontend + lowering সহ: lexer → parser → AST → resolver → type checker → TAC। Module-এর বাকি অংশ এই TAC-এর উপরেই দাঁড়াব —

  • Optimization: আমাদের emitted code-এ constant folding, dead code elimination, algebraic simplification — pass architecture hands-on
  • Code generation: TAC → real x86-64 assembly — register allocation-এর প্রথম সংঘর্ষ
  • Linking: আমাদের .o ফাইল libc-র সাথে মিলবে — পুরো circle

প্রথম লেসনের ম্যাপের প্রতিটা পিন এখন নিজের হাতে বানানো।

আরও পড়ুন

  • Compilers: Principles, Techniques, and Tools (2nd ed.), Chapter 6 — Intermediate-Code Generation — Aho, Sethi, Ullman, Lam (the Dragon Book) · TAC/quads, translation schemes — এই লেসনের build section-এর formal ভিত্তি
  • "LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation" (CGO 2004) — Chris Lattner, Vikram Adve · LLVM IR-এর design rationale — কেন typed SSA একটা ecosystem জন্ম দিল
  • Engineering a Compiler (2nd ed.), Chapter 5 — Intermediate Representations — Keith Cooper, Linda Torczon · IR taxonomy (graph vs linear vs hybrid) আর SSA-র practical treatment