পাঠ ২৬ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Concepts of Programming Languages & Compiler Design / LALR পার্সিং ও পার্সার জেনারেটর

LALR পার্সিং ও পার্সার জেনারেটর

LALR parsing & parser generators
৮ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • LALR-এর মূল ধারণা — LR(1) স্টেট মার্জ করে টেবিল ছোট রাখা
  • LR(0), SLR, LALR, LR(1)-এর মধ্যে ক্ষমতা ও টেবিল-সাইজের ট্রেড-অফ
  • পার্সার জেনারেটর টুলের ধারণা ও বাস্তব-জগতে এর ব্যবহারিক গুরুত্ব
  • L25-এর CLOSURE/GOTO যুক্তিকে একটি সম্পূর্ণ, স্বয়ংক্রিয় টেবিল-জেনারেটরে রূপান্তর

১ · LALR — LR(1) স্টেট মার্জ করা

LALRLookAhead LRএকই কোর আইটেম (লুকঅ্যাহেড বাদ দিয়ে) থাকা LR(1) স্টেটগুলো মার্জ করে একটি ছোট কিন্তু SLR-এর চেয়ে শক্তিশালী অটোমাটন তৈরি করে। (LookAhead LR) হলো প্র্যাক্টিক্যাল বাস্তব-জগতের স্ট্যান্ডার্ড — L25-এর SLR-এর চেয়ে একধাপ এগিয়ে। প্রথমে কল্পনা করুন LR(1) স্টেট — L25-এর LR(0) আইটেমের একটি বেশি শক্তিশালী কিন্তু অনেক বড় সংস্করণ, যেখানে প্রতিটি আইটেমের সাথে সরাসরি একটি নির্দিষ্ট লুকঅ্যাহেড টোকেন যুক্ত থাকে (SLR-এর মতো পুরো নন-টার্মিনালের জন্য একটি সাধারণ FOLLOW সেট ব্যবহার না করে)। LALR এই LR(1) স্টেটগুলোর মধ্যে যেগুলোর কোর (লুকঅ্যাহেড বাদ দিয়ে শুধু আইটেম) একই, সেগুলোকে মার্জ করে দেয় — ফলাফল: অটোমাটন প্রায় SLR/LR(0)-এর মতোই ছোট থাকে, অথচ এটি এমন কিছু গ্রামারও কনফ্লিক্ট ছাড়া পার্স করতে পারে যা শুধু SLR দিয়ে সম্ভব হতো না।

বাস্তব-জগতের প্রাসঙ্গিকতা

LALR-ই সেই অ্যালগরিদম যা বেশিরভাগ বাস্তব, ব্যাপকভাবে-ব্যবহৃত পার্সার জেনারেটর টুল (Yacc, Bison, ও এদের সমগোত্রীয় অন্যান্য ইকোসিস্টেমের টুল) ব্যবহার করে — কারণ এটি ক্ষমতা বনাম টেবিল-সাইজের একটি ব্যবহারিক সুইট-স্পট এ পৌঁছায়।

২ · LR0, SLR, LALR, LR1 — একনজরে তুলনা

নিচের কোড সেলের প্রথম অংশে একটি তুলনামূলক টেবিল প্রিন্ট করা হয়েছে যা এই চারটি অ্যালগরিদমকে টেবিল-সাইজ, আপেক্ষিক ক্ষমতা, এবং বাস্তবে হাতে-নির্মাণযোগ্যতা অনুযায়ী পাশাপাশি রাখে।

LR(0) / SLR
সবচেয়ে ছোট টেবিল, সবচেয়ে কম ক্ষমতা — কিছু সাধারণ গ্রামারেও কনফ্লিক্ট দেখাতে পারে (L25-এর মূল বিষয়)।
LALR
SLR-এর প্রায় সমান ছোট টেবিল, কিন্তু বেশি শক্তিশালী — বাস্তব টুলের ডিফল্ট পছন্দ (এই পাঠের বিষয়)।
LR(1)
সর্বোচ্চ ক্ষমতা, কিন্তু টেবিল অনেক বড় হতে পারে — বাস্তবে খুব কমই সরাসরি ব্যবহৃত হয়, LALR-এর ভিত্তি হিসেবে থাকে।

৩ · পার্সার জেনারেটর — এই পুরো পরিবারের প্র্যাক্টিক্যাল পেঅফ

পার্সার জেনারেটরParser Generatorএকটি টুল যা গ্রামার স্পেসিফিকেশন ইনপুট হিসেবে নিয়ে স্বয়ংক্রিয়ভাবে পার্সিং টেবিল (এবং প্রায়ই পার্সার কোডই) জেনারেট করে দেয়। হলো একটি টুল যা গ্রামারকে ইনপুট হিসেবে নেয় এবং স্বয়ংক্রিয়ভাবে পার্সিং টেবিল (প্রায়ই পার্সার কোডই) আউটপুট করে দেয় — এটি L22-এর হাতে-লেখা রিকার্সিভ ডিসেন্টের ঠিক বিপরীত পন্থা। বাস্তবে, নন-টয় গ্রামারের জন্য LR/LALR পার্সিং টেবিল প্রায় কখনোই হাতে বানানো হয় না — কারণ L25-এর আইটেম-সেট নির্মাণ যান্ত্রিক (mechanical) হলেও বড় স্কেলে হাতে করলে ভুল হওয়ার সম্ভাবনা প্রচুর — তাই এগুলো প্রায় সবসময় মেশিন-জেনারেটেড।

৪ · একটি মিনি পার্সার জেনারেটর বাস্তবায়ন

নিচের কোড সেলে generate_parse_table নামের একটি ফাংশন লেখা হয়েছে যা L25-এর সেই একই টয় গ্রামারের (S ::= ( S ) | a) উপর CLOSURE/GOTO নির্মাণকে স্বয়ংক্রিয়ভাবে একটি সম্পূর্ণ ACTION টেবিলে (shift/reduce/accept) রূপান্তর করে — লেখক নিজে হাতে কোনো স্টেট লেখেননি। ফলাফল L25-এ হাতে-যাচাই করা ৬টি স্টেটের সাথে তুলনা করে দেখানো হয়েছে, এবং এরপর একটি real LR ড্রাইভার দিয়ে সত্যিকারের ইনপুট স্ট্রিং পার্স করেও যাচাই করা হয়েছে — শুধু গঠন মেলা নয়, প্রকৃত পার্সিং আচরণও সঠিক।

Python
# LR0, SLR, LALR, LR1 তুলনামূলক তথ্য
comparison = {
    "lr0":  {"relative_table_size": "সবচেয়ে ছোট", "relative_power": "সবচেয়ে কম", "hand_constructible": "ছোট গ্রামারে সম্ভব (L25)"},
    "slr":  {"relative_table_size": "ছোট",          "relative_power": "মাঝারি",     "hand_constructible": "ছোট গ্রামারে সম্ভব"},
    "lalr": {"relative_table_size": "ছোট (SLR-এর কাছাকাছি)", "relative_power": "উচ্চ", "hand_constructible": "বাস্তবে প্রায় সবসময় মেশিন-জেনারেটেড"},
    "lr1":  {"relative_table_size": "সবচেয়ে বড়",   "relative_power": "সর্বোচ্চ",  "hand_constructible": "বাস্তবে প্রায় কখনো হাতে নয়"},
}
print("অ্যালগরিদম তুলনা:")
for name, info in comparison.items():
    print(f"  {name:5s} -> {info}")
print()

from collections import deque

END = "$"

# L25-এর সেই একই টয় গ্রামার -- এই লেসনের লক্ষ্য: L25-এ হাতে-যাচাই করা স্টেটগুলো
# এখন সম্পূর্ণ স্বয়ংক্রিয়ভাবে একটি পূর্ণাঙ্গ ACTION/GOTO টেবিলে রূপান্তর করা।
productions = [
    ("S'", ("S",)),
    ("S", ("(", "S", ")")),
    ("S", ("a",)),
]
nonterminals = {"S'", "S"}
terminals = {"(", ")", "a"}
start_symbol = "S'"


def productions_of(nt):
    return [p for p in productions if p[0] == nt]


def closure(items):
    items = set(items)
    changed = True
    while changed:
        changed = False
        for (lhs, rhs, dot) in list(items):
            if dot < len(rhs):
                sym = rhs[dot]
                if sym in nonterminals:
                    for (plhs, prhs) in productions_of(sym):
                        new_item = (plhs, prhs, 0)
                        if new_item not in items:
                            items.add(new_item)
                            changed = True
    return frozenset(items)


def goto_items(items, symbol):
    moved = {(lhs, rhs, dot + 1) for (lhs, rhs, dot) in items
             if dot < len(rhs) and rhs[dot] == symbol}
    return closure(moved) if moved else None


def all_symbols():
    syms = set()
    for (_, rhs) in productions:
        syms.update(rhs)
    return syms


def build_lr0_automaton():
    I0 = closure({("S'", ("S",), 0)})
    states = [I0]
    state_index = {I0: 0}
    transitions = {}
    queue = deque([I0])
    while queue:
        I = queue.popleft()
        i_idx = state_index[I]
        for sym in sorted(all_symbols()):
            J = goto_items(I, sym)
            if J is None:
                continue
            if J not in state_index:
                state_index[J] = len(states)
                states.append(J)
                queue.append(J)
            transitions[(i_idx, sym)] = state_index[J]
    return states, transitions


def compute_follow():
    """এই ছোট গ্রামারের জন্য FOLLOW সেট (L23-এর অ্যালগরিদমের একই কাঠামো)।"""
    follow = {nt: set() for nt in nonterminals}
    follow[start_symbol].add(END)
    changed = True
    while changed:
        changed = False
        for (lhs, rhs) in productions:
            for i, sym in enumerate(rhs):
                if sym not in nonterminals:
                    continue
                rest = rhs[i + 1:]
                if rest:
                    first_rest = {rest[0]}  # এই গ্রামারে rest[0] সবসময় টার্মিনাল
                    before = len(follow[sym])
                    follow[sym].update(first_rest)
                    if len(follow[sym]) != before:
                        changed = True
                else:
                    before = len(follow[sym])
                    follow[sym].update(follow[lhs])
                    if len(follow[sym]) != before:
                        changed = True
    return follow


def generate_parse_table(states, transitions):
    """স্বয়ংক্রিয় "পার্সার জেনারেটর": GOTO থেকে shift/goto, complete আইটেম + FOLLOW
    থেকে reduce/accept -- কোনো স্টেট লেখক নিজে হাতে লেখেননি।"""
    follow = compute_follow()
    action = {}
    goto_tbl = {}
    conflicts = []

    for (i, sym), j in transitions.items():
        if sym in terminals:
            key = (i, sym)
            entry = ("shift", j)
            if key in action and action[key] != entry:
                conflicts.append((key, action[key], entry))
            action[key] = entry
        else:
            goto_tbl[(i, sym)] = j

    for idx, I in enumerate(states):
        for (lhs, rhs, dot) in I:
            if dot == len(rhs):
                if lhs == start_symbol:
                    key = (idx, END)
                    entry = ("accept",)
                else:
                    for t in follow[lhs]:
                        key = (idx, t)
                        entry = ("reduce", lhs, rhs)
                        if key in action and action[key] != entry:
                            conflicts.append((key, action[key], entry))
                        action[key] = entry
                    continue
                if key in action and action[key] != entry:
                    conflicts.append((key, action[key], entry))
                action[key] = entry
    return action, goto_tbl, conflicts


states, transitions = build_lr0_automaton()
action, goto_tbl, conflicts = generate_parse_table(states, transitions)

print(f"স্বয়ংক্রিয়ভাবে জেনারেট করা স্টেট সংখ্যা: {len(states)}")
print(f"ACTION টেবিল এন্ট্রি: {len(action)}, GOTO টেবিল এন্ট্রি: {len(goto_tbl)}")
print(f"কনফ্লিক্ট: {len(conflicts)}\n")

for (i, t), entry in sorted(action.items(), key=lambda kv: (kv[0][0], str(kv[0][1]))):
    print(f"  ACTION[I{i}, '{t}'] = {entry}")
for (i, nt), j in sorted(goto_tbl.items()):
    print(f"  GOTO[I{i}, '{nt}'] = I{j}")

assert len(conflicts) == 0
assert len(states) == 6, "L25-এ হাতে-যাচাই করা ৬টি স্টেটের সাথে মিলতে হবে"
print(f"\nস্বয়ংক্রিয়ভাবে তৈরি {len(states)}টি স্টেট L25-এর হাতে-যাচাই করা স্টেট সংখ্যার সাথে মিলেছে -- OK")


def run_lr_driver(tokens):
    """জেনারেট করা টেবিল আসলেই সঠিকভাবে পার্স করে কি না তা যাচাই করার জন্য একটি
    real LR ড্রাইভার -- স্টেট গঠন মেলা যথেষ্ট নয়, এটি সত্যিই একটি ভাষা চিনতে পারা উচিত।"""
    stack = [0]
    pos = 0
    toks = tokens + [END]
    for _ in range(100):
        state = stack[-1]
        lookahead = toks[pos]
        entry = action.get((state, lookahead))
        if entry is None:
            return False
        if entry[0] == "shift":
            stack.append(lookahead)
            stack.append(entry[1])
            pos += 1
        elif entry[0] == "reduce":
            _, lhs, rhs = entry
            for _ in range(2 * len(rhs)):
                stack.pop()
            top_state = stack[-1]
            stack.append(lhs)
            stack.append(goto_tbl[(top_state, lhs)])
        elif entry[0] == "accept":
            return True
    raise RuntimeError("পার্সার থামছে না")


checks = [
    (["(", "a", ")"], True),
    (["a"], True),
    (["(", "a"], False),   # বন্ধনী বন্ধ হয়নি
    (["(", ")"], False),   # ভেতরে 'a' নেই
]
print()
for toks, expected in checks:
    got = run_lr_driver(toks)
    status = "OK" if got == expected else "FAIL"
    print(f"  parse({toks!r}) = {got}  (expected {expected})  [{status}]")
    assert got == expected

print("\nজেনারেট করা টেবিল সত্যিই সঠিকভাবে পার্স করে -- ALL OK")

    
মূল কথা · Key takeaway

M5 জুড়ে আমরা L22-এর হাতে-লেখা রিকার্সিভ ডিসেন্ট থেকে শুরু করে এই পাঠের সম্পূর্ণ স্বয়ংক্রিয় টেবিল-জেনারেটর পর্যন্ত এসেছি — একটি স্পষ্ট বর্ণালী: হাতে-লেখা (L22) → হাতে-করা কনভেনশন (L24) → হাতে-যাচাইকৃত কিন্তু মেকানিক্যাল অ্যালগরিদম (L23, L25) → সম্পূর্ণ স্বয়ংক্রিয় জেনারেশন (L26)। বাস্তব কম্পাইলার প্রায় সবসময় এই স্পেকট্রামের ডান প্রান্তে থাকে — LALR-ভিত্তিক পার্সার জেনারেটর টুল ব্যবহার করে, ঠিক যা এই কোড সেল ছোট আকারে বাস্তবায়ন করে দেখিয়েছে।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ এই পাঠের generate_parse_table ফাংশন আসলে LR(0)/SLR-স্টাইল টেবিল তৈরি করে (L25-এর মতোই), সত্যিকারের LALR নয়। তাহলে এই কোড সেলটি LALR-এর কী শেখায়?

এই কোডটি শেখায় স্বয়ংক্রিয়ভাবে টেবিল তৈরি করার প্রক্রিয়াটি — যা LALR-সহ পুরো LR পরিবারের ভিত্তি। LALR-এর অতিরিক্ত ধাপ হলো LR(1) স্টেট তৈরি করে তারপর একই-কোরের স্টেটগুলো মার্জ করা — এই টয় গ্রামারে (S ::= (S) | a) কোনো দুটি স্টেটের কোর কখনো একই হয় না (প্রতিটি স্টেট আলাদা আইটেম-সেট), তাই LR(0), SLR এবং LALR — সবগুলো এই নির্দিষ্ট গ্রামারে হুবহু একই ৬টি স্টেট দেবে। LALR-এর আসল সুবিধা দেখা যায় বড়, বেশি জটিল গ্রামারে, যেখানে মার্জ করার মতো একাধিক LR(1) স্টেট তৈরি হয়।

প্র ০২ generate_parse_table-এ conflicts তালিকা কীভাবে একটি reduce-reduce কনফ্লিক্ট শনাক্ত করত, যদি এই গ্রামারে থাকত?

কোডে for t in follow[lhs]: key = (idx, t) ... if key in action and action[key] != entry: conflicts.append(...) — এই অংশটি একই স্টেটে দুটি ভিন্ন "dot at end" আইটেম (দুটি ভিন্ন প্রোডাকশনের) একই লুকঅ্যাহেড টোকেনের জন্য reduce অ্যাকশন যোগ করার চেষ্টা করলে তা ধরে ফেলবে — দ্বিতীয়বার একই key-তে ভিন্ন entry লেখার চেষ্টা হলেই তা conflicts-এ লগ হয়ে যায়, ঠিক যেভাবে L23-এর LL(1) টেবিল-নির্মাণ কোডেও কনফ্লিক্ট ধরা হয়েছিল।

প্র ০৩ run_lr_driver ফাংশনে reduce-এর সময় "2 * len(rhs)" সংখ্যক আইটেম স্ট্যাক থেকে পপ করা হয়, len(rhs) নয়। কেন দ্বিগুণ?

কারণ এই ড্রাইভারের স্ট্যাক গ্রামার-চিহ্ন ও স্টেট-নম্বর — দুটোই পর্যায়ক্রমে রাখে (প্রতিটি শিফট/গোটোর পরে চিহ্ন, তারপর সেই চিহ্নের ফলে পৌঁছানো নতুন স্টেট-নম্বর, দুটোই পুশ হয়)। একটি প্রোডাকশনের ডান-পাশে len(rhs)টি চিহ্ন থাকলে, স্ট্যাকে সেগুলোর জন্য মোট 2 * len(rhs)টি এন্ট্রি জমেছিল (প্রতিটি চিহ্নের সাথে তার স্টেট-নম্বর জোড়া করে) — তাই ঠিক ততগুলো পপ করলেই স্ট্যাক সেই প্রোডাকশন শুরু হওয়ার আগের অবস্থায় ফিরে যায়, যেখান থেকে নতুন নন-টার্মিনাল ও তার GOTO-স্টেট পুশ করা হয়।

অনুশীলন

  1. চিন্তা করুন: comparison ডিকশনারিতে "lr0"-এর hand_constructible মান "ছোট গ্রামারে সম্ভব (L25)" লেখা আছে, কিন্তু "lalr"-এর জন্য "বাস্তবে প্রায় সবসময় মেশিন-জেনারেটেড"। এই পার্থক্যের কারণ কী?

    LR(0) স্টেট নির্মাণ (CLOSURE/GOTO) তুলনামূলক সরল, তাই L25-এর মতো ছোট টয় গ্রামারে হাতে করা সম্ভব এবং শিক্ষামূলকভাবে কার্যকর। কিন্তু LALR-এ প্রথমে (ধারণাগতভাবে) LR(1) স্টেট তৈরি করতে হয় (প্রতিটি আইটেমের সাথে নির্দিষ্ট লুকঅ্যাহেড ট্র্যাক করে), তারপর একই-কোরের স্টেট মার্জ করতে হয় — এই দুই ধাপ বাস্তব-আকারের গ্রামারে (শত শত প্রোডাকশন) হাতে করা কার্যত অসম্ভব, তাই এটি প্রায় একচেটিয়াভাবে টুল দিয়েই করা হয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে checks তালিকায় (["(", "(", "a", ")", ")"], True) যোগ করে Run চাপলে নেস্টেড বন্ধনী সঠিকভাবে পার্স হয় কি না দেখুন।

    হ্যাঁ, এটি True রিটার্ন করবে এবং টেস্ট পাস করবে। কারণ L25-এ আলোচিত সেল্ফ-লুপ (GOTO(I1, '(') == I1) ঠিক এই কারণেই আছে — যতবার ইচ্ছা নেস্টেড বন্ধনী শিফট করা যায়, প্রতিবার একই স্টেট I1-এ ফিরে এসে, তারপর যখন "a" আসে তখন গভীর থেকে বাইরের দিকে ধাপে ধাপে reduce হতে হতে পুরো এক্সপ্রেশনটি একটিমাত্র S-এ পরিণত হয় ও accept হয়ে যায়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L25 · LR পার্সিং — LR(0) ও SLR আইটেম