Intermediate Representation — TAC, SSA, আর কেন একটা মাঝের ভাষা দরকার
Intermediate Representation
Compiler-এর মূল ভাষায় প্রবেশ — tree-কে linear three-address code-এ flatten, SSA-র গণিত, আর নিজের হাতে প্রথম lowering।
আগে এটা বুঝি
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 = t3Tree-র nested structure ভেঙে গেল linear chain-এ — আর প্রতিটা
intermediate value-এর নাম দরকার হলো: temporary (t1, t2...)।
Temporary = compiler-এর virtual register — এখনো CPU register না,
কিন্তু একদিন হবে (register allocation, backend-এর কাজ)।
Instruction family:
| Form | উদাহরণ | কাজ |
|---|---|---|
| Binary | t1 = x + y | arithmetic/comparison |
| Unary | t2 = -x | negation |
| Copy | x = t3 | assignment |
| Unconditional jump | goto L2 | control |
| Conditional jump | if t1 \< 0 goto L2 | branch |
| Label | L2: | target marker |
| Call/print | print t4 | side-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: joinOptimization = 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 + y1Def-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 IR | JVM bytecode | WASM | |
|---|---|---|---|
| Nature | typed SSA text/bitcode | stack-based, verified | sandboxed binary |
| Target | static compilation | portable runtime | browser/embedded VM |
| Optimization | aggressive (opt passes) | JIT-এর ভেতরে | limited (fast validate) |
| Stability | internal-ish (version churn) | spec-frozen decades | spec-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.llAnnotated 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]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 sum3L3-এ φ লাগল কেন? Loop-back edge — sum1 (initial) নাকি sum2 (updated) path-dependent। Optimizer এখন sum3-এর দুই incoming-ই আলাদা চিনতে পারে — induction variable analysis-র raw material।
নিজে চালিয়ে দেখুন
তিনটা C function — -O0 বনাম -O2 LLVM IR diff
তিনটা 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.llTypical pattern (versions vary):
trio_O0.ll: alloca ≈ 5-7, br ≈ 8-9, ~90 lines
trio_O2.ll: alloca 0, br ≈ 1-2, ~35 linesDiff পড়ুন:
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 করা যায়।
নিজে বানান
ToyLang AST → TAC emitter — temporaries, labels, listing
- Lesson 5 AST import; quad tuple format fix করুন
- TempAllocator + LabelAllocator counters
- expr(): post-order — children first, op last, temp result
- IfStmt lowering: cond → if_false → then → goto/end pattern
- WhileStmt lowering: start-label back-edge pattern
- LetStmt copy emission; Print emission
- 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।)
নিজে বাড়ান:
&&/||short-circuit lowering:a && b→ eval a; if_false → skip; eval b — branch pattern লাগবে expression-এর ভেতরেই।- Constant folding pre-pass:
add 2 3দেখলেcopy 5— checker lesson-এর spotted opportunity এখানে cash করুন। - 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 না।
বুঝেছেন কি না দেখুন
1TAC-এর 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।
2x = a + b * c - d; — TAC লিখুন (tree evaluation order respect
করে)। কয়টা temporary লাগল?
প্রয়োগ
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-এর দরকার নেই।
3Loop-এর back-edge কেন φ জন্ম দেয়? sum example-এ sum3 = φ(sum1,
sum2)-এর দুই input-এর source path বলুন।
যুক্তি
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)।
4clamp -O2-তে branchless হলো কেন ভালো? কখন branch version বেশি?
প্রয়োগ
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 সহ)।
ডিজাইন
&& 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 না।
6mem2reg 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