পাঠ ২৫ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Concepts of Programming Languages & Compiler Design / LR পার্সিং — LR(0) ও SLR আইটেম

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

LR parsing — LR(0) & SLR items
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • LR পার্সিং-এর মূল ধারণা — পূর্বনির্মিত স্টেট দিয়ে সিস্টেমেটিক কনফ্লিক্ট রেজোলিউশন
  • LR(0) আইটেম — সংজ্ঞা ও একটি প্রোডাকশনের সব সম্ভাব্য আইটেম
  • CLOSURE ও GOTO অপারেশন — কীভাবে এগুলো মিলে সম্পূর্ণ LR(0) অটোমাটন তৈরি করে
  • SLR-এর সংক্ষিপ্ত পরিচিতি — FOLLOW সেট ব্যবহার করে reduce সিদ্ধান্ত নেওয়া

১ · LR পার্সিং কী

LR পার্সিংLR Parsingএকটি টেবিল-চালিত বটম-আপ পার্সিং পদ্ধতি যা পূর্বনির্মিত স্টেট ব্যবহার করে সিস্টেমেটিকভাবে শিফট/রিডিউস সিদ্ধান্ত নেয়। হলো L24-এর শিফট-রিডিউস মেকানিজমের একটি অটোমেটেড, টেবিল-চালিত সংস্করণ। "LR" নামটি L13/L21-এর পরিভাষা থেকে সরাসরি এসেছে — বাম থেকে ডানে স্ক্যান (Left-to-right) এবং রাইটমোস্ট ডেরিভেশন রিভার্সে তৈরি করা (Rightmost derivation in reverse)। L24-এ কনফ্লিক্ট সমাধান করা হয়েছিল একটি হাতে-করা কনভেনশন ("রিডিউস অগ্রাধিকার") দিয়ে — LR পার্সিং এর বদলে পূর্বনির্মিত স্টেট ব্যবহার করে, যা প্রতিটি মুহূর্তে ঠিক কোন অ্যাকশন নিতে হবে তা সুনির্দিষ্টভাবে বলে দেয়।

২ · LR(0) আইটেম

LR(0) আইটেমLR(0) Itemএকটি গ্রামার প্রোডাকশন, যেখানে একটি ডট (•) বসিয়ে দেখানো হয় ডান-পাশের কতটুকু এখন পর্যন্ত রিকগনাইজড। হলো এই পুরো পদ্ধতির বিল্ডিং ব্লক — একটি গ্রামার প্রোডাকশন, যেখানে একটি ডট (•) বসিয়ে দেখানো হয় ডান-পাশের অংশের ঠিক কতটুকু এখন পর্যন্ত চেনা (recognized) হয়েছে। উদাহরণস্বরূপ, প্রোডাকশন E ::= E + T-এর সম্ভাব্য চারটি আইটেম —

$$E \to \bullet\, E + T \qquad E \to E \bullet + T \qquad E \to E + \bullet\, T \qquad E \to E + T \bullet$$

একদম বামের আইটেম বলছে "এখনো কিছুই দেখা হয়নি, শুরু করতে হবে"; সবচেয়ে ডানেরটি বলছে "সম্পূর্ণ ডান-পাশ চেনা হয়ে গেছে — এখন রিডিউস করার সময়"।

৩ · CLOSURE ও GOTO — অটোমাটন নির্মাণ

দুটি অপারেশন দিয়ে LR(0) আইটেম-সেট (প্রতিটি সেট = একটি পার্সার স্টেট) নির্মিত হয় —

CLOSURE(I)
কোনো আইটেম-সেট I দেওয়া থাকলে, যদি কোনো আইটেমে ডটের ঠিক পরে একটি নন-টার্মিনাল থাকে, সেই নন-টার্মিনালের সব প্রোডাকশন ডট-at-start সহ যোগ করো (পার্সার হয়তো সেগুলোর যেকোনো একটি চিনতে যাচ্ছে) — নতুন আইটেম যোগ হওয়া বন্ধ না হওয়া পর্যন্ত পুনরাবৃত্তি করো।
GOTO(I, X)
আইটেম-সেট I ও গ্রামার চিহ্ন X দেওয়া থাকলে, I-এর যেসব আইটেমে ডটের ঠিক পরে X আছে, সেগুলোতে ডট এক ঘর X-এর পরে সরিয়ে নিয়ে তার CLOSURE নাও — এই ফলাফলই স্টেট-ট্রানজিশন সংজ্ঞায়িত করে (প্রতিটি আইটেম-সেটই একটি স্টেট)।

৪ · SLR — FOLLOW সেট দিয়ে শক্তিশালীকরণ

SLRSimple LRLR(0) আইটেমের উপর L23-এর FOLLOW সেট বসিয়ে reduce সিদ্ধান্ত নেওয়ার সময় নির্ধারণ করে, যা কিছু কনফ্লিক্ট সমাধান করে যা শুধু LR(0) দিয়ে হয় না। (Simple LR) কাঁচা LR(0) আইটেমের উপর L23-এর FOLLOW সেট বসিয়ে সিদ্ধান্ত নেয় কখন reduce করতে হবে — একটি আইটেম A ::= α •-এর জন্য "reduce" অ্যাকশন শুধুমাত্র সেসব লুকঅ্যাহেড টোকেনে যোগ করা হয় যেগুলো FOLLOW(A)-এর সদস্য, সব টোকেনে নয়। এভাবে SLR কিছু কনফ্লিক্ট সমাধান করতে পারে যা শুধু LR(0) দিয়ে সম্ভব ছিল না (L26-এ এই টেবিল-নির্মাণের পূর্ণাঙ্গ বাস্তবায়ন দেখা যাবে)।

৫ · হাতে-যাচাইকৃত নির্মাণ — টয় গ্রামার

একটি ইচ্ছাকৃতভাবে ছোট টয় গ্রামার নেওয়া যাক, যাতে আইটেম-সেট হাতে ট্রেস করা সম্ভব হয় —

$$S' \to S \qquad S \to ( S ) \mid a$$

(প্রথম নিয়মটি অগমেন্টেড স্টার্ট প্রোডাকশন — একটি নতুন স্টার্ট সিম্বল S' যোগ করা হয় শুধু গ্রামারের মূল স্টার্ট সিম্বলকে রেফার করার জন্য, যাতে "পার্স সম্পূর্ণ" অবস্থাটি একটি সুস্পষ্ট আইটেম S' ::= S • দিয়ে শনাক্ত করা যায়।) নিচের কোড সেলে closure() ও goto() ফাংশন real বাস্তবায়ন করে এই গ্রামারের উপর BFS চালিয়ে সব পৌঁছানো-যোগ্য LR(0) স্টেট বের করা হয়েছে, এবং প্রথম কয়েকটি স্টেট হাতে-করা যাচাইয়ের সাথে মিলিয়ে দেখানো হয়েছে।

Python
from collections import deque

# টয় গ্রামার (ইচ্ছাকৃতভাবে ছোট, হাতে-যাচাই করার জন্য), অগমেন্টেড স্টার্ট প্রোডাকশনসহ:
#   S' ::= S      (অগমেন্টেড স্টার্ট প্রোডাকশন)
#   S  ::= ( S )
#   S  ::= a
productions = [
    ("S'", ("S",)),
    ("S", ("(", "S", ")")),
    ("S", ("a",)),
]
nonterminals = {"S'", "S"}


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


def closure(items):
    """CLOSURE(I): ডটের ঠিক পরে নন-টার্মিনাল থাকলে সেই নন-টার্মিনালের সব প্রোডাকশন
    ডট-at-start সহ যোগ করো, ফিক্সড-পয়েন্ট পর্যন্ত।"""
    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, symbol):
    """GOTO(I, X): I-এর যেসব আইটেমে ডটের ঠিক পরে X আছে, সেগুলোতে ডট এক ঘর এগিয়ে
    নিয়ে CLOSURE নাও -- এটাই একটি নতুন স্টেট (বা বিদ্যমান কোনো স্টেট)।"""
    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 format_item(item):
    lhs, rhs, dot = item
    parts = list(rhs)
    parts.insert(dot, ".")
    return f"{lhs} -> {' '.join(parts)}"


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(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


states, transitions = build_lr0_automaton()

print(f"মোট LR(0) স্টেট: {len(states)}\n")
for idx, I in enumerate(states):
    print(f"I{idx}:")
    for item in sorted(I, key=format_item):
        print(f"  {format_item(item)}")
    print()

print("ট্রানজিশন:")
for (i, sym), j in sorted(transitions.items()):
    print(f"  GOTO(I{i}, '{sym}') = I{j}")

# ---- হাতে-করা যাচাই ----
# I0-এ থাকা উচিত: S' -> .S, S -> .(S), S -> .a (CLOSURE প্রয়োগ করে)
expected_I0 = frozenset({
    ("S'", ("S",), 0),
    ("S", ("(", "S", ")"), 0),
    ("S", ("a",), 0),
})
assert states[0] == expected_I0
print("\nI0 = closure({S' -> .S}) হাতে-যাচাই মিলেছে: OK")

# GOTO(I0, '(') -> S -> (.S) এর CLOSURE, যেখানে আবার S->.( S ) ও S->.a যোগ হয়
expected_goto_paren = frozenset({
    ("S", ("(", "S", ")"), 1),
    ("S", ("(", "S", ")"), 0),
    ("S", ("a",), 0),
})
i1 = transitions[(0, "(")]
assert states[i1] == expected_goto_paren
print(f"GOTO(I0,'(') = I{i1} হাতে-যাচাই মিলেছে: OK")

# GOTO(I0, 'a') -> S -> a. (dot at end, reduce state)
expected_goto_a = frozenset({("S", ("a",), 1)})
i3 = transitions[(0, "a")]
assert states[i3] == expected_goto_a
print(f"GOTO(I0,'a') = I{i3} হাতে-যাচাই মিলেছে: OK")

# GOTO(I0, 'S') -> S' -> S. (accepting state)
expected_goto_S = frozenset({("S'", ("S",), 1)})
i2 = transitions[(0, "S")]
assert states[i2] == expected_goto_S
print(f"GOTO(I0,'S') = I{i2} (accepting state) হাতে-যাচাই মিলেছে: OK")

# I1 (= GOTO(I0,'(')) নিজের উপর সেল্ফ-লুপ করে '(' দিয়ে -- একই আইটেম-সেট আবার তৈরি হয়
assert transitions[(i1, "(")] == i1
print(f"GOTO(I{i1},'(') == I{i1} (সেল্ফ-লুপ, নেস্টেড বন্ধনীর জন্য) হাতে-যাচাই মিলেছে: OK")

assert len(states) == 6
print(f"\nমোট স্টেট সংখ্যা = {len(states)} -- প্রথম কয়েকটি স্টেট হাতে-করা ট্রেসের সাথে সম্পূর্ণ মিলেছে")

    
মূল কথা · Key takeaway

LR(0) আইটেম, CLOSURE ও GOTO — এই তিনটি ধারণা মিলে একটি সম্পূর্ণ, মেকানিক্যালি নির্মাণযোগ্য পার্সিং অটোমাটন তৈরি করে, যা L24-এর হাতে-করা কনফ্লিক্ট-রেজোলিউশন কনভেনশনের প্রয়োজনীয়তা দূর করে দেয়। SLR এই একই অটোমাটনের উপর FOLLOW সেট বসিয়ে আরও নির্ভুলভাবে reduce সিদ্ধান্ত নেয় — L26-এ আমরা এই পুরো প্রক্রিয়াটি একটি স্বয়ংক্রিয় "পার্সার জেনারেটর"-এ রূপান্তর করব।

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

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

প্র ০১ I0-এ CLOSURE প্রয়োগ করলে S -> .(S) ও S -> .a যোগ হয়, কিন্তু এই দুটি আইটেমের উপর আবার CLOSURE প্রয়োগ করলে কি নতুন কিছু যোগ হয়?

না, কিছু যোগ হয় না। CLOSURE নিয়ম শুধু তখনই নতুন আইটেম যোগ করে যখন ডটের ঠিক পরে একটি নন-টার্মিনাল থাকে। S -> . ( S )-এ ডটের পরে আছে টার্মিনাল ( — নন-টার্মিনাল নয়, তাই এখান থেকে CLOSURE আর কিছু যোগ করবে না। একইভাবে S -> . a-এ ডটের পরে টার্মিনাল a। তাই CLOSURE এখানেই ফিক্সড-পয়েন্টে পৌঁছে থামে — এটাই I0-এ ঠিক তিনটি আইটেম থাকার কারণ, চারটি বা তার বেশি নয়।

প্র ০২ GOTO(I1, '(') == I1 -- এই সেল্ফ-লুপের ব্যাকরণগত অর্থ কী? এটি কি একটি বাগ?

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

প্র ০৩ I2 = { S' -> S . } স্টেটে ডট একদম শেষে। এই স্টেটে GOTO টেবিলে কোনো এন্ট্রি নেই কেন?

কারণ GOTO/শিফট শুধু তখনই সম্ভব যখন ডটের ঠিক পরে কোনো চিহ্ন থাকে (সেই চিহ্নটি "আশা করা" হচ্ছে)। S' -> S •-এ ডট একদম শেষে — আর কোনো চিহ্ন বাকি নেই এগিয়ে যাওয়ার জন্য। এই ধরনের "dot at end" আইটেম আসলে একটি reduce (বা, যেহেতু এটি অগমেন্টেড স্টার্ট প্রোডাকশন, accept) অ্যাকশন নির্দেশ করে, কোনো GOTO ট্রানজিশন নয় — ঠিক L24-এর "dot at end মানে রিডিউসের সময়" ধারণার সাথে মেলে।

অনুশীলন

  1. চিন্তা করুন: I4 = { S -> ( S . ) } থেকে GOTO(I4, ')') চালালে কী পাওয়া যাবে — হাতে হিসাব করে বলুন এটি reduce স্টেট নাকি আরেকটি shift স্টেট।

    S -> ( S . )-এ ডট এগিয়ে )-এর পরে গেলে পাওয়া যায় S -> ( S ) . — ডট একদম শেষে। এটি একটি reduce স্টেট (কোডে I5 হিসেবে দেখানো), যেখানে সম্পূর্ণ ( S ) রিকগনাইজড হয়ে গেছে এবং এটিকে একটি S-এ রিডিউস করা উচিত। শিফট স্টেট হতো যদি ডটের পরে আরও কোনো চিহ্ন বাকি থাকত, কিন্তু )-ই এই প্রোডাকশনের শেষ চিহ্ন।

  2. পরীক্ষা করুন: উপরের কোড সেলে productions তালিকায় ("S", ("a", "a")) যোগ করে (অর্থাৎ S ::= aa একটি নতুন বিকল্প) Run চাপলে স্টেট সংখ্যা কি বাড়ে?

    হ্যাঁ, স্টেট সংখ্যা বাড়বে (৬-এর বেশি হবে)। I0-এর CLOSURE-এ এখন S -> . a a-ও যোগ হবে, এবং GOTO(I0, 'a') এখন S -> a . ও S -> a . a — দুটো আইটেম নিয়ে একটি নতুন স্টেট তৈরি করবে (আগে যেখানে শুধু S -> a . ছিল)। সেই নতুন স্টেট থেকে আবার GOTO(..., 'a') চালালে S -> a a .-এ পৌঁছানোর জন্য আরেকটি নতুন স্টেট লাগবে — মোট স্টেট সংখ্যা বেড়ে যাবে, যা দেখায় গ্রামার জটিল হলে অটোমাটনও বড় হয়।

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

আগের পাঠ
L24 · বটম-আপ পার্সিং — শিফট-রিডিউস