Grammar ও Ambiguity — BNF, derivation, আর দ্ব্যর্থক বাক্য
Grammars and Ambiguity
Token stream-কে structure দেওয়ার formal ভাষা — grammar; আর grammar-এর সবচেয়ে বিপজ্জনক ফাঁদ ambiguity চেনা ও নির্মূল করা।
আগে এটা বুঝি
গত লেসনের শেষে আমাদের হাতে token-এর একটা flat list:
INT(1) PLUS INT(2) STAR INT(3)প্রশ্ন: 1 + 2 * 3 মানে (1 + 2) * 3 = 9? নাকি 1 + (2 * 3) = 7?
Tokens-এ উত্তর নেই। Structure-এর নিয়ম কোথাও লেখা থাকতে হবে —
আর সেই লেখার ভাষাটাই হলো grammar।
Natural language-এ আমরা ambiguity-র সাথে বড় হয়েছি। English-এ “I saw the man with the telescope” — telescope দিয়ে দেখলাম, নাকি telescope-wala man-কে দেখলাম? বাংলায় “বাবা ছেলেকে ডাকলেন” সহজ, কিন্তু “খাওয়ার জন্য ভাত” জাতীয় বাক্যও দ্বিগুণ পড়া যায়। Human language tolerance রাখে — listener context দিয়ে ঠিক করে নেয়।
Computer-এর কোনো context নেই, কোনো “সম্ভবত” নেই। Compiler-কে প্রতিটা input-এর ঠিক একটা structure বের করতেই হবে — অথচ grammar-টা এমনভাবে লিখতে হবে যেন দ্বিগুণ পড়ার সুযোগই না থাকে।
আজকের প্রশ্ন: grammar formal-ভাবে লেখা হয় কেমন করে? Ambiguity চেনা যায় কী করে? আর ধরা পড়লে কীভাবে সারানো যায়?
মূল ধারণা
BNF — grammar-এর গণিত notation
Backus-Naur Form, 1960 — ALGOL report থেকে। তিন রকম জিনিস:
| জিনিস | Convention | ToyLang উদাহরণ |
|---|---|---|
| Nonterminal | lowercase name | expr, term, factor |
| Terminal | quoted | "+", "let", INT |
| Production | → বা : | term → term "*" factor |
ToyLang-এর expression grammar-এর ভুল, ambiguous version:
expr → expr "+" expr (১)
| expr "-" expr (২)
| expr "*" expr (৩)
| INT (৪)চারটা alternative — পড়তে সহজ, ঠিক এজন্যই বিপজ্জনক। এই grammar নিজেই বলছে “যোগ বাদ দিলে বাকি সব সমান” — কোনো precedence নেই, কোনো associativity নেই।
Derivation আর parse tree
Derivation = start symbol থেকে শুরু করে বারবার কোনো nonterminal-কে তার একটা production দিয়ে replace করা, যতক্ষণ না সব terminal।
Leftmost derivation: সবসময় বাম দিকের nonterminal আগে expand।
1+2*3-এর দুইটা আলাদা leftmost derivation এই grammar-এ আছে:
Derivation A (প্রথম expr → expr + expr): Derivation B (আগে expr*expr):
expr expr
→ expr + expr → expr * expr
→ expr + expr * expr → INT * ... না, ভেতরে:
→ INT(1) + expr * expr → expr + expr * expr
→ INT(1) + INT(2) * expr → INT(1) + INT(2) * INT(3)
→ INT(1) + INT(2) * INT(3) (ভিন্ন ক্রমে, ভিন্ন tree)প্রতিটা derivation একটা parse tree paint করে। আর B-grammar- এর মূল অপরাধ: একই string-এর দুইটা ভিন্ন parse tree —
(+) = 7 (*) = 9
/ \ / \
1 (*) (+) 3
/ \ / \
2 3 1 2Tree evaluate bottom-up করলে বামটা দেয় 1+(2*3)=7, ডানটা
(1+2)*3=9। Compiler কোনটা বেছে নেবে — undefined। একই program
দুই compiler-এ দুই উত্তর দেয় — এর চেয়ে খারাপ bug programming-
এ বিরল।
Ambiguity-র তিনটা classic রূপ
১. Precedence: 1+2*3 — দেখলেন উপরে। Fix জানা: * / %
উঁচু level-এ, + - নিচে।
২. Associativity: a - a - a — (a-a)-a = -a না a-(a-a)=a?
Minus left-associative। কিন্তু আমাদের ambiguous grammar-এ দুটো
tree-ই valid। Exponentiation উল্টো: 2^3^2 মানে 2^(3^2)=512,
right-associative।
৩. Dangling else: সবচেয়ে বিখ্যাত:
if (a) if (a)
if (b) if (b)
f(); f();
else else
g(); g();else-টা কোন if-এর? Grammar বলে না। C-র resolution: else
সবসময় কাছের unmatched if-এর — ডান parse। কিন্তু এই decision
grammar-এ না লিখে documentation-এ রাখা হলে প্রতিটা parser writer
নিজের মতো guess করবে।
Transformation ১ — precedence layering
Ambiguity সরানোর প্রথম হাতিয়ার: level ভাগ করা। প্রতিটা precedence level একটা নতুন nonterminal:
expr → term (("+" | "-") term)* ← lowest precedence
term → factor (("*" | "/" | "%") factor)*
factor → INT | IDENT | "(" expr ")" ← highest (parens escape hatch!)তিনটা property free-তে আসে:
*বেশি deeply nested হয় — higher precedence automatic- Same-level chain বাম ঝোঁকে (
term term op term op...) — left associativity automatic - Parentheses
factor-এ ঢুকে যেকোনো level override করে
এটাই “precedence climbing into grammar levels” — আর এই layered structure-ই পরের লেসনের recursive descent parser-এর function structure ঠিক করে দেয়: প্রতিটা nonterminal = একটা function।
Transformation ২ — left recursion elimination
expr → expr "+" term | termএটা left-recursive: expr নিজের production-এর সবচেয়ে বামে।
Recursive descent parser-এ এটা মানে infinite loop — expr()
শুরু হতেই আবার expr() ডাকবে, stack overflow। LL-family parsers
(left-to-right, leftmost) সব left recursion hate করে।
Mechanical fix — নতুন nonterminal যেটা suffix repeat ধরে:
আগে: expr → expr "+" term | term
পরে: expr → term expr'
expr' → "+" term expr' | ε (ε = empty)Semantics অক্ষত (একই strings accept হয়), কিন্তু tree-র shape বদলায় — repetition এখন ডান দিকে chain। Left associativity recovery করতে হয় accumulation দিয়ে (পরের লেসনে দেখব)।
Transformation ৩ — left factoring
Dangling-else grammar:
stmt → "if" "(" expr ")" stmt "else" stmt
| "if" "(" expr ")" stmt
| otherদুই alternative-এর common prefix লম্বা। Predictive parser-এর
অসুবিধা: "if" দেখে কোন branch নেবে decide করা যায় না। Fix —
common part বের করে আনা:
stmt → "if" "(" expr ")" stmt if_rest | other
if_rest → "else" stmt | εএখন "if" দেখলে confidently প্রথম production; if_rest-এ
গিয়ে "else" দেখা গেলে সেখানে decide। আর ε-choice-টাই dangling
else-এর resolution point — FOLLOW-set logic বলে দেয় default কী
হবে (else attach করবে কাছের if-এ, C-semantics)।
ভেতরে কী ঘটছে
Grammar class map — কোন grammar কোন parser
সব CFG সব parser-এ চলে না। Rough map (পরের লেসনে বিস্তারিত):
| Class | Parser family | Constraint | Real user |
|---|---|---|---|
| Regular | lexer DFA | nesting নেই | tokens |
| LL(1) | predictive recursive descent | left recursion ✗, factored ✓ | hand-written parsers |
| LL(k)/PEG | backtracking/packrat | unlimited lookahead cost | Python 3.9+ PEG |
| LALR(1) | yacc/bison table | conflicts resolve করতে হয় | PostgreSQL, Bash |
| LR(1) | canonical table | বিশাল table | theory benchmark |
| GLR | fork-and-merge | slow but handles ambiguity | tree-sitter, bison %glr |
LL vs LR-এর core পার্থক্য এক লাইনে: LL আগে expand করে পরে match (top-down, derivation predict), LR আগে match করে পরে reduce (bottom-up, tree root-এ শেষে পৌঁছায়)। এজন্যই LL left recursion-এ মরে (নিজের wait নিজে), আর LR-এর জন্য left recursion কোনো সমস্যাই না — উল্টো natural।
yacc/bison-এ ambiguity resolution — declaration দিয়ে
Unambiguous grammar লেখার বিকল্প: ambiguous grammar রেখে rules declare করা:
%left '+' '-'
%left '*' '/' '%'
%%
expr : expr '+' expr | expr '-' expr | expr '*' expr | INT ;bison conflict detect করে declared precedence দিয়ে resolve করে। Trade-off: grammar ছোট থাকে, কিন্তু correctness-এর দায়িত্ব grammar- থেকে shift হয়ে declaration-এ — ভুল declare করলে silent wrong trees। C++-এর মতো জটিল ভাষায় bison-ও হাল ছাড়ে — Clang hand-written recursive descent বেছেছে ঠিক এজন্য।
উদাহরণ
a - a - a — দুই tree, এক verdict
Concrete numbers দিয়ে: 10 - 4 - 3। Ambiguous grammar-এ:
Tree L (left assoc): Tree R (right assoc):
(-) (-)
/ \ / \
(-) 3 10 (-)
/ \ / \
10 4 4 3
= (10-4)-3 = 3 = 10-(4-3) = 9Layered grammar-এ এখন derive করি — left associativity কোথা থেকে আসে দেখুন:
expr ⇒ term (("+"|"-") term)* — repetition list বাম-ভিত্তিক
⇒ [term₁] [- term₂] [- term₃]
⇒ ((term₁ − term₂) − term₃) ← বাম থেকে foldRepetition operator * (zero-or-more) একটা list produce করে,
আর evaluation convention বলে list fold left-to-right। Grammar
layering + list folding = associativity settled।
Dangling else — দুই tree হাতে
if (x) { if (y) A(); } else B(); // intended: outer if-এ elseBraces ছাড়া লিখলে if (x) if (y) A(); else B(); — ambiguous
grammar দুই tree দেয়: inner-attach (C semantics: else B() তখনই
চলবে x true, y false!) আর outer-attach। C standard choose করে
inner — “else matches nearest unmatched if”. Python সমস্যাটা
eliminate করে indentation দিয়ে; Ada explicit end if; তিন
design, এক সমস্যা।
ছবিতে
Layered grammar cascade
input: 1 + 2 * 3
expr ────────────────────────────▶ term ── + ── term
│ │
▼ ▼
factor term
│ │
▼ ▼
[1] factor * factor
│ │
▼ ▼
[2] [3]
Tree (bottom-up read = evaluation order):
(+)
/ \
[1] (*)
/ \
[2] [3]
= 1 + (2*3) = 7 ✓ গণিতের নিয়ম মিললLeft recursion elimination — before/after
BEFORE (left-recursive): AFTER (right-recursive suffix):
expr → expr "+" term expr → term expr'
| term expr' → "+" term expr'
| ε
LL call sequence: LL call sequence:
expr() → expr() → expr() ... expr() → term() → expr'()
💥 stack overflow expr'(): peek "+"
consume, term(), expr'()
peek EOF → ε pathRecursion direction-ই এখানে সব: left-recursion top-down-এ self-call before progress (deadlock), right-recursion আগে progress তারপর recurse (fine)। Bottom-up LR parser-এ উল্টোটা সত্য।
নিজে চালিয়ে দেখুন
Ambiguous grammar-এর দুই parse tree হাতে আঁকা, তারপর pyparsing দিয়ে prove
ধাপ ১ — কাগজে আঁকুন (হ্যাঁ, আসল কাগজ): 8 - 3 - 2-এর দুই parse
tree ambiguous grammar E → E - E | INT থেকে। Verify: বাম tree
দেয় 3, ডান tree দেয় 7। তারপর if a then if b then S else S
(dangling else)-এর দুই tree।
ধাপ ২ — pyparsing ইনস্টল (explicitly):
pip install pyparsing # macOS/Linux উভয়ে; venv ব্যবহার ভালোধাপ ৩ — ambiguous grammar simulate করা pyparsing-এ directly সম্ভব না (ও নিজেই deterministic), তাই আমরা দুই deterministic grammar-এর ভিন্ন trees দেখব — যা ambiguity-র দুই interpretation represent করে:
from pyparsing import Word, nums, oneOf, Forward, Group
INT = Word(nums)
# Grammar A: right-assoc minus (উল্টো নিয়ম — demonstration-এর জন্য)
def make_assoc(side):
E = Forward()
atom = INT | Group("(" + E + ")")
if side == "left":
E <<= atom + (oneOf("- +") + atom)[...]
else:
E <<= (atom[...]) # simplified demo
return E
# সোজা route: infixNotation ব্যবহার করে precedence declare
from pyparsing import infixNotation, opAssoc
expr = infixNotation(INT, [
(oneOf("* /"), 2, opAssoc.LEFT),
(oneOf("+ -"), 2, opAssoc.LEFT),
])
for src in ["1+2*3", "8-3-2", "(1+2)*3"]:
print(src, "=>", expr.parse_string(src).as_list())Expected output:
1+2*3 => [['1', '+', ['2', '*', '3']]] # * deeper = higher prec
8-3-2 => [['8', '-', '3'], '-', '2'] # hmm! flat list?
(1+2)*3 => [[['1', '+', '2']], '*', '3']8-3-2-এর flat output-টা pyparsing-এর grouping behavior — LEFT
assoc same-op chain এক group-এ flatten হয়। Fold order তবু left;
নিজের ৩০-লাইন tree printer (build section-এ) দিয়ে real nested
shape দেখবেন।
ধাপ ৪ — pip ছাড়া fallback: pure-python hand trace। Ambiguous grammar-এর দুই derivation manually encode করুন:
# দুইটা ভিন্ন tree, একই string "8-3-2"
t_left = ["-", ["-", 8, 3], 2] # (8-3)-2 = 3
t_right = ["-", 8, ["-", 3, 2]] # 8-(3-2) = 7
eval_ = lambda t: t if isinstance(t, int) else (
eval_(t[1]) - eval_(t[2]))
print(eval_(t_left), eval_(t_right)) # 3 7 — দুই interpretation!এই দুই line-ই ambiguity-র পুরো সার: structure differentiates, string doesn’t.
একই string-এ ambiguous grammar genuinely দুই valid tree দেয়, আর layered/infixNotation grammar deterministic একটা tree দেয় — ambiguity একটা grammar property, language property না।
নিজে বানান
Precedence-correct expression grammar + 30-line tree printer
- Ambiguous expr grammar-কে three-layer EBNF-এ rewrite করুন (expr/term/factor)
- গত লেসনের Lexer import করুন (toylang/lexer.py)
- প্রতিটা nonterminal = একটা function — recursive descent preview
- Generic tree-printer লিখুন (30 লাইন) — indent দিয়ে nested tuple print
- Prove করুন: 1+2*3, 8-3-2, (1+2)*3 তিনটার tree shape সঠিক
Grammar — আমাদের official ToyLang expression grammar, module জুড়ে এটাই থাকবে:
# ToyLang expression grammar (precedence-correct, unambiguous)
#
# expr → term (("+" | "-" | "||" ...) term)* [lowest]
# term → unary (("*" | "/" | "%") unary)*
# unary → ("!" | "-") unary | primary [highest prefix]
# primary → INT | STRING | TRUE | FALSE | IDENT
# | "(" expr ")"
#
# Properties proven by this build:
# * binds tighter than + (1+2*3 = 1+(2*3))
# - is left-associative (8-3-2 = (8-3)-2)
# parens override everything ((1+2)*3)
# unary minus nests (--5 == 5)toylang/miniparse.py — পুরো ফাইল:
"""ToyLang expression-only parser + tree printer.
Needs: toylang/lexer.py from the previous lesson.
"""
from toylang.lexer import Lexer, Tok
class ParseError(Exception):
pass
class Parser:
def __init__(self, src):
self.toks = Lexer(src).tokens()
self.i = 0
def peek(self):
return self.toks[self.i].kind
def eat(self, kind):
tok = self.toks[self.i]
if tok.kind != kind:
raise ParseError(
f"expected {kind.name}, got {tok.kind.name} "
f"@{tok.line}:{tok.col}")
self.i += 1
return tok
# expr → term (("+"|"-"|"||") term)*
def expr(self):
node = self.term()
while self.peek() in (Tok.PLUS, Tok.MINUS, Tok.OROR):
op = self.toks[self.i]; self.i += 1
node = (op.value, node, self.term())
return node
# term → unary (("*"|"/"|"%") unary)*
def term(self):
node = self.unary()
while self.peek() in (Tok.STAR, Tok.SLASH, Tok.PERCENT):
op = self.toks[self.i]; self.i += 1
node = (op.value, node, self.unary())
return node
# unary → ("!"|"-") unary | primary
def unary(self):
if self.peek() in (Tok.BANG, Tok.MINUS):
op = self.eat(self.peek())
return ("u" + op.value, self.unary())
return self.primary()
# primary → INT | TRUE | FALSE | IDENT | "(" expr ")"
def primary(self):
k = self.peek()
if k is Tok.INT or k is Tok.IDENT or k is Tok.STRING:
return self.eat(k).value
if k is Tok.TRUE:
self.eat(Tok.TRUE); return True
if k is Tok.FALSE:
self.eat(Tok.FALSE); return False
if k is Tok.LPAREN:
self.eat(Tok.LPAREN); e = self.expr()
self.eat(Tok.RPAREN); return ("paren", e)
raise ParseError(f"unexpected {k.name}")
# ---------- generic tree printer (the promised ~30 lines) ----------
def render(node, indent=""):
"""Nested tuples/lists -> ASCII art tree."""
if not isinstance(node, (tuple, list)):
return [indent + repr(node)]
label = node[0] if isinstance(node[0], str) and len(node) >= 1 else "?"
kids = node[1:] if isinstance(node, tuple) else node
out = [indent + f"({label})"]
for kid in kids:
out += render(kid, indent + " ")
return out
def show(src):
print(f"\n{src}")
for line in render(Parser(src).expr()):
print(line)
def _test():
assert Parser("1+2*3").expr() == ("+", "1", ("*", "2", "3"))
assert Parser("8-3-2").expr() == ("-", ("-", "8", "3"), "2")
assert Parser("(1+2)*3").expr() == ("*", ("paren", ("+", "1", "2")), "3")
assert Parser("--5").expr() == ("u-", ("u-", "5"))
print("all grammar tests passed")
if __name__ == "__main__":
_test()
show("1+2*3")
show("8-3-2")Expected output:
$ python3 toylang/miniparse.py
all grammar tests passed
1+2*3
(+)
'1'
(*)
'2'
'3'
8-3-2
(-)
(-)
'8'
'3'
'2'দ্বিতীয় tree-টা দেখুন — ভেতরের (-) বামে, মানে (8-3)-2:
left associativity structurally proven, comment-এ claim নয়।
নিজে বাড়ান:
^power operator যোগ করুন — right-associative, unary-র চেয়ে নাকি কম tight? (-2^2=-4মানে^unary-র নিচে।)- Comparison layer (
==,!=,<,>…) expr আর term-এর মাঝে বসান — ToyLang full grammar-এর জন্য লাগবেই। show()-কে eval করার mini-evaluator লিখুন — tree walk করে value বের করুন,8-3-2দেয়3কি না verify করুন।
বাস্তব সিস্টেমে
Grammar battles — বাস্তব ভাষাগুলোতে
C standard (ISO/IEC 9899)। Appendix A-তে পুরো grammar LR-friendly layered form-এ; dangling else explicitly resolved with prose rule “otherwise, the next token is…”। Standard-ই admits ambiguity resolution needs prose — grammar alone যথেষ্ট না সবসময়।
Python PEP 617 (2019-2020)। Guido-রা CPython-এর old
LL(1)-ish parser replace করল PEG (Parsing Expression Grammars)
দিয়ে — কারণ new syntax (match) LL(1)-এ fit করত না। Grammar
notation change = language evolution capability change।
PostgreSQL। SQL dialect এত বড় যে hand-written impossible-ish;
gram.y (bison) চলে — হাজার-line grammar, conflict warnings carefully
audited। SQL-এর SELECT ... FROM ... WHERE clause structure
LALR-এর জন্য বানানো।
tree-sitter grammars। Editor tooling GLR/GLL ব্যবহার করে — incomplete/buggy code-ও parse করতে হয় IDE-তে, error recovery first-class। Ambiguity এখানে feature: multiple interpretations score করে best নেওয়া হয়।
Bash/Yacc heritage। আপনি প্রতিদিন bison-generated parser ব্যবহার করেন — bash, zsh, Apache config, Ruby (Ripper), PHP। LALR era এখনো জীবিত।
যে ভুলগুলো সবাই করে
“Ambiguous grammar মানে ভাঙা language — বড় ভুল করেছে designer।”
Ambiguity grammar-এর property, language-টার না। C-র dangling-else grammar ambiguous, কিন্তু C-র semantics perfectly defined (nearest-if rule)। Designers ইচ্ছা করে ambiguous grammar + external resolution rules নেয় — grammar readable থাকে। সমস্যা হয় তখনই যখন resolution rule undocumented।
“EBNF BNF-এর more powerful version।”
EBNF (parentheses, repetition *, option [ ]) pure notation
sugar — প্রতিটা EBNF construct mechanical BNF-এ expand হয়
(nonnerminal যোগ করে)। Expressive power identical: দুটোই exactly
context-free languages। পার্থক্য readability, power না।
“Precedence আর associativity একই জিনিস।”
Precedence cross-operator: * vs + কে আগে। Associativity same-
operator: a-b-c কোন দিকে fold। আলাদা সমস্যা, আলাদা mechanism —
layered levels precedence দেয়, repetition-fold direction (বা Pratt-
এ binding power asymmetry) associativity দেয়। দুটোই না মিললে
grammar অসম্পূর্ণ।
“Parse tree আর AST একই।”
Parse tree (derivation tree) grammar-এর প্রতিটা intermediate
nonterminal ধরে রাখে — term, factor layers সহ, parentheses
সহ। AST হলো semantic essence: layers collapse, punctuation drop,
grouping nodes simplify। পরের পরের লেসনের মূল বিষয় AST — কিন্তু
distinction এখনই clear রাখা দরকার: grammar আপনার implementation
artifact, AST আপনার design artifact।
“Tool conflict রিপোর্ট করলে grammar necessarily ambiguous।”
bison-এর shift/reduce warning অনেক সময় ambiguity না — grammar LL/ LALR-class constraint violation-ও তা-ই। আর উল্টো দিকটাও সত্য: no-conflict ≠ unambiguous (ambiguity undecidable মনে রাখুন)। Conflict message reading একটা skill — পরের লেসনের experiment-এ হাতে-কলমে practice করব।
বুঝেছেন কি না দেখুন
1Terminal, nonterminal, production — আপনার ToyLang grammar থেকে
একটা করে উদাহরণ দিন।
স্মরণ
Terminal: "+" (আর INT, IDENT — lexer-produced kinds);
nonterminal: expr, term, factor, unary; production:
term → unary (("*" | "/" | "%") unary)* — এক nonterminal-এর
expansion rule। Start symbol সাধারণত expr (বা program)।
22 ^ 3 ^ 2 right-associative exponentiation ধরে value কত? Ambiguous
grammar-এ দুই possible value-ই বের করুন।
প্রয়োগ
2 ^ 3 ^ 2 right-associative exponentiation ধরে value কত? Ambiguous
grammar-এ দুই possible value-ই বের করুন।Right-assoc: 2^(3^2) = 2^9 = 512। ভুল (left) interpretation:
(2^3)^2 = 8^2 = 64। পার্থক্য বিশাল — এজন্যই associativity document
mandatory। Python ** আর math convention দুটোই right-assoc।
3Left recursion elimination-এর পরে tree-র shape বদলায় কেন? Semantics
কি বদলায়?
যুক্তি
Shape বদলায়: left-spine (বামে গভীর) হয়ে যায় right-spine suffix-chain
(expr' গুলো ডানে ঝুলে)। Semantics (accepted language) অক্ষত —
proof mechanical induction। কিন্তু parse-tree shape বদলানোর মানে
evaluation-order assumption আর direct না; LL parser-রা loop/accumulator
দিয়ে left-assoc ফেরত আনে। Shape হলো grammar artifact, meaning
language contract।
4stmt → "while" "(" expr ")" stmt — এই production-এ left factoring
লাগবে কি? কেন বা কেন না?
প্রয়োগ
stmt → "while" "(" expr ")" stmt — এই production-এ left factoring
লাগবে কি? কেন বা কেন না?লাগবে না। Left factoring দরকার হয় যখন একই nonterminal-এর একাধিক
alternative-এ common prefix থাকে (if-এর দুই branch উভয়ে
“if” ”(” expr ”)” stmt দিয়ে শুরু)। While-এর alternatives একটাই;
prefix problem নেই। Predictive decision trivially deterministic।
5“Ambiguity undecidable” শুনে একজন বলল “তাহলে তো compiler কখনো
নিশ্চিত হতে পারে না ওর grammar ঠিক আছে!” — কোথায় ভুল?
যুক্তি
Undecidability মানে arbitrary CFG-এর জন্য universal algorithm নেই — specific grammar-গুলোর জন্য আমরা প্রতিদিন certainty পাই দুই ভাবে: (১) construction-by-design — layered/factored grammar লিখলে provably LL(k) (FIRST/FOLLOW disjoint — next lesson), ambiguity-free by theorem; (২) restricted tools — bison conflict-free report দিলে LALR-unambiguous subset guaranteed। General problem hard, engineered instances tractable।
6আপনার ভাষায় print statement ছাড়াও print(x, y) multi-arg চান।
Argument-list grammar লিখুন এবং trailing comma allow করবেন কি না
— grammar-এ কীভাবে express করবেন?
ডিজাইন
print statement ছাড়াও print(x, y) multi-arg চান।
Argument-list grammar লিখুন এবং trailing comma allow করবেন কি না
— grammar-এ কীভাবে express করবেন?args → args_list | ε; args_list → expr ("," expr)* ","? — trailing
comma-র জন্য শেষে optional ","?। Allow করার argument: diff-friendly
(git blame noise কমে), symmetric add/remove। Rust/Python allow,
C করে না। Alternative cleaner formulation: args_list → expr ("," expr)* | expr "," — decision grammar-এ visible, doc-এ documented।
এরপর কী
এরপর কী
Grammar paper-তে দাঁড়িয়ে আছে — এবার ওটাকে running code বানাবো।
পরের লেসনে parsing-এর দুই পরিবার হাতে-কলমে: recursive descent (LL) — প্রতিটা nonterminal একটা function, FIRST/FOLLOW দিয়ে predictive choice; তারপর এই module-এর সবচেয়ে দামি technique Pratt parsing — binding powers দিয়ে expression parsing, যেটা শিখে গেলে যেকোনো operator-precedence ভাষা কয়েক ঘণ্টায় parse করতে পারবেন। শেষে LR-এর ছায়া: shift/reduce আর bison-এর conflict messages আর ভয় লাগবে না। Build: আমাদের ToyLang-এর full Pratt parser — indented AST print করা সহ।
আরও পড়ুন
- Compilers: Principles, Techniques, and Tools (2nd ed.), Chapter 4 — Syntax Analysis (§4.1–4.3) — Aho, Sethi, Ullman, Lam (the Dragon Book) · Grammar, derivation, ambiguity আর transformations-এর ক্যাননিক্যাল treatment
- Crafting Interpreters, Chapter 5 — Representing Code — Robert Nystrom · Grammar-কে code-এ রূপান্তরের mental model — metaprogramming-flavored চমৎকার ব্যাখ্যা
- Modern Compiler Implementation (ML ed.), Chapter 3 — Parsing — Andrew W. Appel · Ambiguity resolution আর grammar transformations-এর compact academic view