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

Semantic Analysis — syntax ঠিক, অর্থ ভুল ধরা

Semantic Analysis and Symbol Tables

Parse হওয়া tree-তে অর্থের প্রথম ছোঁয়া — symbol table, scoping, name resolution আর precise diagnostics।

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

  • Syntax-valid কিন্তু meaning-invalid program-এর চার শ্রেণির ভুল (undeclared, duplicate declaration, use-before-def, kind mismatch) চিনে বলতে পারবেন grammar কেন এগুলো ধরতে পারে না
  • Scope-stack symbol table design করে block enter/exit-এ push/pop দিয়ে lexical scoping implement করতে পারবেন
  • Shadowing rules আর name lookup walk-outward semantics ব্যাখ্যা করে বলতে পারবেন কোন reference কোন declaration-এ bind হয়
  • Two-pass strategy (collect declarations, then resolve) কখন দরকার তা forward reference example দিয়ে দেখাতে পারবেন
  • Error recovery দিয়ে এক run-এ multiple independent semantic error report করানোর design করতে পারবেn — cascading noise ছাড়া

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

আগে এটা বুঝি

এই program-টা আমাদের parser-এর চোখে perfectly valid:

let total = price + qty;
print(totl);
let total = 0;

Grammar-এর প্রতিটা production মিলেছে। Tree সুন্দরভাবে গঠিত। তবু তিনটা মারাত্মক ভুল লুকিয়ে আছে:

  1. price কোথাও declare হয়নি
  2. totl একটা typo — total না
  3. 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:

CheckExampleGrammar কেন অপার
Undeclared variableprint(x); (x নেই)Token stream local, ফাইল-global knowledge নেই
Duplicate declarationlet x=1; let x=2;Repetition grammar-এ legal
Use before definitionprint(x); let x=1;Order-insensitive grammar
Kind mismatchx(); — x variable, function নাNode-type knowledge semantic
Scope violationblock-এর বাইরে থেকে inner varNesting 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 check

Trace — এই 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 S1

Stack 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 bind

C-র পুরনো 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 sibling

Clang-রা এত দূর যায় যে 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:

EventStack (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 lookuphit 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 material
  • c-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     │  │  │
│  │  └──────────────┘  └──────────────┘  │  │
│  └─────────────────────────────────────┘  │
└───────────────────────────────────────────┘
Lookup সবসময় innermost-থেকে outermost — তীরচিহ্ন হলো search path। Hit প্রথম match-এ; শূন্য পথ মানে undeclared error।
Semantic analysis-এর স্তরগুলো — কোনটা কী ধরে
  1. Name resolutiondeclaration↔reference linking; undeclared/duplicate/shadowing — এই লেসন
  2. Kind/shape checksvariable-কে call করা যায় না; arity mismatch
  3. Type checkingoperator signatures, conversions — পরের লেসন
  4. Flow checksdefinite assignment, unreachable code — dataflow-এর প্রিভিউ

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

EXPERIMENT

Python symtable module — production scope analysis হাতে-কলমে

Linux / macOS (python3 only)· ২০ মিনিট

ধাপ ১ — 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       local

y: 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 থেকে।

নিজে বানান

BUILD IT

ToyLang Resolver — scope stack, precise errors, full file

Python · ●●●○○
  1. Lesson 5-এর AST import করুন (toylang/ast.py)
  2. Resolver class: scopes list-of-dicts, errors list
  3. Program/block walk: enter-push, exit-pop
  4. LetStmt: duplicate-in-scope check, তারপর declare
  5. VarRef lookup: walk outward, miss হলে error record (recovery: continue)
  6. Resolved links collect করুন: decl-span -> [ref-spans]
  7. 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

নিজে বাড়ান:

  1. Unused-variable warning: যে Decl-এর refs empty — report (warning vs error decision document করুন)।
  2. Two-pass mode flag: functions-এর জন্য collect-then-resolve — mutual recursion enable করুন।
  3. 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 ছিল অনেকের কাছে।

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

1

Scope 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 একই।

2

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 হতো।

3

Mutual 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।

4

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।

5

Python-এর 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 করে।

6

Decl.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