কনটেক্সট-ফ্রি গ্রামার ও ডেরিভেশন
এই পাঠে যা শিখবেন
- কনটেক্সট-ফ্রি গ্রামারের আনুষ্ঠানিক সংজ্ঞা এবং নামের পেছনের কারণ
- ডেরিভেশন প্রক্রিয়া — কীভাবে একটি স্ট্রিং "গ্রামার থেকে উৎপন্ন" প্রমাণ করা হয়
- Leftmost বনাম rightmost derivation, এবং M5-এর পার্সিং কৌশলের সাথে এর সরাসরি সম্পর্ক
- Python দিয়ে একটি সত্যিকারের leftmost-derivation-search ফাংশন লেখা ও চালানো
১ · কনটেক্সট-ফ্রি গ্রামার (CFG) কী
একটি কনটেক্সট-ফ্রি গ্রামারContext-Free Grammar (CFG)এমন একটি গ্রামার যেখানে প্রতিটি প্রোডাকশন রুলের বাম পাশে ঠিক একটিমাত্র নন-টার্মিনাল থাকে — সেই নন-টার্মিনালের রুল যেকোনো প্রসঙ্গে (surrounding context) প্রযোজ্য।-এ
প্রতিটি প্রোডাকশন রুলের বাম পাশে (::=-এর আগে) ঠিক একটিমাত্র নন-টার্মিনাল থাকে —
L12-এর প্রতিটি BNF রুল (<expr> ::= ..., <term> ::= ...) ইতিমধ্যেই এই
নিয়ম মেনে চলে, তাই L12-এর গ্রামারটি একটি CFG।
নামটি কেন "context-free" — একটি নন-টার্মিনালের রুল প্রয়োগ করার সময় সেই নন-টার্মিনালটি বাক্যের কোথায়, কী সিম্বলের পাশে আছে তা কোনো ব্যাপার করে না — রুলটি সবসময় একই থাকে, নন-টার্মিনালের চারপাশের "প্রসঙ্গ" (context) নির্বিশেষে। এর বিপরীতে, আরও সাধারণ গ্রামার টাইপে (L15-এর Type-1 context-sensitive গ্রামার) একটি রুল শুধুমাত্র নির্দিষ্ট প্রসঙ্গে প্রযোজ্য হতে পারে — এই কোর্সে আমরা মূলত CFG নিয়েই কাজ করব, কারণ প্রায় সব বাস্তব প্রোগ্রামিং ল্যাঙ্গুয়েজের সিনট্যাক্স ইচ্ছাকৃতভাবে কনটেক্সট-ফ্রি রাখা হয় (কারণটি L15-এ বিস্তারিত)।
২ · ডেরিভেশন — কীভাবে একটি স্ট্রিং "উৎপন্ন" হয়
ডেরিভেশনDerivationস্টার্ট সিম্বল থেকে শুরু করে বারবার একটি নন-টার্মিনালকে তার কোনো এক প্রোডাকশন রুল দিয়ে প্রতিস্থাপন করে শুধু টার্মিনাল পাওয়া পর্যন্ত চালিয়ে যাওয়ার প্রক্রিয়া। হলো গ্রামারের স্টার্ট সিম্বল থেকে শুরু করে, প্রতি ধাপে একটি নন-টার্মিনালকে তার কোনো একটি বৈধ এক্সপ্যানশন (প্রোডাকশন রুলের ডানপাশ) দিয়ে প্রতিস্থাপন করার প্রক্রিয়া — যতক্ষণ না শুধুমাত্র টার্মিনাল অবশিষ্ট থাকে। শেষে পাওয়া টার্মিনাল স্ট্রিং-টি তখন প্রমাণিত হয় যে এটি গ্রামারের ভাষার একটি সদস্য — "এই গ্রামার থেকে এই স্ট্রিং উৎপন্ন করা সম্ভব।"
৩ · Leftmost বনাম rightmost derivation
প্রতি ধাপে যদি একাধিক নন-টার্মিনাল একসাথে উপস্থিত থাকে, কোনটি আগে এক্সপ্যান্ড করা হবে তার একটি নিয়ম দরকার — এখানেই দুটি স্ট্যান্ডার্ড কনভেনশন আসে (M5-এর সাথে সরাসরি সংযুক্ত, মনে রাখুন) —
প্রতি ধাপে সবচেয়ে বামের নন-টার্মিনাল এক্সপ্যান্ড করা হয় — top-down পার্সার (M5/L22 রিকার্সিভ ডিসেন্ট) স্বাভাবিকভাবেই leftmost derivation তৈরি করে।
প্রতি ধাপে সবচেয়ে ডানের নন-টার্মিনাল এক্সপ্যান্ড করা হয় — bottom-up পার্সার (M5/L24 শিফট-রিডিউস) এর ঠিক বিপরীত ক্রমে rightmost derivation পুনর্গঠন করে।
৪ · Worked example — "2 + 3 * 4"-এর leftmost derivation
L12-এর arithmetic গ্রামার ব্যবহার করে "2 + 3 * 4"-এর একটি সম্পূর্ণ leftmost derivation নিচে
ধাপে ধাপে দেখানো হলো — প্রতিটি ধাপে ঠিক কোন রুল প্রয়োগ হয়েছে তা মন্তব্য হিসেবে আছে।
<expr>
⇒ <expr> + <term> (expr ::= expr + term)
⇒ <term> + <term> (expr ::= term)
⇒ <factor> + <term> (term ::= factor)
⇒ 2 + <term> (factor ::= number [= 2])
⇒ 2 + <term> * <factor> (term ::= term * factor)
⇒ 2 + <factor> * <factor> (term ::= factor)
⇒ 2 + 3 * <factor> (factor ::= number [= 3])
⇒ 2 + 3 * 4 (factor ::= number [= 4])
লক্ষ্য করুন প্রতিটি ধাপে সবচেয়ে বামের নন-টার্মিনালটিই এক্সপ্যান্ড হয়েছে — যেমন ৩য় ধাপে
<term> + <term>-এ প্রথম <term>-টি এক্সপ্যান্ড হয়েছে, দ্বিতীয়টি
নয় (যদিও দ্বিতীয়টিও এক্সপ্যান্ড করা গ্রামারগতভাবে বৈধ হতো)। এই কনভেনশন মেনে চলা মানেই এটি একটি leftmost
derivation।
# একটি real leftmost-derivation-search -- L12-এর গ্রামার দিয়ে
# টার্গেট টোকেন লিস্ট থেকে একটি বৈধ leftmost derivation খুঁজে বের করে
grammar = {
'<expr>': [('<expr>', '+', '<term>'), ('<expr>', '-', '<term>'), ('<term>',)],
'<term>': [('<term>', '*', '<factor>'), ('<term>', '/', '<factor>'), ('<factor>',)],
'<factor>': [('(', '<expr>', ')'), ('number',)],
}
NONTERMINALS = set(grammar.keys())
def symbol_matches(symbol, token):
# 'number' একটি টার্মিনাল "ক্যাটেগরি" -- যেকোনো ডিজিট-টোকেনের সাথে মেলে
if symbol == 'number':
return token.isdigit()
return symbol == token
def terminals_match(seq, target_slice):
if len(seq) != len(target_slice):
return False
return all(symbol_matches(s, t) for s, t in zip(seq, target_slice))
def leftmost_derive(grammar, start, target, max_calls=5000):
"""
সবচেয়ে বামের নন-টার্মিনাল বারবার এক্সপ্যান্ড করে একটি ডেরিভেশন খোঁজে (backtracking সহ)।
প্রতিটি সেন্টেনশিয়াল ফর্মের length target-এর length ছাড়িয়ে গেলে prune করে দেয় --
যেহেতু এই গ্রামারে কোনো ε-প্রোডাকশন নেই, একটি ফর্মের length কখনো কমতে পারে না।
"""
calls = [0]
def search(form, steps):
calls[0] += 1
if calls[0] > max_calls or len(form) > len(target):
return None
idx = next((i for i, s in enumerate(form) if s in NONTERMINALS), None)
if idx is None:
return steps if terminals_match(form, target) else None
if not terminals_match(form[:idx], target[:idx]):
return None # ইতিমধ্যে ফাইনালাইজড prefix টার্গেটের সাথে মেলে না -- ডেড ব্রাঞ্চ
nt = form[idx]
for alt in grammar[nt]:
new_form = form[:idx] + list(alt) + form[idx + 1:]
rule = f"{nt} ::= {' '.join(alt) if alt else 'ε'}"
result = search(new_form, steps + [(rule, list(new_form))])
if result is not None:
return result
return None
return search([start], [])
target = ['2', '+', '3']
derivation = leftmost_derive(grammar, '<expr>', target)
if derivation is None:
print("কোনো derivation পাওয়া যায়নি।")
else:
print(f"টার্গেট: {target} → leftmost derivation পাওয়া গেছে ({len(derivation)} ধাপ):\n")
form = ['<expr>']
print(f" {' '.join(form)}")
for rule, new_form in derivation:
print(f" ⇒ {' '.join(new_form):25s} [{rule}]")
print(f"\nচূড়ান্ত ফর্ম টার্গেট টোকেনের সাথে মিলছে: {terminals_match(derivation[-1][1], target)}")
len(form) > len(target) প্রুনিং কাজ করে কারণ L12-এর গ্রামারে কোনো ε (খালি)
প্রোডাকশন নেই — প্রতিটি রুলের ডানপাশে কমপক্ষে একটি সিম্বল থাকে। তাই একটি সেন্টেনশিয়াল ফর্মের length কখনো
কমতে পারে না, শুধু বাড়তে বা সমান থাকতে পারে — অর্থাৎ এটি target-এর length ছাড়িয়ে গেলে সেই ব্রাঞ্চ থেকে
টার্গেটে পৌঁছানো গাণিতিকভাবেই অসম্ভব।
একটি CFG-তে প্রতিটি রুলের বাম পাশে ঠিক একটি নন-টার্মিনাল থাকে বলেই রুল প্রয়োগ প্রসঙ্গ-নিরপেক্ষ। ডেরিভেশন হলো সেই রুলগুলো বারবার প্রয়োগ করে স্টার্ট সিম্বল থেকে একটি টার্মিনাল স্ট্রিং তৈরি করা — leftmost derivation (M5/L22-এর top-down পার্সিং) ও rightmost derivation (M5/L24-এর bottom-up পার্সিং) এই একই প্রক্রিয়ার দুটি ভিন্ন, কিন্তু সমানভাবে বৈধ, ক্রম।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি গ্রামারকে "context-free" বলা হয় কেন — এখানে "context" ঠিক কীসের কথা বলছে?
"Context" মানে একটি নন-টার্মিনাল বাক্যের কোথায়, কোন সিম্বলের পাশে বসে আছে তার তথ্য। একটি context-free রুলে, একটি নন-টার্মিনালকে কীভাবে এক্সপ্যান্ড করা যাবে তা সম্পূর্ণভাবে সেই নন-টার্মিনালটি নিজে কী তার উপর নির্ভর করে — তার আশেপাশে কী আছে তার উপর নয়। এর বিপরীতে একটি context-sensitive রুল (L15-এর Type-1) এমন হতে পারে "শুধুমাত্র যদি A-এর ঠিক আগে B থাকে, তাহলে A-কে C দিয়ে প্রতিস্থাপন করো" — অর্থাৎ প্রতিস্থাপনের বৈধতা প্রসঙ্গ-নির্ভর। CFG-তে এই ধরনের শর্ত নিষিদ্ধ — প্রতিটি রুল সবসময়, সব প্রসঙ্গে সমানভাবে প্রযোজ্য।
প্র ০২ যদি একটি স্ট্রিং-এর leftmost derivation ও rightmost derivation দুটোই বের করা যায়, তাহলে এই দুটো কি ভিন্ন পার্স ট্রি প্রতিনিধিত্ব করে?
না — যদি গ্রামারটি অ্যাম্বিগুয়াস না হয় (L14-এর টপিক), তাহলে একটি নির্দিষ্ট স্ট্রিং-এর leftmost derivation ও rightmost derivation ঠিক একই পার্স ট্রি প্রতিনিধিত্ব করে — শুধু সেই ট্রি-এর নোডগুলো ভিন্ন ক্রমে ভিজিট/তৈরি করা হয়েছে (একটি বাম থেকে, একটি ডান থেকে)। পার্স ট্রি নিজেই derivation-নিরপেক্ষ — এটি শুধু "কোন রুল কোথায় প্রয়োগ হয়েছে" তা structurally দেখায়, কোন ক্রমে প্রয়োগ হয়েছে তা নয়। L14-এ এই পার্স ট্রি ধারণাটি formalize করা হবে।
প্র ০৩
কোড সেলের leftmost_derive ফাংশনে len(form) > len(target) প্রুনিং যদি সরিয়ে দেওয়া হয়, তাহলে কী সমস্যা হতে পারে?
গ্রামারটি left-recursive (<expr> ::= <expr> + <term>) — এই প্রুনিং ছাড়া
সার্চ অ্যালগরিদম বারবার <expr>-কে আরও বড় করে এক্সপ্যান্ড করতে থাকতে পারে (একটি শাখায়)
যা কখনো টার্গেটের length-এ পৌঁছাবে না অথচ থামবেও না দ্রুত — সার্চ স্পেস exponentially বেড়ে যাবে এবং
অ্যালগরিদম হয় অত্যন্ত ধীর হয়ে যাবে অথবা max_calls সীমায় পৌঁছে ব্যর্থ হবে। length-ভিত্তিক
প্রুনিং একটি গাণিতিকভাবে সঠিক শর্টকাট — যেহেতু কোনো ε-প্রোডাকশন নেই, length কখনো কমে না, তাই অতিরিক্ত
length মানেই সেই শাখা নিশ্চিতভাবে ব্যর্থ।
অনুশীলন
-
হাতে-কলমে করুন: L12-এর গ্রামার ব্যবহার করে
"3 * 4"-এর একটি সম্পূর্ণ leftmost derivation লিখুন, প্রতিটি ধাপে ব্যবহৃত রুল উল্লেখ করে।<expr> ⇒ <term> (expr::=term) ⇒ <term> * <factor> (term::=term*factor) ⇒ <factor> * <factor> (term::=factor) ⇒ 3 * <factor> (factor::=number [=3]) ⇒ 3 * 4 (factor::=number [=4])— মোট ৫টি ডেরিভেশন ধাপ। -
পরীক্ষা করুন: কোড সেলে
target = ['2', '+', '3']-কেtarget = ['(', '2', '+', '3', ')']করে Run চেপে দেখুন — মনে রাখবেন'('-কে literal টোকেন হিসেবে ধরা হয়েছে, ডিজিট হিসেবে নয়।সার্চ সফল হবে এবং একটি ৯-ধাপের derivation পাওয়া যাবে যা
<factor> ::= ( <expr> )রুল দিয়ে শুরু হয় (যেহেতু ইনপুট বন্ধনী দিয়ে শুরু, factor-এর দ্বিতীয় বিকল্প "number" প্রথম টোকেনেই বাদ পড়ে যাবে prefix-mismatch prune-এর কারণে) — এরপর ভেতরের<expr>ঠিক আগের মতোই "2 + 3" ডেরাইভ করবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরবর্তী পাঠ — পার্স ট্রি ও অ্যাম্বিগুইটি — এই একই derivation প্রক্রিয়াকে একটি ট্রি হিসেবে ভিজুয়ালাইজ করবে এবং দেখাবে কখন একটি স্ট্রিং-এর একাধিক ভিন্ন derivation থাকা সমস্যাজনক।
- পাঠ ১২ · BNF ও EBNF নোটেশন পূর্ববর্তী পাঠ এই পাঠের arithmetic গ্রামার (expr/term/factor) কোথা থেকে এসেছে তা এখানে দেখুন।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স উপরের কোড সেলের backtracking সার্চ একটি DFS-স্টাইল রিকার্সিভ অ্যালগরিদম — এই কোর্সে সার্চ/ব্যাকট্র্যাকিং প্যাটার্নের ভিত্তি শেখানো হয়েছে।