পাঠ ১৩ · ৫৮-এর মধ্যে · মডিউল ৩
Home / Courses / Concepts of Programming Languages & Compiler Design / কনটেক্সট-ফ্রি গ্রামার

কনটেক্সট-ফ্রি গ্রামার ও ডেরিভেশন

Context-free grammars & derivations
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • কনটেক্সট-ফ্রি গ্রামারের আনুষ্ঠানিক সংজ্ঞা এবং নামের পেছনের কারণ
  • ডেরিভেশন প্রক্রিয়া — কীভাবে একটি স্ট্রিং "গ্রামার থেকে উৎপন্ন" প্রমাণ করা হয়
  • 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-এর সাথে সরাসরি সংযুক্ত, মনে রাখুন) —

Leftmost derivation
প্রতি ধাপে সবচেয়ে বামের নন-টার্মিনাল এক্সপ্যান্ড করা হয় — top-down পার্সার (M5/L22 রিকার্সিভ ডিসেন্ট) স্বাভাবিকভাবেই leftmost derivation তৈরি করে।
Rightmost 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।

<expr> <term> + <term> <factor> + <term> number + <factor> 2 + 3
প্রতিটি ধাপে সবচেয়ে বামের নন-টার্মিনাল এক্সপ্যান্ড হচ্ছে — নিচের কোড সেল এই ঠিক এই সিকোয়েন্সটিই খুঁজে বের করবে, একটি সার্চ অ্যালগরিদম দিয়ে।
Python
# একটি 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 ছাড়িয়ে গেলে সেই ব্রাঞ্চ থেকে টার্গেটে পৌঁছানো গাণিতিকভাবেই অসম্ভব।
মূল কথা · Key takeaway

একটি 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 মানেই সেই শাখা নিশ্চিতভাবে ব্যর্থ।

অনুশীলন

  1. হাতে-কলমে করুন: 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]) — মোট ৫টি ডেরিভেশন ধাপ।

  2. পরীক্ষা করুন: কোড সেলে target = ['2', '+', '3']-কে target = ['(', '2', '+', '3', ')'] করে Run চেপে দেখুন — মনে রাখবেন '('-কে literal টোকেন হিসেবে ধরা হয়েছে, ডিজিট হিসেবে নয়।

    সার্চ সফল হবে এবং একটি ৯-ধাপের derivation পাওয়া যাবে যা <factor> ::= ( <expr> ) রুল দিয়ে শুরু হয় (যেহেতু ইনপুট বন্ধনী দিয়ে শুরু, factor-এর দ্বিতীয় বিকল্প "number" প্রথম টোকেনেই বাদ পড়ে যাবে prefix-mismatch prune-এর কারণে) — এরপর ভেতরের <expr> ঠিক আগের মতোই "2 + 3" ডেরাইভ করবে।

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

আগের পাঠ
L12 · BNF ও EBNF নোটেশন