বটম-আপ পার্সিং — শিফট-রিডিউস
এই পাঠে যা শিখবেন
- শিফট-রিডিউস পার্সিং-এর মূল স্ট্র্যাটেজি — স্ট্যাক-ভিত্তিক বটম-আপ রিকগনিশন
- শিফট বনাম রিডিউস ধাপের সুনির্দিষ্ট সংজ্ঞা ও পার্স সম্পূর্ণ হওয়ার শর্ত
- শিফট-রিডিউস ও রিডিউস-রিডিউস কনফ্লিক্ট — কেন এগুলো ঘটে
- একটি real ShiftReduceParser ক্লাস লিখে সম্পূর্ণ স্ট্যাক ট্রেস উৎপাদন
১ · শিফট-রিডিউস পার্সিং কী
শিফট-রিডিউস পার্সিংShift-Reduce Parsingএকটি স্ট্যাক ব্যবহার করে বটম-আপ পার্সিং-এর স্ট্যান্ডার্ড কৌশল — শিফট বা রিডিউস, দুটির একটি ধাপ প্রতিবার।
হলো M5/L21-এ আলোচিত বটম-আপ পার্সিং-এর সাধারণ স্ট্র্যাটেজি — L25 ও L26-এর নির্দিষ্ট অ্যালগরিদম (LR(0), SLR,
LALR) এই একই মেকানিজমের উপর ভিত্তি করে তৈরি। এটি একটি স্ট্যাক (../dsa/-এর
একটি কোর স্ট্রাকচারের সরাসরি প্রয়োগ) ও বাকি ইনপুট নিয়ে কাজ করে। প্রতিটি ধাপে দুটি সম্ভাব্য অ্যাকশন —
- শিফট (Shift): পরবর্তী ইনপুট টোকেনটি সরাসরি স্ট্যাকের উপর পুশ করা হয়।
- রিডিউস (Reduce): স্ট্যাকের টপ-এর চিহ্নগুলো যদি কোনো গ্রামার প্রোডাকশনের ডান-পাশের (right-hand side) সাথে হুবহু মেলে, সেগুলোকে পপ করে বদলে সেই প্রোডাকশনের বাম-পাশের নন-টার্মিনাল পুশ করা হয় — একটি ছোট অংশকে "চিনে নিয়ে ভাঁজ করে ফেলা", ঠিক L21-এর বটম-আপ বর্ণনার আক্ষরিক বাস্তবায়ন।
পার্স সফলভাবে সম্পূর্ণ হয় যখন স্ট্যাকে শুধু স্টার্ট সিম্বল থাকে এবং ইনপুট সম্পূর্ণ consume হয়ে গেছে।
২ · শিফট-রিডিউস কনফ্লিক্ট
বটম-আপ পার্সিং-এর কেন্দ্রীয় প্র্যাক্টিক্যাল চ্যালেঞ্জ — কখনো কখনো একাধিক অ্যাকশন একসাথে ব্যাকরণগতভাবে বৈধ হয়ে যায়।
কোনো এক মুহূর্তে শিফট করা এবং রিডিউস করা — দুটোই ব্যাকরণগতভাবে বৈধ। পার্সারকে একটি নিয়ম (কনভেনশন) দিয়ে সিদ্ধান্ত নিতে হয় কোনটি বেছে নেবে।
স্ট্যাকের টপ একাধিক ভিন্ন প্রোডাকশনের ডান-পাশের সাথে একসাথে মিলে যায় — একাধিক রিডাকশন সম্ভব, কোনটি সঠিক তা অস্পষ্ট।
৩ · হাতে-করা উদাহরণ — "2 + 3 + 4"
একটি ইচ্ছাকৃতভাবে দ্ব্যর্থক টয় গ্রামার ব্যবহার করা যাক — S ::= S + S | number (এটি সরাসরি
L14-এর অ্যাম্বিগুয়াস গ্রামারের ধরন, বেছে নেওয়া হয়েছে ঠিক একটি real শিফট-রিডিউস কনফ্লিক্ট দেখানোর জন্য)।
"2 + 3 + 4" ট্রেস করলে একটি নির্দিষ্ট মুহূর্তে স্ট্যাকে থাকে [S, +, S] (মান 2+3 এর জন্য) এবং
পরবর্তী ইনপুট টোকেন আরেকটি + — এখানে দুটোই সম্ভব:
- শিফট করলে পার্সার
S + (S + S)আকারে গড়া শুরু করবে (ডান-সহযোজন)। - রিডিউস করলে (S+S কে S-এ ভাঁজ করে) পার্সার
(S + S) + Sআকারে গড়বে (বাম-সহযোজন)।
এটিই real শিফট-রিডিউস কনফ্লিক্ট — গ্রামার নিজে থেকে বলে দেয় না কোনটি সঠিক। এই লেসনে সমাধান করা হয়েছে একটি স্টেটেড কনভেনশন দিয়ে: রিডিউসকে অগ্রাধিকার দেওয়া হয়েছে, কারণ যোগ (+) সাধারণত বাম-সহযোজিত (left-associative) — অর্থাৎ "2+3+4" মানে "(2+3)+4", ঠিক যেমন বেশিরভাগ ভাষায় বাস্তবে হয়।
৪ · সম্পূর্ণ স্ট্যাক ট্রেস
নিচের কোড সেলে একটি real ShiftReduceParser ক্লাস লেখা হয়েছে যা এই কনভেনশন অনুসরণ করে "2+3+4"-এর
প্রতিটি ধাপে ঠিক কী অ্যাকশন নিল এবং তার পরে স্ট্যাকের অবস্থা কী ছিল তা প্রিন্ট করে — চিহ্নিত কনফ্লিক্ট
পয়েন্টটিও স্পষ্টভাবে দেখানো হয়েছে।
# টয় গ্রামার (ইচ্ছাকৃতভাবে দ্ব্যর্থক, শিফট-রিডিউস কনফ্লিক্ট দেখানোর জন্য):
# S ::= S + S | number
class ShiftReduceParser:
"""স্ট্যাক (../dsa/-এর ক্লাসিক স্ট্রাকচার) ব্যবহার করে -- প্রতিটি ধাপে হয় শিফট,
নয় রিডিউস। কনফ্লিক্টের সমাধান: বাম-সহযোজন (left-associativity) নিশ্চিত করতে
S + S . + এ সবসময় রিডিউসকে অগ্রাধিকার দেওয়া হয় (stated convention)।"""
def __init__(self, tokens):
self.stack = []
self.input = list(tokens)
self.trace = []
def can_reduce_number(self):
return bool(self.stack) and self.stack[-1].isdigit()
def can_reduce_splus_s(self):
return len(self.stack) >= 3 and self.stack[-3:] == ["S", "+", "S"]
def step(self):
# নিয়ম ১: স্ট্যাকের টপে একটি বেয়ার number থাকলে সাথে সাথে রিডিউস (S -> number)
if self.can_reduce_number():
self.stack[-1:] = ["S"]
self.trace.append(("reduce S->number", list(self.stack), list(self.input)))
return True
# নিয়ম ২: স্ট্যাকে S + S থাকলে -- এটি একটি শিফট-রিডিউস কনফ্লিক্ট যদি ইনপুটে
# আরেকটি '+' অপেক্ষা করে থাকে (শিফট করা এবং রিডিউস করা দুটোই ব্যাকরণগতভাবে বৈধ)।
if self.can_reduce_splus_s():
if self.input and self.input[0] == "+":
self.trace.append((
"CONFLICT: stack=[S,+,S], lookahead='+' -> রিডিউস বেছে নেওয়া হলো (বাম-সহযোজন কনভেনশন)",
list(self.stack), list(self.input)))
self.stack[-3:] = ["S"]
self.trace.append(("reduce S->S+S", list(self.stack), list(self.input)))
return True
# নিয়ম ৩: নাহলে পরবর্তী ইনপুট টোকেন শিফট করো
if self.input:
tok = self.input.pop(0)
self.stack.append(tok)
self.trace.append((f"shift '{tok}'", list(self.stack), list(self.input)))
return True
return False
def parse(self):
while self.step():
pass
return self.stack == ["S"] and not self.input
tokens = ["2", "+", "3", "+", "4"]
parser = ShiftReduceParser(tokens)
accepted = parser.parse()
print(f"{'action':<70s} {'stack':<20s} input")
for action, stack, remaining in parser.trace:
print(f"{action:<70s} {str(stack):<20s} {remaining}")
print(f"\nParse সফল হয়েছে: {accepted}, চূড়ান্ত স্ট্যাক: {parser.stack}")
assert accepted and parser.stack == ["S"]
result = 2 + 3 + 4 # left-associative মান নির্ণয়ের সাথে ক্রস-চেক
print(f"বাম-সহযোজিত মান নির্ণয়: 2+3+4 = {result}")
assert result == 9
print("ALL OK -- শিফট-রিডিউস ট্রেস ও মান দুটোই মিলেছে")
শিফট-রিডিউস পার্সিং একটি সহজ, স্ট্যাক-ভিত্তিক মেকানিজম, কিন্তু কোন মুহূর্তে শিফট আর কোন মুহূর্তে রিডিউস করতে হবে তা ঠিকভাবে না জানলে কনফ্লিক্ট দেখা দেয়। হাতে-করা কনভেনশন (যেমন এখানে "রিডিউস অগ্রাধিকার") ছোট গ্রামারে কাজ করে, কিন্তু বড়, বাস্তব গ্রামারে এই সিদ্ধান্তগুলো সিস্টেমেটিকভাবে নেওয়া দরকার — ঠিক এটাই L25-L26-এর LR/SLR/LALR অ্যালগরিদম করে, পূর্বনির্মিত স্টেট ব্যবহার করে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ উপরের ট্রেসে প্রথম "reduce S->number" ধাপের ঠিক আগে কেন "shift '2'" ধাপটি দরকার ছিল — সরাসরি "2"-কে S বানানো যেত না?
শিফট-রিডিউস পার্সার কখনো ইনপুট টোকেন সরাসরি রূপান্তর করে না — এটি সবসময় প্রথমে টোকেনটিকে স্ট্যাকে শিফট করে (রক্ষিত/consumed করে), তারপর স্ট্যাকের টপ পরীক্ষা করে দেখে সেটি কোনো প্রোডাকশনের ডান-পাশের সাথে মেলে কি না — মিললে তখন রিডিউস করে। এই দুই-ধাপ প্রক্রিয়াই (শিফট তারপর রিডিউস) শিফট-রিডিউস পার্সিং-এর সংজ্ঞা — ইনপুট টোকেনের সরাসরি রূপান্তর নয়, বরং একটি এক্সপ্লিসিট স্ট্যাক-ভিত্তিক দুই-ধাপ চক্র।
প্র ০২ যদি রিডিউসের বদলে শিফট বেছে নেওয়া হতো (ডান-সহযোজন কনভেনশন), "2+3+4"-এর চূড়ান্ত পার্স ট্রি কেমন হতো, এবং মান কি একই থাকত?
পার্স ট্রি হতো S + (S + S) আকারে — অর্থাৎ "2 + (3 + 4)"। যোগের ক্ষেত্রে গাণিতিকভাবে মান
একই থাকবে (2+(3+4) = 9, ঠিক (2+3)+4-এর মতো), কারণ যোগ সহযোজক (associative)। কিন্তু বিয়োগের মতো
নন-অ্যাসোসিয়েটিভ অপারেটরে এই পার্থক্য গুরুত্বপূর্ণ হয়ে যায় — "12 - 2 - 3"-কে (12-2)-3=7 (বাম-সহযোজন,
সঠিক প্রচলিত অর্থ) বনাম 12-(2-3)=13 (ডান-সহযোজন, ভুল ফলাফল) — তাই কনভেনশন বেছে নেওয়াটা নিছক
তাত্ত্বিক বিষয় নয়, বাস্তব ফলাফলে পার্থক্য তৈরি করে।
প্র ০৩ উপরের গ্রামার S ::= S + S | number-এ can_reduce_number() ও can_reduce_splus_s() একসাথে True হতে পারে কি? কেন বা কেন নয়?
না, কখনো একসাথে True হতে পারে না। can_reduce_number() True হয় যখন স্ট্যাকের একদম টপ
একটি raw digit-string (এখনো "S"-এ রূপান্তরিত হয়নি)। can_reduce_splus_s() True হয় যখন
স্ট্যাকের শেষ তিনটি চিহ্ন হুবহু ["S", "+", "S"]। এই দুটি শর্ত একে অপরের সাথে সাংঘর্ষিক —
যদি টপ একটি raw digit হয়, তার মানে টপ "S" নয়, তাই দ্বিতীয় শর্ত মিলতে পারে না। এই কোডে তাই এটি একটি
রিডিউস-রিডিউস কনফ্লিক্ট নয় — শুধু একটি শিফট-রিডিউস কনফ্লিক্ট (রুল ২-এর
ভেতরে, শিফট বনাম রিডিউস) আছে, যা কোডে স্পষ্টভাবে চিহ্নিত করা হয়েছে।
অনুশীলন
-
চিন্তা করুন: can_reduce_splus_s() ফাংশনটি স্ট্যাকের শেষ তিনটি এন্ট্রি পরীক্ষা করে, প্রথম তিনটি নয়। কেন এটি ঠিক — শিফট-রিডিউস পার্সিং সবসময় স্ট্যাকের কোন প্রান্তে কাজ করে?
শিফট-রিডিউস পার্সিং সবসময় স্ট্যাকের টপ (শেষ প্রান্ত) নিয়ে কাজ করে, কারণ সবচেয়ে সাম্প্রতিক শিফট/রিডিউস করা চিহ্নগুলোই টপে থাকে — এবং একটি "হ্যান্ডল" (reducible সাব-স্ট্রিং) সবসময় সর্বশেষ যোগ হওয়া চিহ্নগুলোর মধ্যেই থাকতে পারে, কারণ পুরোনো (নিচের) চিহ্নগুলো ইতিমধ্যে আগের ধাপেই রিডিউস হয়ে গেছে বা রিডিউসের জন্য অপেক্ষমাণ নয়। এই "সবসময় টপ থেকে দেখা" নিয়মটিই বটম-আপ পার্সিং-এর দক্ষতার মূল ভিত্তি — পুরো স্ট্যাক স্ক্যান করার দরকার নেই।
-
পরীক্ষা করুন: উপরের কোড সেলে tokens-এর মান ["2", "+", "3"] করে Run চেপে দেখুন — মাত্র একটি + থাকায় কি কোনো কনফ্লিক্ট লাইন দেখা যায়?
না, কোনো CONFLICT লাইন দেখা যাবে না। ট্রেস হবে: shift '2' -> reduce S->number -> shift '+' -> shift '3' -> reduce S->number -> reduce S->S+S -> accepted। স্ট্যাক
[S, +, S]-এ পৌঁছানোর সময় ইনপুট ইতিমধ্যে খালি (আর কোনো টোকেন বাকি নেই) — তাই কোডের শর্তif self.input and self.input[0] == "+"False হয়, অর্থাৎ শিফট করার কোনো সম্ভাবনাই নেই, তাই এটি real কনফ্লিক্ট নয় (রিডিউস ছাড়া আর কোনো বিকল্প ছিল না)।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ L25-এ আমরা কনফ্লিক্ট সিস্টেমেটিকভাবে সমাধানের জন্য LR(0)/SLR আইটেম শিখব।
- L21 · পার্সিং ওভারভিউ — টপ-ডাউন বনাম বটম-আপ প্রাসঙ্গিক পাঠ এই পাঠের বটম-আপ/রাইটমোস্ট-ডেরিভেশন-ইন-রিভার্স সম্পর্কের বড় ছবি।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স স্ট্যাক ডেটা স্ট্রাকচারের সরাসরি বাস্তব প্রয়োগ — এই কোর্সে স্ট্যাকের ভিত্তি শেখানো হয়েছে।