AST Design — tree-টা নিজেই একটা data structure design সমস্যা
AST Design
Parse tree থেকে compiler-এর মূল data structure — node taxonomy, variant encoding, spans, আর round-tripping pretty-printer।
আগে এটা বুঝি
আগের লেসনের parser tuples দিয়ে tree বানাল — ("plus", 1, ("star", 2, 3))। কাজ করে, কিন্তু এভাবে পুরো compiler লিখলে
দুই সপ্তাহ পরে নিজের code-ই বোঝা যাবে না: কোন tuple-এ ক’টা element,
string না number কোন position-এ — সব memory-তে রাখতে হবে।
Compiler-এর প্রতিটা পরের stage এই tree-কেই ছুঁয়ে চলবে: semantic analysis name resolve করবে এর উপর, type checker type attach করবে এর উপর, IR generator walk করবে এর উপর। Tree-টাই compiler-এর spine — এর design খারাপ হলে প্রতিটা পরের stage-এ ঋণ শোধ করতে হবে।
আজকের লেসন তাই pure software engineering: grammar theory নেই, CPU নেই — শুধু প্রশ্ন: একটা program-এর structure represent করার সবচেয়ে ভালো data structure কী?
আজকের প্রশ্ন তিনটা: কোন node-গুলো লাগবে? Variant-গুলো encode হবে কীভাবে (class hierarchy? enum?)? আর error বলার জন্য tree-কে কতটা source-position মনে রাখতে হবে?
মূল ধারণা
Node taxonomy — প্রথম বিভাজনটা Expr vs Stmt
প্রতিটা ভাষায় দুই family থাকে:
| Expression | Statement | |
|---|---|---|
| Value produce করে? | হ্যাঁ (1+2, x) | না — action |
| জায়গা | expression-এর ভেতরে nested | sequence-এ, block-এ |
| ToyLang nodes | NumLit, StrLit, BoolLit, VarRef, UnaryOp, BinOp, Group | LetStmt, Print, If, While, Block |
C-র a = b = 0 বা Rust-র {} block-as-expression — boundary
blur করার design-ও আছে (expression-oriented languages)। ToyLang
classic split রাখবে — কারণ দুই family-র ভুল-ধরার নিয়ম আলাদা:
statement-এর result void, expression-এর টাইপ থাকবে (type-checking
lesson-এ ফলবে)।
ToyLang-এর পূর্ণ taxonomy — module জুড়ে এটাই official:
Program ── stmts: [Stmt]
Stmt:
LetStmt (name, init:Expr, span)
Print (arg:Expr, span)
If (cond:Expr, then:[Stmt], else:[Stmt]|None, span)
While (cond:Expr, body:[Stmt], span)
ExprStmt (expr:Expr, span)
Expr:
NumLit (value:int, span)
StrLit (value:str, span)
BoolLit (value:bool, span)
VarRef (name:str, span) ← placeholder! resolution পরে
UnaryOp (op:"neg"|"not", operand:Expr, span)
BinOp (op:str, left:Expr, right:Expr, span)Variant encoding — দুই মহাদেশ
Node-দের “একটা জিনিস, অনেক রূপ” — এটা sum type। Encode করার দুই dominant পদ্ধতি:
Approach A — OOP class hierarchy + visitor:
Expr (abstract)
/ | \
NumLit BinOp VarRef ...
interface Visitor[R]:
visit_num(NumLit): R
visit_bin(BinOp): R
...নতুন node-type যোগ করা সহজ (শুধু নতুন class), কিন্তু নতুন operation যোগ করা মানে সব class-ছোঁয়া বা visitor interface ভাঙা। Java ecosystem-এর classic tension।
Approach B — tagged union / enum + match:
enum Expr:
num(value)
bin(op, l, r)
var(name)
match expr:
case num(v) → ...
case bin(op, l, r) → ...
case var(n) → ...নতুন operation trivial (নতুন match), নতুন node-type মানে সব match update — আর ঠিক এটাই feature: Rust-এর exhaustive match compile- error দেয় প্রতিটা missed site-এ। Compiler-এ operations (analysis) node-types-এর চেয়ে বেশি বদলায় না — দুটোই স্থির — কিন্তু missed-case error-টা এত দামি যে modern compilers প্রায় সবাই Approach B নেয় (rustc enum, Clang-ও C++-এ tagged-ish Record ব্যবহার করে)।
Python-এ আমরা hybrid: dataclasses + kind field + match/case (3.10+) বা if-chain — exhaustive check manual discipline-এ চলবে, tests দিয়ে enforce।
Source span — প্রতিটা node-এ, exception ছাড়া
Span = source-এ কোন character-range থেকে node এসেছে। তিনটা representation:
(line, col, end_line, end_col) ← human-readable, compute-heavy
(start_offset, end_offset) ← compact; line/col lazily derive
FileID + u32 range ← production style (rustc Span)Cost: প্রতি node ~৮-১৬ bytes। Benefit: প্রতিটা diagnostic, IDE underline, go-to-definition, refactoring — সব এই bytes খায়। Position পরে যোগ করা যায় না — token stream ফেলে দেওয়ার পরে offset হারায়। এজন্যই rule: span ঢোকাও day one, optimize করো never (চাইলে arena-compress later)।
Immutability আর transforms
Design question: AST mutable না frozen?
- Frozen (immutable): thread-safe sharing, reasoning সহজ, কিন্তু annotate করা কঠিন — solution: transform — old tree থেকে নতুন tree, phase tag সহ
- Mutable: pragmatic, Python-এ natural; risk: কে কী mutate করল ট্র্যাক হারায়
Production answer: দুটোই — কিন্তু phase separation mandatory: rustc-র AST → HIR → THIR → MIR প্রতিটা আলাদা immutable-ish tree, প্রতিটা stage-এর প্রয়োজন-molded। এক tree-কে সব কাজে ভাড়া দেওয়ার চেয়ে তিনটা purpose-built tree সস্তা — copy cost modern hardware- এ negligible, cognitive cost শূন্যের কাছে।
ভেতরে কী ঘটছে
AST vs CST — punctuation-এর মৃত্যু
Concrete Syntax Tree (parse tree) grammar-এর প্রতিটা step রাখে; AST শুধু meaning-bearing parts:
Input: ( 1 + 2 ) * 3
CST: AST:
(*) (*)
├── ( ├── (+)
│ (+) │ ├── 1
│ ├── 1 │ └── 2
│ ├── + └── 3
│ └── 2
└── *CST-তে (, ) node আছে — grammar-এ terminal ছিল বলে। AST-তে
parentheses information হারায় না — grouping already encoded
tree-shape-এ। একইভাবে হারায়: semicolon, comma-list-এর comma,
redundant layers (term, factor)।
কখন CST লাগে? Formatter (gofmt, black), refactoring tools, tree-sitter highlighting — যেখানে source text faithfully reconstruct করতে হয়। এজন্যই black নিজের CST-ish representation রাখে, আর আমাদের build-এর pretty-printer canonical spacing emit করবে (exact original না — canonical)।
Visitor বনাম switch — dispatch mechanics
OOP double-dispatch-এর ভেতরটা:
accept(visitor):
visitor.visit_bin(this) ← second virtual call!দুই vtable hop per node traversal। Switch-on-kind: one jump table, branch predictor friendly — benchmark-এ ২-৫× difference দেখা যায় hot loops-এ। তবে honest caveat: modern JIT-এ পার্থক্য প্রায় মুছে যায়; architecture choice performance-এর চেয়ে correctness-tooling (exhaustiveness)-এর জন্যই হওয়া উচিত।
Memory layout — arena allocation
Million-node tree-এ প্রতি node আলাদা heap allocation = cache miss
festival। Production trick: arena/bump allocator — বড় chunk
allocate, node-গুলো sequential বসে; tree pointers index-এ পরিণত
(rustc-র Arena, clang-র ASTContext)। Free হয় একবারে পুরো arena —
per-node destructor নেই। GC-ভাষায় এটা free-তে মোটামুটি পাওয়া যায়
(allocation cheap, locality decent) — আরেকটা কারণ compiler teaching
language হিসেবে Java/Haskell viable।
Recursion-এর depth-ও এখানে দাবি করে: deeply-nested expression (হাজার-level paren!) stack overflow করাতে পারে recursive walk-কে — production parsers explicit stack বা depth-limit নেয় (rustc-র “recursion limit” attribute!)।
উদাহরণ
একটা program, পুরো taxonomy ঘুরে
let width = 4;
let height = width * 2;
if (!done) {
print(width * height + 1);
}Python dataclasses-এ এর AST (schematic — spans বাদ দিয়ে পড়া সহজ করতে):
Program([
LetStmt("width", NumLit(4)),
LetStmt("height", BinOp("*", VarRef("width"), NumLit(2))),
If(
cond=UnaryOp("not", VarRef("done")),
then=[Print(BinOp("+",
BinOp("*", VarRef("width"), VarRef("height")),
NumLit(1)))],
else_=None),
])লক্ষ্য করুন তিনটা design ফল:
width * 2-এর*একটা BinOp string op-field — operator-প্রতি আলাদা class না (Add/Mul/Sub classes বানানো over-engineering; op-string/table enough)if-এর body list of Stmt, single-node না — block braces syntax sugar, semantics listelse_=None— absence একটা first-class state (Optional)
Rust-এ একই shape:
enum Stmt {
Let { name: String, init: Expr },
Print { arg: Expr },
If { cond: Expr, then: Vec\<Stmt>, else_: Vec\<Stmt> },
}
enum Expr {
Num(i64),
Var(String),
Bin { op: BinOp, l: Box\<Expr>, r: Box\<Expr> },
Not(Box\<Expr>),
}
// exhaustiveness demo:
fn size(e: &Expr) -> usize {
use Expr::*;
match e {
Num(_) => 1,
Var(_) => 1,
Bin { l, r, .. } => 1 + size(l) + size(r),
// Not arm বাদ দিলে: compile error!
}
}Not arm comment-out করলে rustc বলবে non-exhaustive patterns: \Not(_)` not covered` — প্রতিটা affected site-এ। OOP world-এ
এই guarantee পাওয়ার একমাত্র উপায় abstract-method যোগ করা — যেটা
operation-per-class explosion তৈরি করে।
ছবিতে
পুরো pipeline-এ AST-এর জায়গা
tokens ──parser──▶ AST ──semantic──▶ annotated AST ──▶ TAC/IR
│ │
│ resolver │ type checker
▼ ▼
symbol table type map
│ │
└─────► diagnostics ◄─┘
│
▼
user-facing errors
"let x = true + 1;
^^^^^^^ bool + int"Tree walk-এর দুই order
Tree for 8 - 3 + 2 (= (8-3)+2, left-assoc):
(+)
/ \
(-) 2
/ \
8 3
pre-order (root-left-right): + - 8 3 2 ← prefix notation
post-order (left-right-root): 8 3 - 2 + ← RPN/postfix!
in-order (left-root-right): 8 - 3 + 2 ← source-এর সাথে মিলে
evaluator post-order walk করে: 8, 3, -(=5), 2, +(=7)Printer in-order চাইলে parenthesization নিয়ম দরকার: child-এর bp < parent-এর bp হলে parens — ঠিক Pratt binding power-এর উল্টো দিক। Round-trip test-এর মূল চ্যালেঞ্জ এটাই।
নিজে চালিয়ে দেখুন
Real-world AST dump — Python আর Clang একই snippet-এ
ধাপ ১ — Python-এর built-in ast module (no pip needed):
import ast
src = """
total = 0
for i in [1, 2, 3]:
total = total + i * 2
"""
tree = ast.parse(src)
print(ast.dump(tree, indent=4))Output (truncated):
Module(
body=[
Assign(targets=[Name(id='total')], value=Constant(0)),
For(target=Name(id='i'),
iter=List([Constant(1), Constant(2), Constant(3)]),
body=[Assign(targets=[Name(id='total')],
value=BinOp(left=Name(id='total'),
op=Add(),
right=BinOp(left=Name(id='i'),
op=Mult(),
right=Constant(2))))])])খেয়াল করুন: op=Add() — operator-প্রতি আলাদা class (approach A!);
Constant(0) — literals unified; কোনো paren/comma node নেই (AST!)।
Spans দেখতে: ast.dump(tree, include_attributes=True, indent=2) —
প্রতি node-এ lineno=, col_offset= — ঠিক আমাদের span argument।
ধাপ ২ — Clang-এর AST (macOS-এ clang built-in; Linux: apt install clang):
/* t.c */
int area(int w, int h) {
return w * h + 1;
}clang -Xclang -ast-dump -fsyntax-only t.c | head -30
# JSON version (tool-friendly):
clang -Xclang -ast-dump=json -fsyntax-only t.c | head -60দেখবেন:
FunctionDecl ... \<line:1:1, col:?\<...> area 'int (int, int)'
|-ParmVarDecl ... used w 'int'
|-ParmVarDecl ... used h 'int'
`-CompoundStmt
`-ReturnStmt
`-BinaryOperator ... 'int' '+'
|-BinaryOperator ... 'int' '*'
| |-ImplicitCastExpr ... 'int' \<LValueToRValue>
| | `-DeclRefExpr ... 'int' lvalue ParmVar 'w'
...দুইটা গুরুত্বপূর্ণ observation:
'int'types attached everywhere — clang dump post-sema tree (types already checked); Pythonast.parsepre-sema (pure syntax)। Same word “AST”, দুই lifecycle point।ImplicitCastExpr— checker-এর inserted node; syntax-এ কোনো trace নেই, semantics-এর ফল। এটাই proof: AST evolves।
ধাপ ৩ — compare shapes: একই w * h + 1 Python-এ w*h+1 dump করুন —
BinOp(op=Add(), left=BinOp(op=Mult())); clang-ও same nesting, ভিন্ন
naming। Structure universal-ish, vocabulary local।
প্রতিটা real compiler নিজের AST shape নিজের মতো design করে — same source ভিন্ন tree; AST কোনো universal format না, একটা design artifact।
নিজে বানান
ToyLang AST — dataclasses + round-tripping pretty-printer
- Span dataclass + প্রতিটা node-এ span field
- Expr nodes: NumLit, StrLit, BoolLit, VarRef, UnaryOp, BinOp
- Stmt nodes: LetStmt, Print, If, While, ExprStmt + Program
- PrettyPrinter: in-order emission, binding-power-aware parens
- normalize(): parse→print→parse idempotence যাচাই
- Tests: hand-built AST → printed source → expected string
toylang/ast.py — self-contained (parser integration optional):
"""ToyLang AST: node definitions + pretty-printer."""
from dataclasses import dataclass, field
@dataclass(frozen=True)
class Span:
line: int
col: int
end_col: int
def show(self):
return f"{self.line}:{self.col}-{self.end_col}"
# ---------------- expressions ----------------
@dataclass(frozen=True)
class Expr:
span: Span
@dataclass(frozen=True)
class NumLit(Expr):
value: int
@dataclass(frozen=True)
class StrLit(Expr):
value: str
@dataclass(frozen=True)
class BoolLit(Expr):
value: bool
@dataclass(frozen=True)
class VarRef(Expr):
name: str
@dataclass(frozen=True)
class UnaryOp(Expr):
op: str # "neg" | "not"
operand: Expr
@dataclass(frozen=True)
class BinOp(Expr):
op: str
left: Expr
right: Expr
BP = {"oror": 10, "andand": 20, "eqeq": 40, "bangeq": 40,
"lte": 50, "gte": 50, "lt": 50, "gt": 50,
"plus": 60, "minus": 60, "star": 70, "slash": 70}
SYM = {"plus": "+", "minus": "-", "star": "*", "slash": "/",
"eqeq": "==", "bangeq": "!=", "lt": "\<", "gt": ">",
"lte": "\<=", "gte": ">=", "andand": "&&", "oror": "||"}
# ---------------- statements ----------------
@dataclass(frozen=True)
class Stmt:
span: Span
@dataclass(frozen=True)
class LetStmt(Stmt):
name: str
init: Expr
@dataclass(frozen=True)
class Print(Stmt):
arg: Expr
@dataclass(frozen=True)
class IfStmt(Stmt):
cond: Expr
then: tuple
else_: tuple | None
@dataclass(frozen=True)
class WhileStmt(Stmt):
cond: Expr
body: tuple
@dataclass(frozen=True)
class ExprStmt(Stmt):
expr: Expr
@dataclass(frozen=True)
class Program:
stmts: tuple
def _bp(e: Expr) -> int:
return BP.get(e.op, 99) if isinstance(e, BinOp) else 99
class Printer:
"""AST -> canonical ToyLang source."""
def expr(self, e: Expr, parent_bp: int = 0) -> str:
m = {
NumLit: lambda n: str(n.value),
StrLit: lambda n: f'"{n.value}"',
BoolLit: lambda n: "true" if n.value else "false",
VarRef: lambda n: n.name,
UnaryOp: lambda n: ("-" if n.op == "neg" else "!") +
self.expr(n.operand, 90),
BinOp: lambda n: self._bin(n),
}
return m[type(e)](e)
def _bin(self, n: BinOp) -> str:
my = BP[n.op]
l = self.expr(n.left, my)
r = self.expr(n.right, my + 1) # right child tighter!
s = f"{l} {SYM[n.op]} {r}"
return f"({s})" if my \< parent_bp else s
def stmt(self, s: Stmt, ind: str = "") -> str:
if isinstance(s, LetStmt):
return f"{ind}let {s.name} = {self.expr(s.init)};"
if isinstance(s, Print):
return f"{ind}print({self.expr(s.arg)});"
if isinstance(s, ExprStmt):
return f"{ind}{self.expr(s.expr)};"
if isinstance(s, IfStmt):
out = [f"{ind}if ({self.expr(s.cond)}) {{"]
out += [self.stmt(x, ind + " ") for x in s.then]
out.append(f"{ind}}}")
if s.else_ is not None:
out[-1] += " else {"
out += [self.stmt(x, ind + " ") for x in s.else_]
out.append(f"{ind}}}")
return "\n".join(out)
if isinstance(s, WhileStmt):
out = [f"{ind}while ({self.expr(s.cond)}) {{"]
out += [self.stmt(x, ind + " ") for x in s.body]
out.append(f"{ind}}}")
return "\n".join(out)
raise TypeError(type(s))
def program(self, p: Program) -> str:
return "\n".join(self.stmt(s) for s in p.stmts)
def pretty(node) -> str:
if isinstance(node, Program):
return Printer().program(node)
if isinstance(node, Stmt):
return Printer().stmt(node)
return Printer().expr(node)Test suite — ফাইলের শেষে:
def _test():
S = Span
# hand-built: let x = 1 + 2 * 3;
prog = Program((LetStmt(S(1, 1, 20), "x",
BinOp(S(1, 11, 19), "plus", NumLit(S(1, 9, 9), 1),
BinOp(S(1, 15, 19), "star",
NumLit(S(1, 13, 13), 2),
NumLit(S(1, 17, 19), 3)))),))
assert pretty(prog) == "let x = 1 + 2 * 3;"
# parens needed when child binds looser:
grouped = BinOp(S(1, 1, 12), "star",
BinOp(S(1, 2, 7), "plus",
NumLit(S(1, 3, 3), 1),
NumLit(S(1, 6, 6), 2)),
NumLit(S(1, 11, 11), 3))
assert pretty(grouped) == "(1 + 2) * 3"
# unary chains:
assert pretty(UnaryOp(S(1, 1, 4), "not",
BoolLit(S(1, 4, 7), False))) == "!false"
# if/else round-trip shape:
iff = IfStmt(S(1, 1, 40), VarRef(S(1, 5, 5), "ok"),
(Print(S(2, 5, 14), NumLit(S(2, 11, 11), 1)),),
(Print(S(4, 5, 14), NumLit(S(4, 11, 11), 0)),))
want = ("if (ok) {\n print(1);\n} else {\n print(0);\n}")
assert pretty(iff) == want
print("all AST tests passed")
if __name__ == "__main__":
_test()Expected:
$ python3 toylang/ast.py
all AST tests passedRound-trip contract-টা এখন mechanical: pretty(parse(src))
re-parse করে same AST দেয় — পরের লেসনে parser-কে এই nodes return
করতে wire করলেই property test হয়ে যাবে:
assert pretty(Parser(src).program()) == pretty(Parser(pretty(...)).program())।
নিজে বাড়ান:
- Parser (lesson 4)-কে wire করুন — tuples-এর বদলে এই dataclasses return করুন; lexer positions থেকে span fill করুন।
size(Program)walker লিখুন switch-style — node-count report.find_spans(program, line)— specific line-এর সব node return; IDE hover-এর mini-simulation।
বাস্তব সিস্টেমে
Real ASTs — কারা কেমন রাখে
rustc (AST → HIR → THIR → MIR)। Phase-tagged tree pipeline-এর flagship। প্রতিটা level lower abstraction: AST macros-সহ raw, HIR desugared (loop→while+break), MIR SSA-flavored flat CFG। Borrow checker MIR-এ চলে — সঠিক level সঠিক analysis-এর চাবি।
Clang AST। Post-sema, types সহ, allocator-arena based।
-ast-dump API হিসেবে stable — clang-tidy, ASTMatchers, IDE
tooling সব এর উপর। AST-কে public API বানানোর সবচেয়ে বড় উদাহরণ।
TypeScript compiler। পুরো checker নিজের AST-র উপর single-pass with caching; ts-morph library-রা এই AST manipulate করে codemod চালায়। JS ecosystem-এর refactor tooling প্রায় সব AST-based।
Babel। Plugin API = AST transform API। React JSX থেকে
createElement call — একটা AST-to-AST pass। Web dev-এর “magic”
অনেকটাই এই এক pattern।
tree-sitter। Concrete tree (punctuation সহ!) — কারণ editors highlight/error-recovery-র জন্য exact text mapping চায়। AST-vs-CST choice-এর দুই দিকেরই live উদাহরণ এই দুই toolchain।
Python ast module। stdlib-এই full AST access — linters
(flake8 plugins), security scanners (bandit), formatters (black
token-based তবে ast-assisted), doc generators — সব একই collection
উপরে দাঁড়িয়ে।
যে ভুলগুলো সবাই করে
“AST grammar-এর exact mirror হতে হবে।”
উল্টো — ভালো AST grammar-কে ignore করে meaning model করে:
term/factor layers collapse, commas vanish, for loop হয়তো
desugar হয়ে while+assignment হয়ে যায়। Grammar implementation
artifact, AST design artifact — দুই document, দুই audience।
“একটা perfect AST পুরো compiler চলবে।”
rustc-র চার-level tree pipeline এর উত্তর: analysis বদলালে ideal representation বদলায়। Name-resolution-friendly tree, borrow-check- friendly tree, codegen-friendly tree — একটা না। Phase separation complexity যোগ করে, কিন্তু প্রতিটা pass সরল করে — net win at scale।
“Span শুধু nice-to-have — MVP-তে বাদ।”
Span-ছাড়া compiler-এ error message হয় “Error somewhere” — user ফেলে চলে যায়। আর পরে যোগ করা মানে প্রতিটা constructor signature ভাঙা। Cost ছোট (bytes), benefit existential — day-one inclusion একমাত্র sane order।
“Visitor pattern = AST processing-এর একমাত্র professional উপায়।”
Visitor একটা trade-off (easy add-node, hard add-operation), exhaust matching উল্টোটা। GoF book-এর era-তে OOP purity-র জন্য visitor default ছিল; functional revival-এর পরে industry স্থির হয়েছে tagged-union+ match-এ — কারণ compiler লেখার সময় নতুন operation বেশি লাগে নতুন node-এর চেয়ে।
“Operator-প্রতি আলাদা class (AddNode, MulNode...) বেশি precise, তাই ভালো।”
Class explosion দেয় শুধু: প্রতিটা operation-এ প্রতিটা subclass touch, serialization bloat, pattern-match verbosity। Op একটা finite small set — string/enum field + signature table (type-checker lesson-এ দেখব) একই information কম code-এ রাখে। Precision যেখানে semantic difference আছে সেখানেই দরকার, lexical variation-এ না।
বুঝেছেন কি না দেখুন
1AST থেকে কোন কোন জিনিস intentionally drop হয়? তিনটা উদাহরণ দিন।
স্মরণ
Parentheses (grouping tree-shape-এ encode), separators (semicolon/ comma), grammar helper layers (term/factor)। সাধারণ নিয়ম: যা meaning বদলায় না শুধু spelling — CST-এর, AST-এর না।
2let y = -(a + b) * 2; — আমাদের taxonomy-তে AST আঁকুন (span ছাড়া)।
প্রয়োগ
let y = -(a + b) * 2; — আমাদের taxonomy-তে AST আঁকুন (span ছাড়া)।LetStmt(“y”, BinOp(star, UnaryOp(neg, BinOp(plus, VarRef(a),
VarRef(b))), NumLit(2)))। Negation শুধু (a+b)-এর উপর — *-এর
না (unary bp 90 > 70); paren group node-টা printer-এ implicit,
AST-তে shape দিয়েই express।
3Exhaustive matching কেন compiler-এ এত দামি, যখন সব language-এ না?
যুক্তি
Compiler codebase-এ node-এর ভেতরে new-operation যোগ হয় মাসে-মাসে, new-node বছরে-বছরে। Exhaustive match নতুন node যোগের দিনই সব missed site enumerate করে — distributed TODO list automatic। Dynamic- dispatch world-এ এই knowledge runtime-crash বা silent wrong behavior হয়ে surface করে। Language-এর জন্য tooling guarantee — এটাই মূল্য।
4আপনার ভাষায় compound assignment x += 1 যোগ করবেন। Desugaring
(x = x + 1) নাকি dedicated AssignOp node? Trade-offs?
ডিজাইন
x += 1 যোগ করবেন। Desugaring
(x = x + 1) নাকি dedicated AssignOp node? Trade-offs?Desugar: পরের সব pass এক node-type জানে — simple; কিন্তু x.f() += 1 জাতীয় double-evaluation bug-এর ঝুঁকি আর original spelling
হারায় (diagnostics-এ += দেখাতে op info লাগবেই)। Dedicated node:
semantics explicit, evaluation once guaranteed, printer faithful —
cost এক extra case প্রতি pass-এ। Modern consensus: dedicated node
(desugar only where provably safe)।
5Pretty-printer-এ right child-কে my+1 parent bp pass করা হলো
(self.expr(n.right, my + 1))। এই +1 কেন? কোন ভুল আটকায়?
যুক্তি
my+1 parent bp pass করা হলো
(self.expr(n.right, my + 1))। এই +1 কেন? কোন ভুল আটকায়?Right-assoc rendering-এর জন্য: same-bp child বামে হলে bare (a-b-c
left-fold), ডানে same-bp হলে paren দরকার — a-(b-c) নাহলে print
করলে a-b-c (different tree!)। +1 দিলে right-child same-bp হলে
child_bp < parent_bp fail করে paren force করে। Round-trip
correctness-এর ঠিক এই micro-detail-ই।
6Clang-এর -ast-dump-এ ImplicitCastExpr দেখা গেল — এটা parser
বানায় না checker। AST-র lifecycle নিয়ে এটা কী প্রমাণ করে?
স্মরণ
-ast-dump-এ ImplicitCastExpr দেখা গেল — এটা parser
বানায় না checker। AST-র lifecycle নিয়ে এটা কী প্রমাণ করে?যে “AST” আমরা dump করি তা static না — semantic analysis insertions করে (implicit casts, default args instantiation)। Parse-time tree আর checked tree আলাদা snapshot। এজন্যই phase separation (AST/HIR…) explicit modeling — tree evolves, এবং evolution-টা documented data, accident না।
এরপর কী
এরপর কী
Tree দাঁড়িয়ে গেছে — কিন্তু এখনো প্রতিটা VarRef একটা অন্ধ string।
x কোন x? Inner scope-এর, outer-এর, নাকি undeclared typo?
পরের লেসনে Semantic Analysis: syntax-ঠিক-অর্থ-ভুল-ধরা —
- Scope stack symbol table: enter/exit-এ push/pop
- Declaration↔reference resolution — প্রতিটা VarRef-এর decl-link
- Two-pass strategy forward references-এর জন্য
- Precise error reporting — spans অবশেষে কাজে লাগবে
Build: ~80-line Resolver যেটা আমাদের AST walk করে প্রতিটা name bind করে আর line:col সহ সব semantic violation report করে।
আরও পড়ুন
- Crafting Interpreters, Chapter 5 — Representing Code — Robert Nystrom · AST node encoding-এর meta-model discussion — metaprogramming section আমাদের taxonomy-র ভিত্তি
- Compilers: Principles, Techniques, and Tools (2nd ed.), Chapter 2 — A Simple Syntax-Directed Translator — Aho, Sethi, Ullman, Lam (the Dragon Book) · Syntax-directed translation দিয়ে AST গঠনের ধ্রুপদি framework
- "Engineering a Compiler" (2nd ed.), Section 5.3–5.4 — Keith Cooper, Linda Torczon · IR হিসেবে tree-র জায়গা — graph-based IR-এর সাথে তুলনা