LL(1) পার্সিং — FIRST ও FOLLOW সেট
এই পাঠে যা শিখবেন
- LL(1) পার্সিং-এর সংজ্ঞা — "LL" ও "(1)" প্রতিটি অক্ষরের অর্থ
- FIRST সেট — সংজ্ঞা ও ফিক্সড-পয়েন্ট গণনা অ্যালগরিদম
- FOLLOW সেট — সংজ্ঞা ও ফিক্সড-পয়েন্ট গণনা অ্যালগরিদম
- FIRST/FOLLOW থেকে LL(1) পার্সিং টেবিল তৈরি, এবং কীভাবে বোঝা যায় একটি গ্রামার LL(1) কি না
১ · LL(1) পার্সিং কী
LL(1) পার্সিংLL(1) Parsingএকটি টেবিল-চালিত টপ-ডাউন পার্সিং পদ্ধতি যা মাত্র একটি লুকঅ্যাহেড টোকেন দেখে প্রতিটি পার্সিং সিদ্ধান্ত নেয়। হলো L22-এর হাতে-লেখা রিকার্সিভ ডিসেন্টের একটি টেবিল-চালিত, মেশিন-জেনারেটেবল বিকল্প। নামের প্রতিটি অংশের একটি নির্দিষ্ট অর্থ আছে — প্রথম L মানে ইনপুট বাম থেকে ডানে স্ক্যান করা হয় (Left-to-right scan), দ্বিতীয় L মানে এটি একটি লেফটমোস্ট ডেরিভেশন তৈরি করে (Leftmost derivation, L13-এর পরিভাষা), আর (1) মানে পার্সার প্রতিটি সিদ্ধান্তের জন্য মাত্র একটি টোকেন সামনে তাকিয়ে দেখে (lookahead)। রিকার্সিভ ডিসেন্টে এই সিদ্ধান্ত কোডের if/elif শাখায় লুকিয়ে থাকে — LL(1)-এ এটি একটি এক্সপ্লিসিট টেবিলে রূপান্তরিত হয়, যা মেশিন দিয়ে তৈরি করা যায়।
২ · FIRST সেট
FIRST সেটFIRST SetFIRST(α) হলো α থেকে ডেরাইভ করা কোনো স্ট্রিং-এর প্রথম চিহ্ন হিসেবে আসতে পারে এমন টার্মিনালের সেট (α যদি ε ডেরাইভ করতে পারে, তাহলে ε-ও অন্তর্ভুক্ত)। -এর সংজ্ঞা: FIRST(α) হলো α থেকে ডেরাইভ করা কোনো স্ট্রিং-এর প্রথম চিহ্ন হিসেবে আসতে পারে এমন সব টার্মিনালের সেট (যদি α ε ডেরাইভ করতে পারে, তাহলে ε-ও এই সেটে থাকে)। গণনার নিয়ম —
- একটি টার্মিনালের FIRST হলো সে নিজেই।
- একটি নন-টার্মিনাল A-এর FIRST হলো A-এর প্রতিটি প্রোডাকশনের প্রথম চিহ্নের FIRST-এর ইউনিয়ন — যদি সেই প্রথম চিহ্নটি ε ডেরাইভ করতে পারে, পরের চিহ্নের FIRST-ও যোগ হয়, এভাবে চলতে থাকে।
৩ · FOLLOW সেট
FOLLOW সেটFOLLOW SetFOLLOW(A) হলো স্টার্ট সিম্বল থেকে কোনো ডেরিভেশনে A-এর ঠিক পরে আসতে পারে এমন টার্মিনালের সেট।
-এর সংজ্ঞা: FOLLOW(A) হলো স্টার্ট সিম্বল থেকে কোনো ডেরিভেশনে নন-টার্মিনাল A-এর ঠিক পরে
আসতে পারে এমন টার্মিনালের সেট (A যদি কোনো ডেরিভেশনের একদম শেষ চিহ্ন হতে পারে, একটি বিশেষ এন্ড-মার্কার
$-ও এই সেটে থাকে)। এটিও একটি সম্পর্কিত ফিক্সড-পয়েন্ট অ্যালগরিদমে গণনা করা হয় — প্রতিটি
প্রোডাকশনে A কোথায় কোথায় আছে সেখানে ঠিক তার পরে কী আসতে পারে তা পরীক্ষা করে।
৪ · FIRST/FOLLOW থেকে LL(1) টেবিল
প্রতিটি গ্রামার নিয়ম A ::= α-এর জন্য: FIRST(α)-এর প্রতিটি টার্মিনালের জন্য টেবিল সেল
[A, terminal]-এ এই নিয়মটি যোগ করা হয় — আর যদি α থেকে ε ডেরাইভ হতে পারে, তাহলে FOLLOW(A)-এর
প্রতিটি টার্মিনালের জন্যও এই নিয়মটি যোগ করা হয়। একটি গ্রামার প্রকৃতপক্ষে LL(1) তখনই যখন
কোনো টেবিল সেলে একাধিক নিয়ম পড়ে না — যদি পড়ে, তার মানে একটি মাত্র লুকঅ্যাহেড টোকেন
সিদ্ধান্ত নেওয়ার জন্য যথেষ্ট নয়, অর্থাৎ গ্রামারটি LL(1) নয়।
নিচের কোড সেলে L22-এর গ্রামারের উপর real compute_first ও compute_follow ফাংশন
চালিয়ে FIRST/FOLLOW সেট গণনা করা হয়েছে, তারপর সেগুলো থেকে সম্পূর্ণ LL(1) টেবিল তৈরি করে প্রতিটি সেল পরীক্ষা
করে নিশ্চিত করা হয়েছে কোনো কনফ্লিক্ট নেই।
# গ্রামার (L22-এর left-recursion-eliminated রূপ):
# E ::= T E'
# E' ::= + T E' | - T E' | ε
# T ::= F T'
# T' ::= * F T' | / F T' | ε
# F ::= ( E ) | number
EPSILON = "ε"
END = "$"
grammar = {
"E": [("T", "E'")],
"E'": [("+", "T", "E'"), ("-", "T", "E'"), (EPSILON,)],
"T": [("F", "T'")],
"T'": [("*", "F", "T'"), ("/", "F", "T'"), (EPSILON,)],
"F": [("(", "E", ")"), ("number",)],
}
nonterminals = set(grammar.keys())
start_symbol = "E"
def compute_first(grammar):
"""প্রকৃত ফিক্সড-পয়েন্ট অ্যালগরিদম -- কোনো ফলাফল হার্ডকোড করা নেই।"""
first = {nt: set() for nt in grammar}
changed = True
while changed:
changed = False
for nt, productions in grammar.items():
for prod in productions:
if prod == (EPSILON,):
if EPSILON not in first[nt]:
first[nt].add(EPSILON)
changed = True
continue
all_derive_epsilon = True
for sym in prod:
if sym not in nonterminals:
if sym not in first[nt]:
first[nt].add(sym)
changed = True
all_derive_epsilon = False
break
before = len(first[nt])
first[nt].update(first[sym] - {EPSILON})
if len(first[nt]) != before:
changed = True
if EPSILON not in first[sym]:
all_derive_epsilon = False
break
if all_derive_epsilon:
if EPSILON not in first[nt]:
first[nt].add(EPSILON)
changed = True
return first
def first_of_sequence(seq, first):
result = set()
all_epsilon = True
for sym in seq:
if sym not in nonterminals:
result.add(sym)
all_epsilon = False
break
result.update(first[sym] - {EPSILON})
if EPSILON not in first[sym]:
all_epsilon = False
break
if all_epsilon:
result.add(EPSILON)
return result
def compute_follow(grammar, first, start_symbol):
follow = {nt: set() for nt in grammar}
follow[start_symbol].add(END)
changed = True
while changed:
changed = False
for nt, productions in grammar.items():
for prod in productions:
if prod == (EPSILON,):
continue
for i, sym in enumerate(prod):
if sym not in nonterminals:
continue
rest = prod[i+1:]
if rest:
first_rest = first_of_sequence(rest, first)
before = len(follow[sym])
follow[sym].update(first_rest - {EPSILON})
if EPSILON in first_rest:
follow[sym].update(follow[nt])
if len(follow[sym]) != before:
changed = True
else:
before = len(follow[sym])
follow[sym].update(follow[nt])
if len(follow[sym]) != before:
changed = True
return follow
first_sets = compute_first(grammar)
follow_sets = compute_follow(grammar, first_sets, start_symbol)
print("FIRST সেট:")
for nt in ["E", "E'", "T", "T'", "F"]:
print(f" FIRST({nt}) = {sorted(first_sets[nt])}")
print("\nFOLLOW সেট:")
for nt in ["E", "E'", "T", "T'", "F"]:
print(f" FOLLOW({nt}) = {sorted(follow_sets[nt])}")
# হাতে-যাচাই: FIRST(F) = {'(', 'number'} -- কারণ <factor>-এর দুটি বিকল্পই এই দুটি দিয়ে শুরু
assert first_sets["F"] == {"(", "number"}
# হাতে-যাচাই: FOLLOW(F) -- F-এর পরে T' আসে, যা * বা / দিয়ে শুরু হতে পারে অথবা ε (তখন FOLLOW(T) বসে)
assert follow_sets["F"] == {"*", "/", "+", "-", END, ")"}
print("\nহাতে-করা যাচাই (FIRST(F), FOLLOW(F)) মিলেছে -- PASSED")
# LL(1) টেবিল তৈরি
table = {}
conflicts = []
for nt, productions in grammar.items():
for prod in productions:
first_alpha = {EPSILON} if prod == (EPSILON,) else first_of_sequence(prod, first_sets)
for t in first_alpha - {EPSILON}:
key = (nt, t)
if key in table and table[key] != prod:
conflicts.append(key)
table[key] = prod
if EPSILON in first_alpha:
for t in follow_sets[nt]:
key = (nt, t)
if key in table and table[key] != prod:
conflicts.append(key)
table[key] = prod
print(f"\nLL(1) টেবিলে মোট এন্ট্রি: {len(table)}")
print(f"কনফ্লিক্ট পাওয়া গেছে: {len(conflicts)}")
assert len(conflicts) == 0
print("গ্রামারটি নিশ্চিতভাবে LL(1) -- কোনো টেবিল সেলে একাধিক নিয়ম নেই")
FIRST ও FOLLOW সেট গণনা LL(1) পার্সিং-এর ভিত্তি — এই দুটো সেট একটি গ্রামার থেকে মেকানিক্যালি একটি সিদ্ধান্ত-টেবিল তৈরি করতে দেয়, হাতে-লেখা if/elif ছাড়াই। যদি এই টেবিলে কোনো কনফ্লিক্ট না থাকে (উপরের কোডে যেমন দেখানো হলো), গ্রামারটি প্রমাণিতভাবে LL(1) — একটিমাত্র লুকঅ্যাহেড টোকেন দিয়েই প্রতিটি সিদ্ধান্ত দ্ব্যর্থহীনভাবে নেওয়া যায়।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ FIRST(E') = {+, -, ε} কেন, অথচ FIRST(E) = {(, number}-এ ε নেই?
E'-এর একটি প্রোডাকশন সরাসরি ε (কোনো ইনপুট consume না করে থামা) — তাই
ε তার FIRST সেটে থাকবেই। কিন্তু E ::= T E'-এর একমাত্র প্রোডাকশনে প্রথম চিহ্ন
T, আর T কখনো ε ডেরাইভ করতে পারে না (T-এর প্রতিটি প্রোডাকশন শেষমেশ
একটি number বা বন্ধনীতে গিয়ে থামে) — তাই E-এর FIRST-এ ε কখনো
যোগ হবে না, যেহেতু E-এর প্রথম (এবং একমাত্র) চিহ্ন কখনো ε ডেরাইভ করে না।
প্র ০২ FOLLOW(F)-এ + এবং - কীভাবে আসে, যেখানে F-এর প্রোডাকশনে + বা - কোথাও নেই?
FOLLOW সেট গণনায় শুধু নিজের প্রোডাকশন দেখলে হয় না — F যেখানেই ব্যবহৃত হয়েছে (এখানে
T ::= F T') সেখানে তার ঠিক পরে কী আসতে পারে তা দেখতে হয়। T' নিজেও
ε ডেরাইভ করতে পারে (T'-এর একটি প্রোডাকশন সরাসরি ε), অর্থাৎ F-এর পরে T' "কিছুই না"-ও হতে
পারে — সেক্ষেত্রে F-এর পরে যা আসে তা আসলে T-এর পরে যা আসে তা-ই হবে, অর্থাৎ FOLLOW(T)। আর FOLLOW(T)-এ
+/- আছে (কারণ E ::= T E'-এ T-এর পরে E' আসে, যা +/- দিয়ে শুরু হতে পারে) — তাই সেই +/- চেইন হয়ে
FOLLOW(F)-এও পৌঁছে যায়।
প্র ০৩ যদি এই গ্রামারে F-এর একটি নতুন প্রোডাকশন F ::= number যোগ করার বদলে ভুল করে E ::= number-ও যোগ করা হতো, LL(1) টেবিলে কী ঘটত?
একটি কনফ্লিক্ট তৈরি হতো। বর্তমানে M[E, number] সেলে একটিই নিয়ম আছে: E ::= T E'
(কারণ number, FIRST(T)-এর সদস্য)। যদি E ::= number-ও যোগ করা হতো, তাহলে সেই একই সেল
M[E, number]-এ দুটি নিয়ম পড়ত — এটাই ঠিক এই কোডের conflicts
তালিকা যেভাবে ধরে ফেলবে (একই key-তে দ্বিতীয়বার ভিন্ন প্রোডাকশন যোগ করার চেষ্টা হলে সেটি লগ হয়) —
পার্সার তখন একটি টোকেন দেখে সিদ্ধান্ত নিতে পারবে না কোন নিয়মটি প্রয়োগ করবে, অর্থাৎ গ্রামারটি আর LL(1)
থাকবে না।
অনুশীলন
-
চিন্তা করুন: FOLLOW(E) = {$, )} কেন — E-এর পরে ঠিক কোথায় কোথায় এই দুটো চিহ্ন আসতে পারে গ্রামারে খুঁজে বের করুন।
দুটি জায়গায়: (১) E স্টার্ট সিম্বল, তাই একটি সম্পূর্ণ ডেরিভেশনের শেষে E-এর পরে ইনপুট শেষ হয়ে যায় — সেই এন্ড-অফ-ইনপুট নির্দেশ করতে
$যোগ করা হয়। (২)F ::= ( E )প্রোডাকশনে E-এর ঠিক পরে টার্মিনাল)আছে — তাই)-ও FOLLOW(E)-তে যোগ হয়। এই দুটি ছাড়া গ্রামারে আর কোথাও E ব্যবহৃত হয়নি, তাই FOLLOW(E) = {"{"}$, ){"}"} -এর বাইরে আর কিছু নেই। -
পরীক্ষা করুন: উপরের কোড সেলে grammar ডিকশনারিতে "T'"-এর প্রোডাকশন তালিকা থেকে (EPSILON,) এন্ট্রিটি সাময়িকভাবে বাদ দিয়ে Run চাপলে কী ঘটে দেখুন।
FIRST(T') থেকে ε বাদ পড়বে, যার ফলে FOLLOW গণনায় F-এর পরে T'-এর মাধ্যমে FOLLOW(T) আর propagate হবে না — FOLLOW(F) ছোট হয়ে যাবে (শুধু {"{"}*, /{"}"} থাকবে, +/-/$/) হারিয়ে যাবে), কারণ কোডে এখন আর "T' যদি ε ডেরাইভ করতে পারে তাহলে FOLLOW(T) যোগ করো" এই শর্তটি সত্যি হবে না। এটি স্পষ্টভাবে দেখায় কেন ε-উৎপাদনকারী প্রোডাকশন বাদ দেওয়া চলবে না — এটি গ্রামারের প্রকৃত ভাষা (এবং তার FOLLOW সেট) বদলে দেয়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ L24-এ আমরা টপ-ডাউন থেকে সরে গিয়ে বটম-আপ শিফট-রিডিউস পার্সিং-এ যাব।
- L22 · রিকার্সিভ ডিসেন্ট পার্সিং পূর্বের পাঠ এই পাঠের গ্রামার সরাসরি L22-এর left-recursion-eliminated গ্রামার থেকে নেওয়া।
- Discrete Mathematics কোর্স তাত্ত্বিক ভিত্তি ফিক্সড-পয়েন্ট অ্যালগরিদম ও সেট-থিওরেটিক ইউনিয়ন/ক্লোজার ধারণার তাত্ত্বিক ভিত্তি এই কোর্সে আছে।