পার্স ট্রি ও অ্যাম্বিগুইটি
এই পাঠে যা শিখবেন
- পার্স ট্রি-এর আনুষ্ঠানিক সংজ্ঞা এবং 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)। দুটোই এই গ্রামার অনুযায়ী সম্পূর্ণ বৈধ — গ্রামারটি
নিজে থেকে কোনোটাকে "ভুল" বলে না, এটাই অ্যাম্বিগুইটির প্রকৃত সমস্যা।
# পার্স ট্রি = (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
উত্তর দেয়।
অ্যাম্বিগুইটি কোনো তাত্ত্বিক কৌতূহল নয় — একই স্ট্রিং-এর একাধিক পার্স ট্রি মানে একাধিক ভিন্ন অর্থ, যা একটি
প্রোগ্রামিং ল্যাঙ্গুয়েজের জন্য গ্রহণযোগ্য নয় (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-ডিজাইন থেকে সম্পূর্ণ স্বাধীন।
অনুশীলন
-
চিন্তা করুন: নাইভ গ্রামার
<expr> ::= <expr>+<expr> | <expr>*<expr> | number-এ "2 + 3 * 4"-এর জন্য কি তৃতীয় কোনো ভিন্ন পার্স ট্রি সম্ভব? কেন বা কেন নয়?না। এই স্ট্রিং-এ ঠিক একটি
+ও একটি*আছে — root হয়+হবে (তখন*একটি সাব-ট্রিতে চলে যায়, একটিই বিন্যাস সম্ভব) অথবা root*হবে (একইভাবে একটিই বিন্যাস)। মাত্র দুটো অপারেটর থাকায় সম্ভাব্য root-এর পছন্দ মাত্র দুটো, তাই ঠিক দুটো ভিন্ন পার্স ট্রি — তৃতীয় কোনো কাঠামোগতভাবে ভিন্ন সম্ভাবনা নেই। -
কোড এক্সটেন্ড করুন: উপরের কোড সেলে "2 * 3 + 4"-এর জন্য দুটো অ্যাম্বিগুয়াস-গ্রামার ট্রি ও একটি লেয়ার্ড-গ্রামার ট্রি বানিয়ে evaluate করে দেখুন কোন মান আসে।
অ্যাম্বিগুয়াস গ্রামারে দুটো সম্ভাব্য মান:
(2*3)+4 = 10এবং2*(3+4) = 14। লেয়ার্ড গ্রামার (যেখানে*সবসময়<term>-এর ভেতরে আগে গ্রুপ হয়) শুধুমাত্র10দেবে — স্ট্যান্ডার্ড গণিতের নিয়ম (গুণ আগে) অনুযায়ী এটিই সঠিক উত্তর।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ — চমস্কি হায়ারার্কি — গ্রামারগুলোকে তাদের generative ক্ষমতা অনুযায়ী শ্রেণিবদ্ধ করবে এবং দেখাবে কেন প্রোগ্রামিং ল্যাঙ্গুয়েজের সিনট্যাক্স ইচ্ছাকৃতভাবে কনটেক্সট-ফ্রি রাখা হয়।
- পাঠ ১৩ · কনটেক্সট-ফ্রি গ্রামার ও ডেরিভেশন পূর্ববর্তী পাঠ পার্স ট্রি আসলে যে derivation প্রক্রিয়ার ট্রি-প্রতিনিধিত্ব, সেই মূল ধারণাটি এখানে বিস্তারিত।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স পার্স ট্রি আক্ষরিক অর্থেই একটি ট্রি ডেটা স্ট্রাকচার — এই কোর্সে ট্রি ট্রাভার্সালের ভিত্তি শেখানো হয়েছে।