Foundationপ্রথম নীতি থেকে
LEVEL 5লেসন ৩/১৬কঠিন৫০ মিনিট

Grammar ও Ambiguity — BNF, derivation, আর দ্ব্যর্থক বাক্য

Grammars and Ambiguity

Token stream-কে structure দেওয়ার formal ভাষা — grammar; আর grammar-এর সবচেয়ে বিপজ্জনক ফাঁদ ambiguity চেনা ও নির্মূল করা।

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

  • BNF ও EBNF notation-এ toy language-এর grammar লিখে production rule, terminal, nonterminal আলাদা করতে পারবেন
  • Leftmost আর rightmost derivation trace করে একটা string-এর parse tree হাতে আঁকতে পারবেন
  • `1+2*3`, `a-a-a`, dangling else — প্রতিটা ক্ষেত্রে ambiguity কোথায় জন্ম নেয় তা parse tree দুইটা এঁকে দেখাতে পারবেন
  • Precedence layering (expr→term→factor), left recursion elimination, left factoring — তিনটা transformation apply করতে পারবেন
  • কেন arbitrary CFG-এর ambiguity undecidable তা বলতে পারবেন এবং সেই সীমা practical parser design-কে কীভাবে affect করে তা ব্যাখ্যা করতে পারবেন

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

আগে এটা বুঝি

গত লেসনের শেষে আমাদের হাতে 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 থেকে। তিন রকম জিনিস:

জিনিসConventionToyLang উদাহরণ
Nonterminallowercase nameexpr, term, factor
Terminalquoted"+", "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   2
একই input `1+2*3`, দুইটা ভিন্ন tree, দুইটা ভিন্ন উত্তর। এটাই ambiguous grammar-এর সংজ্ঞা।

Tree 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-তে আসে:

  1. * বেশি deeply nested হয় — higher precedence automatic
  2. Same-level chain বাম ঝোঁকে (term term op term op...) — left associativity automatic
  3. 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 (পরের লেসনে বিস্তারিত):

ClassParser familyConstraintReal user
Regularlexer DFAnesting নেইtokens
LL(1)predictive recursive descentleft recursion ✗, factored ✓hand-written parsers
LL(k)/PEGbacktracking/packratunlimited lookahead costPython 3.9+ PEG
LALR(1)yacc/bison tableconflicts resolve করতে হয়PostgreSQL, Bash
LR(1)canonical tableবিশাল tabletheory benchmark
GLRfork-and-mergeslow but handles ambiguitytree-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) = 9

Layered grammar-এ এখন derive করি — left associativity কোথা থেকে আসে দেখুন:

expr ⇒ term (("+"|"-") term)*  — repetition list বাম-ভিত্তিক
     ⇒ [term₁] [- term₂] [- term₃]
     ⇒ ((term₁ − term₂) − term₃)   ← বাম থেকে fold

Repetition 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-এ else

Braces ছাড়া লিখলে 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   ✓ গণিতের নিয়ম মিলল
Input `1 + 2 * 3` layered grammar দিয়ে descend করলে। প্রতিটা level একটা precedence tier; parens যেকোনো tier-এ ফিরে আসার লিফট।

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 → ε path
বাম: LL-parser-এ infinite descent। ডান: eliminated form — loop-friendly suffix nonterminal।

Recursion direction-ই এখানে সব: left-recursion top-down-এ self-call before progress (deadlock), right-recursion আগে progress তারপর recurse (fine)। Bottom-up LR parser-এ উল্টোটা সত্য।

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

EXPERIMENT

Ambiguous grammar-এর দুই parse tree হাতে আঁকা, তারপর pyparsing দিয়ে prove

Linux / macOS — python3; optional: pip install pyparsing· ২৫ মিনিট

ধাপ ১ — কাগজে আঁকুন (হ্যাঁ, আসল কাগজ): 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 না।

নিজে বানান

BUILD IT

Precedence-correct expression grammar + 30-line tree printer

Python · ●●○○○
  1. Ambiguous expr grammar-কে three-layer EBNF-এ rewrite করুন (expr/term/factor)
  2. গত লেসনের Lexer import করুন (toylang/lexer.py)
  3. প্রতিটা nonterminal = একটা function — recursive descent preview
  4. Generic tree-printer লিখুন (30 লাইন) — indent দিয়ে nested tuple print
  5. 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 নয়।

নিজে বাড়ান:

  1. ^ power operator যোগ করুন — right-associative, unary-র চেয়ে নাকি কম tight? (-2^2 = -4 মানে ^ unary-র নিচে।)
  2. Comparison layer (==,!=,<,>…) expr আর term-এর মাঝে বসান — ToyLang full grammar-এর জন্য লাগবেই।
  3. 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 করব।

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

1

Terminal, 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)।

2

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।

3

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

4

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 করবেন?

ডিজাইন

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