পাঠ ২৪ · ৫৮-এর মধ্যে · মডিউল ৫
Home / Courses / Concepts of Programming Languages & Compiler Design / বটম-আপ পার্সিং — শিফট-রিডিউস

বটম-আপ পার্সিং — শিফট-রিডিউস

Bottom-up parsing — shift-reduce
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • শিফট-রিডিউস পার্সিং-এর মূল স্ট্র্যাটেজি — স্ট্যাক-ভিত্তিক বটম-আপ রিকগনিশন
  • শিফট বনাম রিডিউস ধাপের সুনির্দিষ্ট সংজ্ঞা ও পার্স সম্পূর্ণ হওয়ার শর্ত
  • শিফট-রিডিউস ও রিডিউস-রিডিউস কনফ্লিক্ট — কেন এগুলো ঘটে
  • একটি 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"-এর প্রতিটি ধাপে ঠিক কী অ্যাকশন নিল এবং তার পরে স্ট্যাকের অবস্থা কী ছিল তা প্রিন্ট করে — চিহ্নিত কনফ্লিক্ট পয়েন্টটিও স্পষ্টভাবে দেখানো হয়েছে।

Python
# টয় গ্রামার (ইচ্ছাকৃতভাবে দ্ব্যর্থক, শিফট-রিডিউস কনফ্লিক্ট দেখানোর জন্য):
#   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 -- শিফট-রিডিউস ট্রেস ও মান দুটোই মিলেছে")

    
মূল কথা · Key takeaway

শিফট-রিডিউস পার্সিং একটি সহজ, স্ট্যাক-ভিত্তিক মেকানিজম, কিন্তু কোন মুহূর্তে শিফট আর কোন মুহূর্তে রিডিউস করতে হবে তা ঠিকভাবে না জানলে কনফ্লিক্ট দেখা দেয়। হাতে-করা কনভেনশন (যেমন এখানে "রিডিউস অগ্রাধিকার") ছোট গ্রামারে কাজ করে, কিন্তু বড়, বাস্তব গ্রামারে এই সিদ্ধান্তগুলো সিস্টেমেটিকভাবে নেওয়া দরকার — ঠিক এটাই 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" নয়, তাই দ্বিতীয় শর্ত মিলতে পারে না। এই কোডে তাই এটি একটি রিডিউস-রিডিউস কনফ্লিক্ট নয় — শুধু একটি শিফট-রিডিউস কনফ্লিক্ট (রুল ২-এর ভেতরে, শিফট বনাম রিডিউস) আছে, যা কোডে স্পষ্টভাবে চিহ্নিত করা হয়েছে।

অনুশীলন

  1. চিন্তা করুন: can_reduce_splus_s() ফাংশনটি স্ট্যাকের শেষ তিনটি এন্ট্রি পরীক্ষা করে, প্রথম তিনটি নয়। কেন এটি ঠিক — শিফট-রিডিউস পার্সিং সবসময় স্ট্যাকের কোন প্রান্তে কাজ করে?

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

  2. পরীক্ষা করুন: উপরের কোড সেলে 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-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L23 · LL(1) পার্সিং — FIRST ও FOLLOW সেট