পাঠ ১৪ · ৫৮-এর মধ্যে · মডিউল ৩
Home / Courses / Concepts of Programming Languages & Compiler Design / পার্স ট্রি ও অ্যাম্বিগুইটি

পার্স ট্রি ও অ্যাম্বিগুইটি

Parse trees & ambiguity
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • পার্স ট্রি-এর আনুষ্ঠানিক সংজ্ঞা এবং derivation-এর সাথে এর সম্পর্ক
  • অ্যাম্বিগুয়াস গ্রামার কী এবং কেন এটি একটি বাস্তব, গুরুতর সমস্যা
  • "2 + 3 * 4"-এর দুটি genuinely ভিন্ন পার্স ট্রি হাতে-কলমে তৈরি ও evaluate করা
  • Precedence-লেয়ারিং কীভাবে অ্যাম্বিগুইটি দূর করে — L12-এর গ্রামারে ফিরে গিয়ে যাচাই

১ · পার্স ট্রি কী

একটি পার্স ট্রিParse Tree (Syntax Tree / Derivation Tree)একটি derivation-এর ট্রি-প্রতিনিধিত্ব — root স্টার্ট সিম্বল, প্রতিটি internal নোড একটি নন-টার্মিনাল যার children সেই নোডে প্রয়োগ করা প্রোডাকশন রুলের ডানপাশের সিম্বলগুলো। (syntax tree/derivation tree নামেও পরিচিত) হলো L13-এর derivation প্রক্রিয়ার একটি ট্রি প্রতিনিধিত্ব — DSA কোর্সের ট্রি ডেটা স্ট্রাকচারের সরাসরি প্রয়োগ। root নোড স্টার্ট সিম্বল, প্রতিটি internal নোড একটি নন-টার্মিনাল যার children সেই নোডে প্রয়োগ করা প্রোডাকশন রুলের ডানপাশের সিম্বলগুলো, এবং leaves (বাম থেকে ডানে পড়লে) derived টার্মিনাল স্ট্রিং-টি বানায়। একটি derivation-এর প্রতিটি ধাপ একটি নির্দিষ্ট ট্রি-নোডে "সন্তান যোগ করা"-র সমতুল্য — L13-এর derivation ও এই পাঠের পার্স ট্রি আসলে একই তথ্যের দুটি ভিন্ন Views।

২ · অ্যাম্বিগুয়াস গ্রামার — একটি বাস্তব সমস্যা

একটি গ্রামার অ্যাম্বিগুয়াস যদি এর ভাষার কোনো স্ট্রিং-এর একাধিক ভিন্ন পার্স ট্রি থাকে (সমতুল্যভাবে, একাধিক ভিন্ন leftmost derivation থাকে)। এটি নিছক একটি কসমেটিক/স্টাইলিস্টিক সমস্যা নয় — ভিন্ন পার্স ট্রি মানেই একই স্ট্রিং-এর জন্য ভিন্ন অর্থ (ভিন্ন অপারেটর গ্রুপিং), অর্থাৎ প্রোগ্রামের সিমান্টিক্স (L02) সত্যিকার অর্থেই অস্পষ্ট হয়ে যায় — কোন গ্রুপিং "সঠিক" তা গ্রামার নিজে থেকে বলতে পারে না।

৩ · ক্লাসিক উদাহরণ — নাইভ গ্রামারে "2 + 3 * 4"

নিচের নাইভ, single-level গ্রামারটি বিবেচনা করুন —

<expr> ::= <expr> + <expr> | <expr> * <expr> | number

এই গ্রামারটি "2 + 3 * 4" স্ট্রিং-এর জন্য দুটো ভিন্ন পার্স ট্রি দেয় — একটি +-কে root বানায় (তাহলে * ডানদিকের সাব-ট্রিতে চলে যায়, ফলাফল 2 + (3 * 4) = 14), আরেকটি *-কে root বানায় (তাহলে + বামদিকের সাব-ট্রিতে চলে যায়, ফলাফল (2 + 3) * 4 = 20)। দুটোই এই গ্রামার অনুযায়ী সম্পূর্ণ বৈধ — গ্রামারটি নিজে থেকে কোনোটাকে "ভুল" বলে না, এটাই অ্যাম্বিগুইটির প্রকৃত সমস্যা।

Tree A — root: * * + 4 2 3 (2 + 3) * 4 = 20 Tree B — root: + + 2 * 3 4 2 + (3 * 4) = 14
একই স্ট্রিং "2 + 3 * 4", একই নাইভ গ্রামার — কিন্তু দুটো genuinely ভিন্ন গঠনের পার্স ট্রি, দুটো ভিন্ন সংখ্যাসূচক ফলাফল। নিচের কোড সেল এই দুটো ট্রি ও তাদের evaluate করা মান বাস্তবে গণনা করবে।
Python
# পার্স ট্রি = (label, children) নেস্টেড টাপল -- children ফাঁকা মানে leaf
# len(children)==1 মানে পাস-থ্রু নন-টার্মিনাল রুল (যেমন <expr>::=<term>)
# len(children)==3 মানে বাইনারি অপারেটর রুল (operand, operator-leaf, operand)

def evaluate_parse_tree(node):
    label, children = node
    if not children:
        return float(label)                        # সংখ্যা-লিফ
    if len(children) == 1:
        return evaluate_parse_tree(children[0])     # পাস-থ্রু নন-টার্মিনাল
    if len(children) == 3:
        left, op_node, right = children
        op = op_node[0]
        left_val = evaluate_parse_tree(left)
        right_val = evaluate_parse_tree(right)
        if op == '+': return left_val + right_val
        if op == '-': return left_val - right_val
        if op == '*': return left_val * right_val
        if op == '/': return left_val / right_val
    raise ValueError(f"অপ্রত্যাশিত নোড আকৃতি: {node}")

# --- নাইভ অ্যাম্বিগুয়াস গ্রামার: <expr> ::= <expr>+<expr> | <expr>*<expr> | number ---

tree_A = ('<expr>', [                                          # গ্রুপিং: (2 + 3) * 4
    ('<expr>', [('<expr>', [('2', [])]), ('+', []), ('<expr>', [('3', [])])]),
    ('*', []),
    ('<expr>', [('4', [])]),
])

tree_B = ('<expr>', [                                          # গ্রুপিং: 2 + (3 * 4)
    ('<expr>', [('2', [])]),
    ('+', []),
    ('<expr>', [('<expr>', [('3', [])]), ('*', []), ('<expr>', [('4', [])])]),
])

result_A = evaluate_parse_tree(tree_A)
result_B = evaluate_parse_tree(tree_B)

print('একই স্ট্রিং "2 + 3 * 4"-এর দুটি ভিন্ন পার্স ট্রি (নাইভ অ্যাম্বিগুয়াস গ্রামার):\n')
print(f"  Tree A -- (2 + 3) * 4  →  evaluate_parse_tree(tree_A) = {result_A}")
print(f"  Tree B -- 2 + (3 * 4)  →  evaluate_parse_tree(tree_B) = {result_B}")
print(f"\n  ফলাফল ভিন্ন কিনা: {result_A != result_B}  -- একই ইনপুট, একই গ্রামার, দুই রকম অর্থ!")

# --- L12-এর লেয়ার্ড আন-অ্যাম্বিগুয়াস গ্রামার: <expr>/<term>/<factor> ---
# এই গ্রামারে "2 + 3 * 4"-এর জন্য মাত্র ONE বৈধ পার্স ট্রি সম্ভব

tree_correct = ('<expr>', [
    ('<expr>', [('<term>', [('<factor>', [('2', [])])])]),
    ('+', []),
    ('<term>', [
        ('<term>', [('<factor>', [('3', [])])]),
        ('*', []),
        ('<factor>', [('4', [])]),
    ]),
])

result_correct = evaluate_parse_tree(tree_correct)
print(f"\nL12-এর লেয়ার্ড গ্রামার -- একমাত্র বৈধ পার্স ট্রি:")
print(f"  evaluate_parse_tree(tree_correct) = {result_correct}")
print(f"  স্ট্যান্ডার্ড precedence (multiply আগে) মিলছে কিনা: {result_correct == 14.0}")

    

৪ · অ্যাম্বিগুইটি সমাধান — Precedence লেয়ারিং

L12-এ ব্যবহৃত <expr>/<term>/<factor> তিন-স্তরের গ্রামারটি ঠিক এই সমস্যার সমাধান হিসেবে ডিজাইন করা — <term>-কে <expr>-এর ভেতরে "নেস্ট" করে রাখার মানে হলো গুণ/ভাগ (<term>-এর কাজ) সবসময় যোগ/বিয়োগের (<expr>-এর কাজ) চেয়ে বেশি "শক্তভাবে" বাঁধা থাকে — গ্রামারের গঠনই নিশ্চিত করে যে গুণ/ভাগ আগে গ্রুপ হবে। ফলে এই গ্রামারের অধীনে "2 + 3 * 4"-এর জন্য একটিই বৈধ পার্স ট্রি সম্ভব — উপরের কোড সেল ঠিক এই ট্রি-টি বাস্তবে গণনা করে নিশ্চিত করেছে যে এটি সঠিক precedence মেনে 14 উত্তর দেয়।

মূল কথা · Key takeaway

অ্যাম্বিগুইটি কোনো তাত্ত্বিক কৌতূহল নয় — একই স্ট্রিং-এর একাধিক পার্স ট্রি মানে একাধিক ভিন্ন অর্থ, যা একটি প্রোগ্রামিং ল্যাঙ্গুয়েজের জন্য গ্রহণযোগ্য নয় (L01-এর ভাষায়: প্রতিটি বৈধ প্রোগ্রামের একটিমাত্র অর্থ থাকা আবশ্যক)। সমাধান হলো গ্রামারকে precedence অনুযায়ী স্তরে সাজানো — L12-এর <expr>/ <term>/<factor> গ্রামার এর একটি বাস্তব, কাজ-করা উদাহরণ।

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

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

প্র ০১ যদি একটি প্রোগ্রামিং ল্যাঙ্গুয়েজের গ্রামার অ্যাম্বিগুয়াস থেকে যায়, তাহলে বাস্তবে একটি কম্পাইলার/ইন্টারপ্রেটার কী করবে?

একটি বাস্তব পার্সার (M5) সবসময় ঠিক একটি নির্দিষ্ট পার্স ট্রি বেছে নেবে — কিন্তু কোনটা বাছবে তা নির্ভর করবে পার্সারের implementation-নির্দিষ্ট বিস্তারিত (যেমন কোন রুল আগে চেষ্টা করা হয়) উপর, ভাষার আনুষ্ঠানিক সংজ্ঞার উপর নয়। এর মানে একই সোর্স কোড বিভিন্ন কম্পাইলারে বিভিন্ন আচরণ দেখাতে পারে — একটি মারাত্মক পোর্টেবিলিটি ও নির্ভরযোগ্যতা সমস্যা (L04-এর ভাষায়)। এই কারণেই বাস্তব ভাষার গ্রামার ডিজাইন করার সময় অ্যাম্বিগুইটি সরিয়ে ফেলা (এই পাঠের precedence-layering কৌশলের মতো) একটি আবশ্যকীয় ধাপ, ঐচ্ছিক নয়।

প্র ০২ "2 + 3 * 4"-এ বন্ধনী দিয়ে "2 + (3 * 4)" লিখলে কি নাইভ গ্রামারের অ্যাম্বিগুইটি সমস্যা দূর হয়ে যায়?

শুধু ওই একটি নির্দিষ্ট ইনপুট স্ট্রিং-এর জন্য একটি নির্দিষ্ট গ্রুপিং জোর করে বলা হলো — কিন্তু গ্রামারটি নিজে এখনো অ্যাম্বিগুয়াস, কারণ অন্য অসংখ্য ইনপুটে (যেমন বন্ধনী ছাড়া "2 + 3 * 4 + 5") এখনো একাধিক পার্স ট্রি সম্ভব থাকবে। একটি গ্রামারের অ্যাম্বিগুইটি হলো গ্রামারের একটি বৈশিষ্ট্য (এর ভাষার যেকোনো একটি স্ট্রিং-এ একাধিক পার্স ট্রি থাকলেই যথেষ্ট) — একটি নির্দিষ্ট ইনপুটকে বন্ধনী দিয়ে "ঠিক" করা গ্রামারটিকে নিজে unambiguous বানায় না, শুধু ওই একটি ব্যবহারকারীকে সমস্যা এড়াতে সাহায্য করে।

প্র ০৩ কোড সেলের evaluate_parse_tree ফাংশন কীভাবে একই কোড দিয়ে অ্যাম্বিগুয়াস গ্রামারের ট্রি এবং লেয়ার্ড গ্রামারের ট্রি — দুটোই evaluate করতে পারল?

কারণ ফাংশনটি নির্দিষ্ট নন-টার্মিনালের নাম (যেমন <expr> বনাম <term>) নিয়ে মাথা ঘামায় না — এটি শুধু প্রতিটি নোডের children সংখ্যা (arity) দেখে সিদ্ধান্ত নেয়: ০টি children মানে সংখ্যা-লিফ, ১টি মানে পাস-থ্রু (নিচে নেমে যাও), ৩টি মানে বাইনারি অপারেটর প্রয়োগ করো। যেহেতু উভয় গ্রামারেই প্রতিটি রুল এই তিনটির একটির সাথে মেলে (একটি গ্রামার শুধু কম নন-টার্মিনাল স্তর ব্যবহার করে), একই evaluator ফাংশন উভয় ধরনের ট্রি-তেই কাজ করে — এটি দেখায় যে evaluate করার লজিক গ্রামারের precedence-ডিজাইন থেকে সম্পূর্ণ স্বাধীন।

অনুশীলন

  1. চিন্তা করুন: নাইভ গ্রামার <expr> ::= <expr>+<expr> | <expr>*<expr> | number-এ "2 + 3 * 4"-এর জন্য কি তৃতীয় কোনো ভিন্ন পার্স ট্রি সম্ভব? কেন বা কেন নয়?

    না। এই স্ট্রিং-এ ঠিক একটি + ও একটি * আছে — root হয় + হবে (তখন * একটি সাব-ট্রিতে চলে যায়, একটিই বিন্যাস সম্ভব) অথবা root * হবে (একইভাবে একটিই বিন্যাস)। মাত্র দুটো অপারেটর থাকায় সম্ভাব্য root-এর পছন্দ মাত্র দুটো, তাই ঠিক দুটো ভিন্ন পার্স ট্রি — তৃতীয় কোনো কাঠামোগতভাবে ভিন্ন সম্ভাবনা নেই।

  2. কোড এক্সটেন্ড করুন: উপরের কোড সেলে "2 * 3 + 4"-এর জন্য দুটো অ্যাম্বিগুয়াস-গ্রামার ট্রি ও একটি লেয়ার্ড-গ্রামার ট্রি বানিয়ে evaluate করে দেখুন কোন মান আসে।

    অ্যাম্বিগুয়াস গ্রামারে দুটো সম্ভাব্য মান: (2*3)+4 = 10 এবং 2*(3+4) = 14। লেয়ার্ড গ্রামার (যেখানে * সবসময় <term>-এর ভেতরে আগে গ্রুপ হয়) শুধুমাত্র 10 দেবে — স্ট্যান্ডার্ড গণিতের নিয়ম (গুণ আগে) অনুযায়ী এটিই সঠিক উত্তর।

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

আগের পাঠ
L13 · কনটেক্সট-ফ্রি গ্রামার ও ডেরিভেশন