Parsing — recursive descent, Pratt, আর LR-এর ছায়া
Parsing
Grammar থেকে running parser — hand-written recursive descent আর Pratt parsing-এর practical mastery, পেছনে LR-এর theory।
আগে এটা বুঝি
গত দুই লেসনে আমরা 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 করে:
| Operator | Binding power | Associativity |
|---|---|---|
|| | 10 | left |
&& | 20 | left |
== != | 40 | left |
< > \<= >= | 50 | left |
+ - | 60 | left |
* / % | 70 | left |
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_bpcomparison: same-power operator এসে থামে — left associativity free (a-b-cভেতরের call বাইরেরটাকে রেখে দেয়)- passing
bp-1for 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):
| Step | Call | Action | State |
|---|---|---|---|
| ১ | expr(0) | nudge INT(1) → left=1 | rest: + 2 * 3 |
| ২ | loop | + bp 60 > 0 ✓ consume | rest: 2 * 3 |
| ৩ | right = expr(60) | ||
| ৪ | expr(60) | nudge INT(2) → left=2 | rest: * 3 |
| ৫ | loop | * bp 70 > 60 ✓ consume | rest: 3 |
| ৬ | right = expr(70) → 3 | ||
| ৭ | left = (2 * 3) | rest: end | |
| ৮ | * আর নেই → return (2*3) | ||
| ৯ | back in step ২-এর loop | EOF → exit | left = 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-assocBottom-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 candidateDot এগোয় 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 —
- Error detect করে report (position সহ!)
- Token skip করে synchronizing point খুঁজে যাও — statement
boundary (
;,}, keyword-দের FIRST) - 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-র নিয়মের সাথে মেলে- Recursive Descentdecision প্রতিটা function-entry-তে, FIRST set দিয়ে predict
- Prattdecision প্রতিটা operator-token-এ, binding power দিয়ে
- LR/LALRdecision table-এ precomputed — stack-state machine, runtime-এ lookup
নিজে চালিয়ে দেখুন
Bison/yacc দিয়ে conflict তৈরি করা, পড়া, আর ঠিক করা
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 | heady.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(" ",""))))
EOFLark 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।
নিজে বানান
ToyLang Pratt Parser — precedence table, indented AST
- গত লেসনের Lexer import করুন (toylang/lexer.py)
- PRECEDENCE dict: প্রতিটা infix Tok → binding power
- expr(min_bp): nudge → led-loop — আগের লেসনের trace অনুযায়ী
- unary/prefix handling: MINUS আর BANG প্রিফিক্সে 90 bp
- parenthesized expr: primary-তে recursion
- 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')
(andandTree পড়ুন নিচ থেকে উপরে: 3-1 সবার ভেতরে, তারপর *2, তারপর
+1, তারপর \<=10, শেষে &&ok — evaluation order ঠিক এভাবেই।
নিজে বাড়ান:
^যোগ করুনINFIX_BP-তেright_assoc=Trueদিয়ে — test:2^3^2tree ডান-spine হবে।- Statement layer যোগ করুন:
let/if/while/print/blockdispatch — example section-এর ছক অনুযায়ী। - 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।
বুঝেছেন কি না দেখুন
1Recursive 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।
2Pratt table-এ +-এর bp 60, ==-এর 40। a == b + 1 parse হবে
কীভাবে? Trace করুন।
প্রয়োগ
+-এর 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-তে
+ ভেতরে, == বাইরে।
3Left associativity Pratt-এ কোন line থেকে আসে? Right-assoc ^
যোগ করতে কী change?
যুক্তি
^
যোগ করতে কী 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 নেই।
4Shift/reduce trace-এ F + F দেখে E → E + F reduce করা হলো।
Parser কীভাবে জানল ওই moment-টাই? Naive “top-of-stack দেখো”-তে
কী ভুল হতো?
প্রয়োগ
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।
6Clang-রা 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