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

Parsing — recursive descent, Pratt, আর LR-এর ছায়া

Parsing

Grammar থেকে running parser — hand-written recursive descent আর Pratt parsing-এর practical mastery, পেছনে LR-এর theory।

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

  • Grammar-এর প্রতিটা nonterminal থেকে একটা করে function বানিয়ে recursive descent parser লিখতে পারবেন
  • FIRST/FOLLOW set intuition দিয়ে বলতে পারবেন predictive choice কোথায় সম্ভব আর backtracking কখন লাগে
  • Binding power table define করে Pratt algorithm দিয়ে যেকোনো precedence/associativity-র expression grammar parse করতে পারবেন
  • Shift/reduce stack trace পড়ে LR parser-এর চলা দেখাতে পারবেন এবং bison-এর shift/reduce conflict message-এর মানে ব্যাখ্যা করতে পারবেন
  • Panic-mode error recovery design করে parser crash-এর বদলে multiple useful error report করাতে পারবেn

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

আগে এটা বুঝি

গত দুই লেসনে আমরা tokens বানিয়েছি আর grammar লিখেছি। Grammar এখনো কাগজে-কলমে — এবার ওটাকে জীবন্ত code করতে হবে: এমন program যে token stream খায়, tree উগরে দেয়।

Parsing-এর জগতে দুইটা মহাদেশ আছে:

  • Top-down (LL family) — root থেকে শুরু করে নিচে নামা। Hand-written হয়, পড়া যায়, debug সহজ। Recursive descent আর তার expression-specialized রূপ Pratt parsing এখানেই।
  • Bottom-up (LR family) — leaf থেকে উপরে ওঠা। Table-driven, generated (yacc/bison), বড় grammar handle করে — কিন্তু ভেতরটা মানুষ পড়ে না।

Industry-এর secret: production compiler-দের বিশাল অংশ top-down hand-written (Clang, Go, Rust, V8, javac)। কারণ একটাই — error message quality আর maintainability। তবু LR-এর ধারণা না জানলে PostgreSQL-এর gram.y বা bison-এর conflict warning দেখে ঘাবড়াবেন।

আজ দুই মহাদেশই ঘুরব — LL-এ হাঁটব, LR-এর ছায়ায় দাঁড়াব। আর build-এ ToyLang-এর full expression parser লিখব Pratt দিয়ে — আমাদের compiler-এর দ্বিতীয় module।

আজকের প্রশ্ন: token stream থেকে tree বানানোর decision গুলো কোথায় নেওয়া হয়, আর decision না নিয়েও কাজ চালানোর (LR) উপায়টা কেমন?

মূল ধারণা

Recursive descent — grammar-এর সোজা translation

আগের লেসনের layered grammar:

expr   → term (("+" | "-") term)*
term   → factor (("*" | "/" | "%") factor)*
factor → INT | IDENT | "(" expr ")"

Recursive descent-এর নিয়ম এক লাইনে: প্রতিটা nonterminal = একটা function, sequence = consecutive calls, alternation = if/else on lookahead, repetition = while loop:

def expr():                    # expr → term (("+"|"-") term)*
    node = term()
    while peek() in (PLUS, MINUS):
        op = advance()
        node = (op, node, term())
    return node

def factor():                  # factor → INT | "(" expr ")"
    if peek() == INT:   return eat(INT).value
    if peek() == LPAREN:
        eat(LPAREN); e = expr(); eat(RPAREN)
        return e
    error("expected expression")

Grammar-এর structure আর function-এর structure এক-একটা mirror — এজন্যই hand-written parser পড়তে সহজ: grammar document-ই code।

Predictive choice আর FIRST set

Alternation-এ কোন branch? Predictive parser এক token look- ahead-এ decide করে — সেটা সম্ভব যখন প্রতিটা branch-এর FIRST set (token যেগ দিয়ে branch শুরু হতে পারে) disjoint:

factor-এর branches:
  INT branch     FIRST = {INT}
  paren branch   FIRST = {LPAREN}
  → disjoint ✓ এক token-এই decide হয় → LL(1)

FIRST sets overlap হলে দুই উপায়: grammar transform (left factoring — গত লেসন) বা more lookahead। Backtracking হলো third option: এক branch try, fail করলে input position reset করে পরের branch — সহজ কিন্তু exponential worst case, আর error position হারায়। Production parser-রা প্রায় কখনো backtrack করে না; PEG parsers (Python 3.9+) ordered-choice semantics দিয়ে একই effect deterministic ভাবে পায়।

FOLLOW set আর ε-decision

Empty production (ε) কখন choose করব — এটাই FOLLOW-এর কাজ। Dangling-else-এর factored grammar মনে করুন:

if_rest → "else" stmt | ε

else না দেখলে ε-path — কিন্তু ঠিক করাটা FOLLOW(if_rest)-এর উপর নির্ভর করে: যদি else নিজেই FOLLOW-set-এ থাকে (nested if!), conflict — আর এখানেই convention ঢোকে: ε বেছে নিলে else কাছের if-এ attach হয় (shift-over-reduce, C-semantics)। FIRST/FOLLOW mechanics এখানে language policy-র সাথে মিশে যায়।

Pratt — expression parsing-এর সুইস ছুরি

Layered grammar (expr/term/factor) কাজ করে — তবে প্রতিটা নতুন operator-level মানে নতুন nonterminal, নতুন function, নতুন layer। ১০ precedence level-এর ভাষা (C!) মানে ১০-layer grammar।

Vaughan Pratt (1973) দেখালে: expression parsing-এর জন্য layers অপ্রয়োজনীয় — একটা function যথেষ্ট, যদি প্রতিটা operator নিজের binding power declare করে:

OperatorBinding powerAssociativity
||10left
&&20left
== !=40left
< > \<= >=50left
+ -60left
* / %70left
unary - !90 (prefix)—

Algorithm-এর পুরো core:

expr(min_bp):
    left = nudge(first token)            # literal/unary/paren
    while peek is infix AND bp(peek) > min_bp:
        op = advance()
        right = expr(bp(op))             # left-assoc: pass own bp
                                         # right-assoc: pass bp-1
        left = (op, left, right)
    return left

দুইটা জাদু-সংখ্যা মাত্র:

  • > min_bp comparison: same-power operator এসে থামে — left associativity free (a-b-c ভেতরের call বাইরেরটাকে রেখে দেয়)
  • passing bp-1 for right-assoc (যেমন future ^): tie-তে ভেতরে ঢোকে — right associativity free

Precedence table এখন data, code না — নতুন operator মানে একটা table row। এই একটা কারণেই Pratt সব hand-written compiler-এর expression story।

ভেতরে কী ঘটছে

Pratt trace — 1 + 2 * 3 step by step

Table-টা মাথায় রেখে full trace (min_bp শুরুতে 0):

StepCallActionState
১expr(0)nudge INT(1) → left=1rest: + 2 * 3
২loop+ bp 60 > 0 ✓ consumerest: 2 * 3
৩right = expr(60)
৪expr(60)nudge INT(2) → left=2rest: * 3
৫loop* bp 70 > 60 ✓ consumerest: 3
৬right = expr(70) → 3
৭left = (2 * 3)rest: end
৮* আর নেই → return (2*3)
৯back in step ২-এর loopEOF → exitleft = 1+(2*3)

Step ৫-ই পুরো মজা: *-এর 70, ভেতরের min_bp 60 — 70 > 60 তাই multiplication যোগের ডান হাতের ভেতরে ঢুকে গেল। Precedence কোনো special-case না, একটা integer comparison।

Associativity trace — 8 - 3 - 2:

expr(0): left=8; see -(60)>0 → right=expr(60)
  expr(60): left=3; see -(60) > 60? NO → return 3
left = (8-3); see -(60)>0 → right=expr(60)=2
result: ((8-3)-2) = 3 ✓ left-assoc

Bottom-up shift/reduce — উল্টো দিকের যাত্রা

LR parser tree বানায় leaf থেকে root-এ। দুইটা মাত্র action:

  • Shift: পরের token stack-এ push
  • Reduce: stack-এর উপরের কয়েক item একটা nonterminal-এ collapse (production-এর right side মিললে)

1 + 2-এর journey:

stack          input      action
[ ]            1 + 2 $    shift 1
[1]            + 2 $      reduce: INT → F
[F]            + 2 $      shift +
[F +]          2 $        shift 2
[F + 2]        $          reduce: INT → F
[F + F]        $          reduce: E → E + F    ← handle পাওয়া গেল!
[E]            $          accept

“Handle” মানে: stack-এর উপরে production-এর RHS এমন ক্রমে বসা। LR parser-এর পুরো বুদ্ধি হলো কখন shift কখন reduce সেটা আগে থেকে জানা — precomputed DFA state-এর table থেকে।

LR(1) automaton — one screen intuition

State = item-এর set। Item হলো production যার ভেতরে একটা dot: “E → E • + F” মানে “E + F দেখছি, E আর + শেষ”।

State 0 (শুরু):                '+' খেয়ে goto:
  E' → • E                     State 1:
  E  → • E + F                   E' → E •      ← accept candidate
  E  → • F                       E  → E • + F  ← '+' shift করবে
  F  → • INT
  F  → • (E )                 INT খেয়ে goto:
                                 State 2: F → INT •  ← reduce candidate

Dot এগোয় shift-এ, production পূর্ণ হলে reduce। Conflict হয় ঠিক দুই জায়গায়:

  • Shift/reduce: এক state-এ দুইটাই valid (dangling else-এর classic case)
  • Reduce/reduce: দুই production একসাথে পূর্ণ

LALR(1) (yacc/bison default): canonical LR(1)-এর states merge করে same-core গুলো — table ১০× ছোট, খুব অল্প extra conflict। এই trade-off-ই bison-কে practical বানিয়েছে।

Error recovery basics

Naive parser প্রথম ভুলে exception — user-এর জন্য অসবর্ণ। Production approach: panic mode —

  1. Error detect করে report (position সহ!)
  2. Token skip করে synchronizing point খুঁজে যাও — statement boundary (;, }, keyword-দের FIRST)
  3. Parse continue — আরও error ধরা হয়

চারটা craft-rule:

  • Cascading suppression: এক node error-flagged হলে তার children- এর derived errors report না করা
  • Error productions: সাধারণ typo-র জন্য explicit grammar rule (stmt → expr ";" | expr ";" ";" ";") — GCC/Clang করে
  • Insertion assumption: missing ; assume করে synthetic token বসানো, error বলা “expected ’;’” — parse থামে না
  • Never lose the tree: error AST nodes রেখে semantic analysis পর্যন্ত যাওয়া — IDE-র জন্য mandatory

উদাহরণ

Full statement parse — dispatch থেকে tree

Expression শিখলাম; statement level-এ একটা dispatcher বসে:

parse_stmt():
  LET    → let_stmt:  "let" IDENT "=" expr ";"
  IF     → if_stmt:   "if" "(" expr ")" block ("else" block)?
  WHILE  → while_stmt:"while" "(" expr ")" block
  PRINT  → print_stmt:"print" "(" expr ")" ";"
  LBRACE → block:     "{" stmt* "}"
  else   → expr_stmt: expr ";"

Input:

let x = 2;
if (x * 3 \<= 10) { print(x); } else { print(0); }

Tree (indented, ├─ শাখা-চিহ্ন সহ):

program
├─ let x = 2
└─ if
   ├─ cond: (\<=) (* x 3) 10
   ├─ then-block
   │   └─ print(x)
   └─ else-block
       └─ print(0)

Dispatch-টা FIRST-set logic-ই: প্রতিটা statement-kind-এর distinct leading keyword — perfect LL(1)। Expression-এর ভেতরে Pratt চলে। দুই technique পাশাপাশি, এক parser-এ — ঠিক production compilers- এর মতো (Clang: RD for statements, Pratt-ish for expressions)।

ছবিতে

Shift/reduce-এর ফাঁদ — dangling else conflict

stack: [... IF ( cond ) stmt ]      input: ELSE ...

রাস্তা ১ — SHIFT: else-টা stack-এ, ভেতরের if-এর সাথে জোড়া লাগবে
           → nearest-if attachment (C semantics)

রাস্তা ২ — REDUCE: ভেতরের if-stmt এখনই complete ধরে বাইরের
           if-এর সাথে else যাবে → outer attachment

bison resolution: %nonassoc ELSE / %prec বা default = shift
→ shift means: else কাছের if-এ — ভাগ্য ভালো, C-র নিয়মের সাথে মেলে
`if (x) if (y) A(); else B();` — else এসে পৌঁছালে parser-এর দুই রাস্তা। এই state-ই bison-এর shift/reduce warning-এর জন্মদাতা।
Parser families — কে কোথায় decision নেয়
  1. Recursive Descentdecision প্রতিটা function-entry-তে, FIRST set দিয়ে predict
  2. Prattdecision প্রতিটা operator-token-এ, binding power দিয়ে
  3. LR/LALRdecision table-এ precomputed — stack-state machine, runtime-এ lookup

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

EXPERIMENT

Bison/yacc দিয়ে conflict তৈরি করা, পড়া, আর ঠিক করা

macOS (built-in yacc) / Linux (apt install bison)· ২৫ মিনিট

tiny.y লিখুন — deliberately ambiguous:

%{
#include \<stdio.h>
int yylex(void);
void yyerror(const char *s) { fprintf(stderr, "%s\n", s); }
%}
%token IF COND STMT ELSE
%%
prog   : stmt ;
stmt   : IF COND stmt
       | IF COND stmt ELSE stmt
       | STMT ;
%%

Generate করুন আর conflict দেখুন:

yacc -v tiny.y          # macOS; Linux: bison -v tiny.y
# stderr: "conflicts: 1 shift/reduce"
ls y.output && grep -n "conflict" y.output | head

y.output-এ conflict-এর exact state পাবেন:

state N
    IF COND stmt . ELSE stmt
    IF COND stmt .
    ELSE  shift, and go to state M     ← রাস্তা ১
    ELSE  [reduce using rule 2]        ← রাস্তা ২ (bracketed = resolved)

এখন fix — declaration যোগ করুন:

%nonassoc IF COND STMT
%nonassoc LOWER_THAN_ELSE
%nonassoc ELSE
%%
... (same rules, but:)
stmt : IF COND stmt %prec LOWER_THAN_ELSE
     | IF COND stmt ELSE stmt ;
yacc -v tiny.y && echo OK    # কোনো conflict message নেই

Conflict gone — কারণ precedence declaration বলে দিল: ELSE-এর precedence বেশি, তাই shift জিতবে (= nearest-if)।

pip route (optional, cross-platform GUI-less playground):

pip install lark
python3 - <<'EOF'
from lark import Lark
amb = Lark('''start: s\ns: "i" s "e" s | "i" s | "x"\n''',
            parser="lalr", start="start")
print("parsed:", list(amb.iter_parse("iixe x".replace(" ",""))))
EOF

Lark LALR mode-ও same-family machinery — তবে conflict reporting bison-এর মতো verbose না; bison-ই শেখার জন্য best।

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

Ambiguous grammar tool-কে mechanically detect করায় (shift/reduce conflict count), আর conflict resolution declaration দিয়ে deterministic behavior enforce করা যায় — LR-এর ভয়টা মূলত unfamiliarity।

নিজে বানান

BUILD IT

ToyLang Pratt Parser — precedence table, indented AST

Python · ●●●○○
  1. গত লেসনের Lexer import করুন (toylang/lexer.py)
  2. PRECEDENCE dict: প্রতিটা infix Tok → binding power
  3. expr(min_bp): nudge → led-loop — আগের লেসনের trace অনুযায়ী
  4. unary/prefix handling: MINUS আর BANG প্রিফিক্সে 90 bp
  5. parenthesized expr: primary-তে recursion
  6. Indented AST printer + assert-based tests

toylang/parser.py — পুরো ফাইল (lexer dependency ছাড়া সম্পূর্ণ logic; lexer গত লেসনের ফাইল থেকে import):

"""ToyLang Pratt parser: token stream -> expression AST.
Needs: toylang/lexer.py (lesson 2).
"""
from toylang.lexer import Lexer, Tok

# infix operator -> (binding_power, right_assoc?)
INFIX_BP = {
    Tok.OROR:    (10, False),
    Tok.ANDAND:  (20, False),
    Tok.EQEQ:    (40, False), Tok.BANGEQ: (40, False),
    Tok.LT:      (50, False), Tok.GT: (50, False),
    Tok.LTE:     (50, False), Tok.GTE: (50, False),
    Tok.PLUS:    (60, False), Tok.MINUS: (60, False),
    Tok.STAR:    (70, False), Tok.SLASH: (70, False),
    Tok.PERCENT: (70, False),
}
PREFIX_RBP = 90          # unary - আর ! — সবার চেয়ে টাইট


class ParseError(Exception):
    def __init__(self, msg, tok):
        super().__init__(f"{msg} @{tok.line}:{tok.col}")


class Parser:
    """Pratt parser over a fixed token buffer."""

    def __init__(self, src: str):
        self.toks = Lexer(src).tokens()
        self.i = 0

    # ---- token cursor helpers -------------------------------------
    def peek(self) -> Tok:
        return self.toks[self.i].kind

    def cur(self):
        return self.toks[self.i]

    def advance(self):
        tok = self.toks[self.i]
        self.i += 1
        return tok

    def expect(self, kind: Tok):
        if self.peek() != kind:
            raise ParseError(
                f"expected {kind.name}, found "
                f"{self.cur().kind.name}", self.cur())
        return self.advance()

    # ---- the Pratt core -------------------------------------------
    def expr(self, min_bp: int = 0):
        left = self.nud()
        while True:
            k = self.peek()
            entry = INFIX_BP.get(k)
            if entry is None:
                break
            lbp, right_assoc = entry
            if lbp \<= min_bp:          # tie → বাইরেরটার, left-assoc
                break
            op = self.advance()
            rbp = lbp - 1 if right_assoc else lbp
            right = self.expr(rbp)
            left = (k.name.lower(), left, right)
        return left

    def nud(self):
        """Prefix position: literals, names, parens, unary."""
        tok = self.advance()
        k = tok.kind
        if k is Tok.INT:
            return int(tok.value)
        if k is Tok.STRING:
            return ("str", tok.value)
        if k is Tok.TRUE:
            return True
        if k is Tok.FALSE:
            return False
        if k is Tok.IDENT:
            return ("var", tok.value)
        if k is Tok.MINUS:
            return ("neg", self.expr(PREFIX_RBP))
        if k is Tok.BANG:
            return ("not", self.expr(PREFIX_RBP))
        if k is Tok.LPAREN:
            inner = self.expr(0)
            self.expect(Tok.RPAREN)
            return ("group", inner)
        raise ParseError(f"unexpected {k.name}", tok)

    def parse(self):
        tree = self.expr(0)
        if self.peek() is not Tok.EOF:
            raise ParseError("trailing input", self.cur())
        return tree


# ---------- indented AST printer -----------------------------------
def dump(node, indent=""):
    pad = indent + "| "
    if isinstance(node, tuple):
        head = node[0]
        print(f"{indent}({head}")
        for kid in node[1:]:
            dump(kid, pad)
        print(f"{indent})")
    elif isinstance(node, int) and not isinstance(node, bool):
        print(f"{indent}{node}")
    else:
        print(f"{indent}{node!r}")


def _test():
    P = Parser
    assert P("1+2*3").parse() == ("plus", 1, ("star", 2, 3))
    assert P("8-3-2").parse() == ("minus", ("minus", 8, 3), 2)
    assert P("--5").parse() == ("neg", ("neg", 5))
    assert P("!true || false").parse() == (
        "oror", ("not", True), False)
    assert P("(1+2)*3").parse() == (
        "star", ("group", ("plus", 1, 2)), 3)
    assert P("a \<= b == true").parse() == (
        "eqeq", ("lte", ("var", "a"), ("var", "b")), True)

    for bad in ["1 +", "(1+2", "*3"]:
        try:
            P(bad).parse()
            raise AssertionError(f"no error for {bad!r}")
        except ParseError:
            pass

    print("all parser tests passed")


if __name__ == "__main__":
    _test()
    dump(Parser("1 + 2 * (3 - 1) \<= 10 && ok").parse())

Expected output:

$ python3 toylang/parser.py
all parser tests passed
(andand
| (lte
| | (plus
| | | 1
| | | (star
| | | | 2
| | | | (group
| | | | | (minus
| | | | | | 3
| | | | | | 1
| | | | )
| | | )
| | )
| | 10
| )
| ('var', 'ok')
(andand

Tree পড়ুন নিচ থেকে উপরে: 3-1 সবার ভেতরে, তারপর *2, তারপর +1, তারপর \<=10, শেষে &&ok — evaluation order ঠিক এভাবেই।

নিজে বাড়ান:

  1. ^ যোগ করুন INFIX_BP-তে right_assoc=True দিয়ে — test: 2^3^2 tree ডান-spine হবে।
  2. Statement layer যোগ করুন: let/if/while/print/block dispatch — example section-এর ছক অনুযায়ী।
  3. Panic recovery: ParseError ধরে sync-to-; করে errors list collect করুন — এক run-এ সব ভুল দেখান।

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

Production parser-রা আসলে কী চালায়

Clang (C/C++)। Hand-written recursive descent + Pratt-style expression precedence table। কারণ: world-class diagnostics আর incremental parsing (IDE) — table-driven LR-এ দুটোই কষ্টকর।

Go (go/parser)। Pure recursive descent, stdlib-readable। Design goal: একজন engineer এক afternoon-এ পুরো parser বুঝে ফেলবে।

rustc। Hand-written RD; expression-এ Pratt (rustc ভেতরে parse_expr_with binding-power machinery)।

Python 3.9+ PEG parser। LL-এর বদলে PEG (ordered choice, packrat memoization) — grammar এখন Grammar/Tokens-এ data হিসেবে, new syntax যোগ করা সহজ হয়েছে। Trade-off: memory (memoization)।

PostgreSQL, Bash, Ruby (Ripper)। bison/LALR still alive — বড় declarative grammar যেখানে grammar-first development natural।

tree-sitter। Editor ecosystem-এ GLR variant — error tolerance first-class: half-written code-ও parse tree দেয়, তাই VS Code/ Neovim highlighting ভাঙে না।

V8/JavaScriptCore। Hand-written RD + lazy parsing — function body প্রয়োজন না হলে pre-parse only। Parser performance সরাসরি page-load latency।

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

“LR মৃত — সবাই এখন hand-written।”

Hand-written RD expression-heavy languages-এ dominant ঠিক, কিন্তু bison/LALR প্রতিদিন production-এ চলে: PostgreSQL-এর প্রতিটা query, আপনার shell-এর প্রতিটা command, countless config DSL। Tool choice language-এর property — SQL-এর মতো grammar-এ RD লিখলে হাজার-line boilerplate।

“Pratt শুধু expression-এর জন্য।”

Pratt-এর abstraction হলো “token-এর prefix আর infix role” — statement-ও এভাবে চলে: let prefix-role, = infix-role assignment operator। Crafting Interpreters-এর পুরো jlox parser-ই Pratt-flavored dispatch। কিছু parser (Rust-analyzer) পুরো language Pratt-style event-driven বানায়।

“Backtracking ছাড়া parsing করা যায় না — নাহলে ভুল branch-এ ঢুকে যাব।”

Predictive parser-রা FIRST/FOLLOW guarantee দিয়ে backtrack-free correct হয় — grammar properly factored থাকলে কোনো wrong-branch entry-ই ঘটে না। Backtracking দরকার হয় শুধু unfactored grammar না-transform করে চালাতে চাইলে। Cost: exponential worst case — production-এ প্রায় কেউ দাম দেয় না।

“More lookahead always better — LL(k), k বড় হোক।”

Lookahead বাড়ানো prediction power বাড়ায় কিন্তু decision table exponentially ফুলে ওঠে আর error message ঘোলাটে হয় (“expected one of 47 things”)। Practical sweet spot: LL(1) + clean factoring বা Pratt। Python-এর old parser-ও LL(1)-ish ছিল; বড় jump ছিল k না — paradigm (PEG)।

“Conflict warning harmless — bison default resolve করেই দেয়।”

Default resolution (shift) মাঝে মাঝে ঠিক হয় (dangling else!), কিন্তু accidentally ঠিক — আপনি verify করেননি। Silent mis-parse এর পরে surface করে semantically wrong program-এ, debugging nightmare। Professional practice: zero unexplained conflicts — প্রতিটা conflict হয় ঠিক করা, না documented-intentional।

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

1

Recursive descent-এ nonterminal, sequence, alternation, repetition — প্রতিটা grammar construct-এর code equivalent কী?

স্মরণ

Nonterminal → function; sequence → consecutive statements/calls; alternation → if/elif on lookahead (FIRST sets); repetition → while loop; ε-production → do-nothing path chosen by FOLLOW logic। Grammar document আর code এক-এ-one mapping — এটাই RD-র readability superpower।

2

Pratt table-এ +-এর bp 60, ==-এর 40। a == b + 1 parse হবে কীভাবে? Trace করুন।

প্রয়োগ

expr(0): nud a; see ==(40)>0 → rhs=expr(40): nud b; see +(60) > 40 → rhs=expr(60)=1; return (b+1)। Result: a == (b+1) ✓ arithmetic tighter than comparison। উল্টো হলে a + 1 == b-তে + ভেতরে, == বাইরে।

3

Left associativity Pratt-এ কোন line থেকে আসে? Right-assoc ^ যোগ করতে কী change?

যুক্তি

Line: if lbp \<= min_bp: break — tie-তে (lbp == min_bp) ভেতরের call থেমে দেয়, sibling operator বাইরের loop-এ যায় → left fold। Right-assoc: entry-তে flag, pass rbp = lbp - 1 ভেতরে — ভেতরের call-এ tie আর break করে না, ডান-spine তৈরি হয়। দুই case-ই pure integer arithmetic — কোনো special-case নেই।

4

Shift/reduce trace-এ F + F দেখে E → E + F reduce করা হলো। Parser কীভাবে জানল ওই moment-টাই? Naive “top-of-stack দেখো”-তে কী ভুল হতো?

প্রয়োগ

Precomputed automaton state বলে দেয় — state-এ item E → E • + F complete ছিল আর next token ($/operator) reduce-এর FOLLOW-এ। Naive top-of-stack matching ভুল reduce করত অনেক জায়গায় — যেমন F + F যদি বড় production X → F + F * F-এর অংশ হতো, premature reduce precedence ভাঙত। Context-sensitivity-ই LR-এর পুরো মেধা।

5

আপনার ToyLang parser-এ panic-mode recovery যোগ করবেন। Sync points কোনগুলো বেছে নেবেন, কেন? Cascading error কীভাবে suppress করবেন?

ডিজাইন

Sync points: ; (statement terminator), } (block end), statement- leading keywords (LET/IF/WHILE/PRINT) — কারণ এদের পরে grammar fresh restart নেয়। Suppression: error-node flag propagate — flagged subtree- র ভেতরে নতুন error গণনা না করা, বা dedup by span-overlap। Goal: এক run-ে maximum independent signal, minimum noise।

6

Clang-রা hand-written RD বেছেছে কেন, অথচ PostgreSQL bison? Decision factors গুলো নাম করে বলুন।

যুক্তি

Factors: (১) error message quality — RD-তে প্রতিটা failure-point customizable, LALR table opaque; (২) incremental/partial parsing (IDE) — RD-র control flow resumable; (৩) grammar size/stability — SQL বিশাল stable → generation pays off; C++ জটিল evolving → hand-control wins; (৪) team workflow — grammar-file DSL vs plain code। Universal best নেই; requirements-driven।

এরপর কী

এরপর কী

Parser এখন tuple-tree দেয় — কিন্তু tuple দিয়ে compiler লেখা অসহনীয়: কোন shape কোথায়, কে জানবে? পরের লেসনে tree-টাকে proper data structure বানাব: AST design।

  • Node taxonomy: Expr/Stmt hierarchy কীভাবে ভাগ হয়
  • Variant encoding: OOP visitor বনাম tagged unions — Rust enum match exhaustiveness compiler-কেই ভুল ধরতে দেয়
  • প্রতিটা node-এ source span — diagnostics-এর investment
  • AST বনাম CST — কোনটা কী রাখে

Build: ToyLang AST Python dataclasses দিয়ে + pretty-printer যেটা round-trip করে (print(parse(x)) == x)। Compiler-এর কেন্দ্রীয় data structure দাঁড়িয়ে যাবে।

আরও পড়ুন

  • "Top Down Operator Precedence" (1973) — Vaughan Pratt · Pratt parsing-এর মূল paper — আধুনিক expression parsing-এর প্রায় সব implementation-এর পূর্বপুরুষ
  • Crafting Interpreters, Chapter 6 — Parsing Expressions — Robert Nystrom · Pratt technique-এর সবচেয়ে জনপ্রিয় modern walkthrough — আমাদের build-এর ধরনেরই
  • Compilers: Principles, Techniques, and Tools (2nd ed.), Chapter 4 — Syntax Analysis (§4.6–4.8) — Aho, Sethi, Ullman, Lam (the Dragon Book) · LR(k)/LALR table construction-এর full treatment — এই লেসনের one-screen intuition-এর গভীর version