Semantic Analysis — syntax ঠিক, অর্থ ভুল ধরা
Semantic Analysis and Symbol Tables
Parse হওয়া tree-তে অর্থের প্রথম ছোঁয়া — symbol table, scoping, name resolution আর precise diagnostics।
আগে এটা বুঝি
এই program-টা আমাদের parser-এর চোখে perfectly valid:
let total = price + qty;
print(totl);
let total = 0;Grammar-এর প্রতিটা production মিলেছে। Tree সুন্দরভাবে গঠিত। তবু তিনটা মারাত্মক ভুল লুকিয়ে আছে:
priceকোথাও declare হয়নিtotlএকটা typo —totalনাtotalদুইবার declare হয়েছে একই scope-এ
এই ভুলগুলোর কোনোটাই context-free grammar জানে না — কারণ এগুলোর সবগুলোই context-এর সমস্যা: ফাইলের অন্য কোথাও কী আছে তার সাথে সম্পর্ক। Programming-languages module-এর syntax-vs-semantics লেসনে আপনি এই দুই layer-এর তাত্ত্বিক ভাগ দেখেছেন — আজ সেই semantics layer-টা compiler-এর ভেতরে machine বানাব।
এটাই compiler-এর সেই stage যেখানে প্রথমবার কোডের অর্থ ছোঁয়া হয় — parse করা structure-এ না, মানুষের intended meaning-এর সাথে মিলিয়ে।
আজকের প্রশ্ন: “নাম আর ঘোষণার মিল” — এই সাধারণ শোনাতা সমস্যাটা scoping rules যোগ হলে কেন এত কঠিন হয়ে যায়, আর এর industrial- strength সমাধান (symbol table) কেমন data structure?
মূল ধারণা
Grammar-এর বাইরে যা যা ধরতে হয়
Semantic analysis-এর checklist — ToyLang version:
| Check | Example | Grammar কেন অপার |
|---|---|---|
| Undeclared variable | print(x); (x নেই) | Token stream local, ফাইল-global knowledge নেই |
| Duplicate declaration | let x=1; let x=2; | Repetition grammar-এ legal |
| Use before definition | print(x); let x=1; | Order-insensitive grammar |
| Kind mismatch | x(); — x variable, function না | Node-type knowledge semantic |
| Scope violation | block-এর বাইরে থেকে inner var | Nesting context |
Type-related checks (bool কোথায় int দরকার) পরের লেসনের — আজ name-world-এর checks।
Symbol table — মূল data structure
Conceptually একটা mapping: name → declaration info:
entry:
name: "total"
kind: variable (ভবিষ্যতে: function/class)
declared: Span(3:5-3:9) ← error message-এর জন্য!
resolved: [reference list] ← কোন কোন VarRef এখানে এসেছেশেষ field-টা underrated: declaration তার সব references-এর link জানলে go-to-definition, rename refactoring, unused-variable warning — সব free। LSP servers ঠিক এই links-ই serve করে।
Scope stack — nesting-এর সমাধান
Lexical scoping-এর নিয়ম: innermost declaration জেতে। Data structure? একটা stack of tables:
enter block → push({})
exit block → pop()
lookup(name):
top-থেকে নিচে walk, first hit wins
declare(name):
top-এ ঢোকানোর আগে duplicate-in-scope checkTrace — এই program-এ:
{ push S1={}
let x = 1; S1: {x→d1}
{ push S2={}
print(x); lookup x: S2 miss → S1 hit d1 ✓
let x = 2; S2: {x→d2} ← shadowing!
print(x); lookup x: S2 hit d2 ✓ (prints 2)
} pop S2 ← d2 dies here
print(x); lookup x: S1 hit d1 ✓ (prints 1)
} pop S1Stack discipline-ই scoping-এর পুরো magic: pop হলেই inner declarations automatically মরে — lifetime management free।
Two-pass — forward reference-এর দাম
Single pass যথেষ্ট যখন rule: use-এর আগে declare (ToyLang, Go-র variables)। কিন্তু mutual recursion:
fn is_even(n) { if n==0 {true} else {is_odd(n-1)} }
fn is_odd(n) { if n==0 {false} else {is_even(n-1)} }is_even-এর body-তে is_odd resolve করতে হবে যেটা পরে
declare। Single-pass fail। Solution — দুই pass:
Pass 1 (collect): পুরো scope-এর declarations আগে টেবিলে
Pass 2 (resolve): এখন bodies walk করে references bindC-র পুরনো rule (“function declare before use”) ঠিক single-pass legacy — header files-এর prototype ceremony এই limitation-ই জন্ম দিয়েছিল। Java/Rust/Go functions mutually recursive by default — two-pass (বা lazy) resolution। ToyLang variables-এ strict use-after-let রাখব (single-pass), rule-টা নিজেই একটা language design choice — মনে রাখবেন।
Attribute grammars — framing
Theory এটাকে বলে attribute grammar: প্রতিটা node-এ synthesized attributes (children থেকে উপরে — যেমন expression-এর type) আর inherited attributes (parent/context থেকে নিচে — যেমন current scope)। আমাদের resolver-এ inherited attribute = scope stack parameter, synthesized = collected errors + resolution links। Attribute grammar formalism বড় ভাষায় directly ব্যবহৃত হয় না, কিন্তু vocabulary-টা analysis-র কাঠামো বর্ণনায় everywhere — পরের type-checking lesson- ও এই frame-এই পড়বে।
ভেতরে কী ঘটছে
Production symbol tables — কেন plain stack-of-hash না
আমাদের toy: list[dict]। rustc-র scale-এ সমস্যা দুইটা: memory
(per-block hash allocation) আর snapshot/revert (backtracking,
macro expansion retry)। Production designs:
Design A — stack of maps (toy, fine):
scopes: [{}, {}, ...]
Design B — single dense table + scope markers (real):
entries: flat vec, insertion order-এ
scopes: entry-index stack — pop-to-marker = revert O(1)
lookup: উল্টো direction linear scan (interned names hot-cache)
Design C — interning + ribs (rustc):
names → u32 intern id (string compare একবারই)
rib per scope-kind (normal/macro/module)Interning-টা লক্ষ্য করুন: identifier string একবার hash হয়, তারপর u32 compare — million-reference program-এ string comparison বাঁচে million বার।
Use-before-def — definite assignment-এর আত্মীয়
ToyLang rule simple: textual position-এ let আগে হতে হবে। কিন্তু control flow-এর ভেতরে?
if (cond) { let x = 1; }
print(x); ← x declared কি? cond-এর উপর নির্ভর!Declaration-level check pass করে (x visible), কিন্তু runtime-এ x নাও থাকতে পারে। এটা definite assignment analysis (Java- র famous flow check) — flow-sensitive semantic analysis: CFG walk করে প্রতিটা point-এ কোন var definitely-initialized। ToyLang-এ block-scoped let দিয়ে সমস্যাটা side-step করব (x block-এর ভেতরেই dead) — কিন্তু জানবেন production compilers-এ এটা separate mini-pass, dataflow module-এর পূর্বরূপ।
Error recovery — অনেক ভুল, এক run
Naive approach: প্রথম semantic error-এ throw। User experience? Fix-run-fix-run loop — প্রতি iteration একটা করে ভুল। Production contract: one run, all independent errors। Techniques:
১. Error node/flag: resolve fail হলে error-entry inject করে
পরে continue — same-name পরের references-এ repeated error
suppress (dedup by name+decl-site)
২. Cascade cut: `price` undeclared হলে তার type unknown → type
checker-এ derived errors ("unknown + int") report না করা —
Unknown type poison-pill pattern
৩. Recovery anchors: statement boundary-তে state reset — parse-
level panic mode-এর semantic siblingClang-রা এত দূর যায় যে invalid code-এও best-effort tree বানিয়ে type-check চালায় — IDE-র live feedback-এর জন্য mandatory।
উদাহরণ
Full resolution trace — spans সহ
Input:
let a = 1;
{
let b = a;
{
print(b);
}
print(c);
}
print(a);Step-by-step resolver trace:
| Event | Stack (top বামে) | Result |
|---|---|---|
let a (1:1) | [{}] | declare a@1:5 → top |
| enter block | [{}, {a}] | — |
let b (2:3) | [{}, {a,b}] | declare b@2:7 |
init a lookup | hit a@1:5 ✓ bind | |
| enter block | [{}, {}, {a,b}] | — |
print(b) (3:5) | walk: miss,miss,hit b@2:7 ✓ | |
| exit block | [{}, {a,b}] | — |
print(c) (5:3) | miss, miss → ERROR 5:9 | |
| exit block | [{}] | b dies |
print(a) (7:1) | hit a@1:5 ✓ |
Output:
error[sem]: undeclared name 'c' @5:9-5:10
note: no declaration named 'c' in any enclosing scopeদুইটা subtle বিষয় লক্ষ্য করুন:
b-এর span (2:7) থেকেprint(b)(3:5)-এর link — go-to-definition- এর raw materialc-error-এর পরেওprint(a)correctly resolve হলো — recovery working
ছবিতে
Scope nesting আর lookup path
┌─ module scope ────────────────────────────┐
│ g : 1:1 │
│ │
│ ┌─ fn f scope ────────────────────────┐ │
│ │ param n │ │
│ │ │ │
│ │ ┌─ while-body ─┐ ┌─ then-block ┐ │ │
│ │ │ i : 4:5 │ │ t : 7:7 │ │ │
│ │ │ lookup(i)✓ │ │ lookup(g)───┼──┼──┼──▶ hit (2 hops up)
│ │ │ lookup(z)✗──┼──┼──▶ ERROR │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────┘ │
└───────────────────────────────────────────┘- Name resolutiondeclaration↔reference linking; undeclared/duplicate/shadowing — এই লেসন
- Kind/shape checksvariable-কে call করা যায় না; arity mismatch
- Type checkingoperator signatures, conversions — পরের লেসন
- Flow checksdefinite assignment, unreachable code — dataflow-এর প্রিভিউ
নিজে চালিয়ে দেখুন
Python symtable module — production scope analysis হাতে-কলমে
ধাপ ১ — nested-scope snippet introspect:
import symtable
code = """
greeting = "hi"
def outer(x):
y = x + 1
def inner():
return y + z_local_fix # z intentionally missing
z = 10
return inner
class C:
attr = greeting
"""
t = symtable.symtable(code, "demo.py", "exec")
def walk(tab, depth=0):
print(" "*depth + f"[{tab.get_name()}] "
f"type={tab.get_type()}")
for s in tab.get_symbols():
flags = []
if s.is_global(): flags.append("global")
if s.is_local(): flags.append("local")
if s.is_free(): flags.append("FREE")
if s.is_parameter(): flags.append("param")
print(" "*depth + f" {s.get_name():12} {','.join(flags)}")
for c in tab.get_children():
walk(c, depth+1)
walk(t)Output:
[top] type=module
greeting local
outer local
C local
[outer] type=function
x local,param
y local
z local
inner local
[inner] type=function
y FREE ← outer-এর y! scope-walk ধরেছে
z_local_fix FREE
[C] type=class
attr localy: FREE — inner-এ y declared না, global-ও না; CPython-এর resolver
binding chain detect করেছে। Closure implementation (cell objects)
এই analysis-এর direct ফল।
ধাপ ২ — clang diagnostic positions পড়া:
/* scope.c */
int main(void) {
{
int hidden = 42;
}
printf("%d\n", hidden);
int dup = 1;
int dup = 2;
return 0;
}clang -Wall -fsyntax-only scope.c 2>&1
# macOS-এ printf-এর জন্য #include \<stdio.h> যোগ করুনOutput (truncated):
scope.c:6:20: error: use of undeclared identifier 'hidden'
scope.c:8:9: error: redefinition of 'dup'
scope.c:7:9: note: previous definition is hereদেখুন: (১) exact line:col — token spans কাজ করছে; (২) note: —
previous definition-এর pointer, ঠিক আমাদের resolved-links দিয়ে
সম্ভব; (৩) এক run-এ দুই error — recovery। আমাদের build এই format-
ই mimic করবে।
CPython নিজের compiler-এর জন্য exactly এই ধরনের scope-resolution pass চালায় (symtable module সেটার window); closure/free-variable detection আসে scope-walking থেকে।
নিজে বানান
ToyLang Resolver — scope stack, precise errors, full file
- Lesson 5-এর AST import করুন (toylang/ast.py)
- Resolver class: scopes list-of-dicts, errors list
- Program/block walk: enter-push, exit-pop
- LetStmt: duplicate-in-scope check, তারপর declare
- VarRef lookup: walk outward, miss হলে error record (recovery: continue)
- Resolved links collect করুন: decl-span -> [ref-spans]
- Expected-output assert tests: ৩টা failing program
toylang/resolver.py:
"""ToyLang semantic analysis pass 1: name resolution.
Needs: toylang/ast.py (lesson 5).
"""
from dataclasses import dataclass, field
from toylang.ast import (Program, LetStmt, Print, IfStmt,
WhileStmt, ExprStmt, VarRef,
BinOp, UnaryOp)
@dataclass
class SemanticError:
msg: str
line: int
col: int
end_col: int
def render(self):
return (f"error[sem]: {self.msg} "
f"@{self.line}:{self.col}-{self.end_col}")
@dataclass
class Decl:
name: str
span: tuple # (line, col, end_col)
refs: list = field(default_factory=list) # ref spans
class Resolver:
"""Two responsibilities:
1. every VarRef -> its Decl (or error)
2. every duplicate/undeclared -> SemanticError
"""
def __init__(self):
self.scopes = [{}] # scope stack; top = last
self.errors = []
self.links = {} # decl key -> Decl (go-to-definition)
# ---- scope machinery ------------------------------------------
def _push(self): self.scopes.append({})
def _pop(self): self.scopes.pop()
def _lookup(self, name):
for scope in reversed(self.scopes):
if name in scope:
return scope[name]
return None
def _declare(self, name, span):
top = self.scopes[-1]
if name in top:
prev = top[name]
self._err(
f"duplicate declaration of '{name}'",
span, note_prev=prev.span)
return
d = Decl(name, span)
top[name] = d
self.links[span] = d
return d
def _err(self, msg, span, note_prev=None):
line, col, end_col = span
self.errors.append(SemanticError(msg, line, col, end_col))
if note_prev:
pl, pc, pe = note_prev
self.errors.append(SemanticError(
f"note: previous declaration @{pl}:{pc}", pl, pc, pe))
# ---- walking ----------------------------------------------------
def resolve(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: lambda x: self.expr_stmt(x)}
m[type(s)](s)
def let_stmt(self, s):
self.expr(s.init) # init-এর refs আগে
self._declare(s.name, _span(s))
def print_stmt(self, s):
self.expr(s.arg)
def expr_stmt(self, s):
self.expr(s.expr)
def if_stmt(self, s):
self.expr(s.cond)
self.block(s.then)
if s.else_ is not None:
self.block(s.else_)
def while_stmt(self, s):
self.expr(s.cond)
self.block(s.body)
def block(self, stmts):
self._push()
for s in stmts:
self.stmt(s)
self._pop()
# ---- expressions --------------------------------------------------
def expr(self, e):
if isinstance(e, VarRef):
d = self._lookup(e.name)
if d is None:
self._err(f"undeclared name '{e.name}'",
_span(e))
return
d.refs.append(_span(e)) # resolution link!
elif isinstance(e, BinOp):
self.expr(e.left); self.expr(e.right)
elif isinstance(e, UnaryOp):
self.expr(e.operand)
# literals: nothing
def _span(node):
return (node.span.line, node.span.col, node.span.end_col)Test harness — ফাইলের শেষে:
def _test():
from toylang.ast import NumLit, Span as Sp
def P(*stmts): return Program(tuple(stmts))
def let(n, v):
return LetStmt(Sp(1, 1, 9), n, NumLit(Sp(1, 9, 9), v))
# Test 1: clean program — zero errors + link built
r = Resolver()
r.resolve(P(let("x", 1),
Print(Sp(2, 1, 10), VarRef(Sp(2, 7, 8), "x"))))
assert r.errors == []
assert r.links[(1, 1, 9)].refs == [(2, 7, 8)]
# Test 2: undeclared + duplicate together, one run
r = Resolver()
r.resolve(P(
let("a", 1),
let("a", 2), # duplicate
Print(Sp(3, 1, 10), VarRef(Sp(3, 7, 8), "c")) # undeclared
))
msgs = [e.render() for e in r.errors]
assert any("duplicate declaration" in m for m in msgs)
assert any("undeclared name 'c'" in m for m in msgs)
# Test 3: use-before-def violates single-pass rule
r = Resolver()
r.resolve(P(Print(Sp(1, 1, 10),
VarRef(Sp(1, 7, 8), "late")),
let("late", 5)))
assert any("undeclared" in e.msg for e in r.errors)
# Test 4: shadowing across scopes is NOT an error
r = Resolver()
r.resolve(P(let("x", 1)))
r._push()
r._declare("x", (9, 1, 10))
r._pop()
assert not r.errors
print("all resolver tests passed")
if __name__ == "__main__":
_test()Expected output:
$ python3 toylang/resolver.py
all resolver tests passedআর একটা demo main যোগ করলে example section-এর program-টার exact error format দেখবেন:
demo = """
let a = 1;
{
let b = a;
print(c);
}
print(a);
"""error[sem]: undeclared name 'c' @4:9-4:10নিজে বাড়ান:
- Unused-variable warning: যে Decl-এর refs empty — report (warning vs error decision document করুন)।
- Two-pass mode flag: functions-এর জন্য collect-then-resolve — mutual recursion enable করুন।
- Go-to-definition CLI: input
file:line:colনিয়ে target decl-span print করুন — links map এখানেই কাজে লাগবে।
বাস্তব সিস্টেমে
Real resolvers — কার ভেতরে কী আছে
rustc resolver। “Rib”-ভিত্তিক scope stack — প্রতিটা rib একটা scope-kind (module, macro expansion, normal block)। Macro hygiene (মacro-র internal names বাইরে leak না করা) resolver-level machinery — syntax-এর বাইরের সবচেয়ে সুন্দর semantic feature-গুলোর একটা।
Clang Sema। DeclContext tree — namespace/function/class প্রতিটা
একটা context, qualified lookup (std::vector) walk করে chain।
C++ overload resolution-এর সাথে মিশে billion-line-scale caching
problem।
Java। Three-phase spec-mandated resolution (imports → members → method bodies) — ভাষার spec-ই resolution algorithm prescribe করে, compiler-independent determinism guarantee-র জন্য।
CPython symtable। আপনি experiment-এ দেখেছেন — closure/free-var detection এই pass-ই decide করে bytecode-এ LOAD_DEREF emit হবে কি না। Resolution = runtime architecture।
LSP/go-to-definition। প্রতিটা jump-to-definition একটা stored resolution-link query। rust-analyzer-রা incremental resolution রাখে — edit করলে affected scope-টুকুই re-resolve হয়।
Rollup/Webpack scope hoisting। Bundler-রা ES modules-এর scope analysis করে বোঝে কোন imports pure-rename — তারপর flatten করে smaller bundle। Web performance-এর পেছনেও symbol resolution।
যে ভুলগুলো সবাই করে
“Symbol table মানে একটা global hash map।”
Scoping-ই এর মূল — flat map দিয়ে let x=1; { let x=2; print(x);}
wrongly resolve হবে (last-write-wins)। Stack/blocks mandatory;
production-এ markers/interning উপরে দেখেছেন। Flat map শুধু single-
scope DSL-এ যথেষ্ট।
“প্রথম semantic error-এ compile থামানোই সঠিক behavior।”
User-এর সময় নষ্ট fix-run cycle-এ। Professional contract: এক run- এ maximum independent signal — তাই error-node injection, cascade suppression, dedup। Clang/rustc এক invalid file-এ কয়েক dozen meaningful error দেয়; naive throw দিত ২০%।
“Shadowing always bug — ban করা উচিত।”
Rust idiomatic code shadowing-heavy (let x = parse(...); let x = validate(x);) — rebind-with-refinement pattern। C-র -Wshadow
warn-only ঠিক balance। Policy language-designer-এর; compiler-এর
কাজ policy-কে mechanically express করা, moral judgment না।
“Resolution আর type checking এক pass-এ একসাথে করাই efficient।”
Merged pass tempting, কিন্তু type checking resolution-এর output (resolve links) দরকার — circular dependency। Separation gives cleaner errors, testable stages, আর parallelism option (per-block resolution parallelizable)। Premature merge premature coupling।
“Use-before-def শুধু pedantic style rule।”
Runtime cost real: JS var hoisting-এর undefined-surprises, Python
UnboundLocalError — সব unresolved-flow-এর ব্যাহক। Compile-time catch
(definite assignment) এই whole bug-class eliminate করে — Java-র
এই এক check-ই new-language adoption argument ছিল অনেকের কাছে।
বুঝেছেন কি না দেখুন
1Scope stack-এ enter/exit আর lookup/declare — চার operation-এর
amortized complexity কত? কেন?
স্মরণ
Push/pop O(1)। Lookup worst-case O(depth × avg-table-size) — shallow nesting-এ effectively O(1); declare-এ duplicate-check O(table size) top-scope-এ। Hash-backed scopes-এ typical constant। Production interning constant-factor কমায়, asymptotic একই।
2let x=1; { let x=2; print(x); } print(x); — দুই print-এ কী বসবে
আর কোন decl-এ bind হবে?
প্রয়োগ
let x=1; { let x=2; print(x); } print(x); — দুই print-এ কী বসবে
আর কোন decl-এ bind হবে?Inner print → inner x (S-top hit, value 2); outer print → outer x (inner popped, value 1)। Shadowing-across-scope — ToyLang allows, same-scope duplicate হলে এটা error হতো।
3Mutual recursion কেন single-pass resolver-এ fail করে? Two-pass
exactly কী move করে?
যুক্তি
Single pass body-walk করার সময় পরের declaration এখনো table-এ নেই — is_even→is_odd lookup misses। Two-pass শুধু ordering উল্টায়: সব declarations আগে collect, তারপর bodies — তখন forward name present। Move হলো declare-time বনাম resolve-time-এর gap।
4price undeclared — এরপর price + 1-এ type checker কী করবে?
Cascade suppression-এর design লিখুন।
ডিজাইন
price undeclared — এরপর price + 1-এ type checker কী করবে?
Cascade suppression-এর design লিখুন।Resolver Unknown/poison type attach করে error একবার report করে। Type checker-এ rule: যেকোনো operand Unknown হলে result Unknown, কোনো নতুন error না — propagation silent। শুধু root cause একবার দেখায়, derived dozen নয়। Trade-off: genuine secondary errors মাঝে মাঝে suppress হয় — acceptable noise trade।
5Python-এর y: FREE (experiment) আর C-র nested access — মধ্যে ফারাক
কোথায়? কেন Python-এ special FREE category লাগে?
যুক্তি
y: FREE (experiment) আর C-র nested access — মধ্যে ফারাক
কোথায়? কেন Python-এ special FREE category লাগে?Python-এ functions-এর scope dynamic (call-time) — closure চাইলে compiler-কে আগেই জানতে হয় কোন vars cell-এ থাকবে (LOAD_DEREF vs LOAD_FAST)। FREE = cross-function lexical link, runtime indirection mandatory। C-র lexical nesting শুধু blocks (functions-এর ভেতরে nested function না) — static offsets enough, কোনো capture নেই। Language semantics resolution strategy dictate করে।
6Decl.refs list-টা কেন রাখলাম? তিনটা consumer feature নাম করুন।
স্মরণ
(১) go-to-definition (ref-span → decl-span query); (২) rename refactoring (refs-এর সব site update); (৩) unused-variable warnings (empty refs); bonus: find-all-references LSP request। Links = analysis pass-এর durable product, error message-এর চেয়ে বেশি দীর্ঘায়ু।
এরপর কী
এরপর কী
Names bound — এখন values-এর শ্রেণি: types। পরের লেসনে Type Checking —
- Checker আসলে একটা ছোট evaluator যে value-র বদলে type compute করে
- Operator signature tables, coercion ladder
if-condition must-be-bool জাতীয় positional constraints- Type errors as structured data — spans আবার কাজে
আর programming-languages module-এর type-systems লেসনের theory (static/dynamic, soundness) এবার concrete code হয়ে ফিরবে। Build: ToyLang-এর পূর্ণ TypeChecker + ৬টা failing test program।
আরও পড়ুন
- Modern Compiler Implementation (Java/ML ed.), Chapter 5 — Semantic Analysis — Andrew W. Appel · Symbol table abstraction আর escaper/finder pattern-এর classic treatment
- "Engineering a Compiler" (2nd ed.), Chapter 4 — Contextual Analysis — Keith Cooper, Linda Torczon · Context-sensitive checks-কে syntax-directed framework-এ বসানোর engineering view
- Compilers: Principles, Techniques, and Tools (2nd ed.), Section 2.7 — Symbol Tables — Aho, Sethi, Ullman, Lam (the Dragon Book) · Symbol table data structure-এর foundational discussion