পাঠ ১২ · ৫৮-এর মধ্যে · মডিউল ৩

BNF ও EBNF নোটেশন

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

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

  • BNF প্রোডাকশন রুলের সিনট্যাক্স — ::=, |, টার্মিনাল বনাম নন-টার্মিনাল
  • একটি সম্পূর্ণ arithmetic expression গ্রামার — এই কোর্সের পার্সিং মডিউলে বারবার ফিরে আসবে
  • EBNF-এর [X] ও {X} শর্টহ্যান্ড, এবং কেন এগুলো শুধু নোটেশনাল সুবিধা
  • Python দিয়ে EBNF-এর {X} কে সমতুল্য প্লেইন-BNF রুলে যান্ত্রিক রূপান্তর

১ · BNF (Backus-Naur Form) কী

BNFBackus-Naur Formকনটেক্সট-ফ্রি গ্রামারের প্রোডাকশন রুল লেখার স্ট্যান্ডার্ড, ক্লাসিক নোটেশন — <nonterminal> ::= expansion আকারে লেখা হয়। হলো L11-এ পরিচিত হওয়া গ্রামারের রুলগুলো লেখার একটি স্ট্যান্ডার্ড নোটেশন। প্রতিটি BNF রুলের আকার —

<nonterminal> ::= expansion

যেখানে ::=-এর ডানদিকের expansion হলো টার্মিনাল (আক্ষরিক সিম্বল, যেমন + বা if) ও/অথবা নন-টার্মিনাল-এর (অ্যাংগেল ব্র্যাকেটে লেখা প্লেসহোল্ডার, যেমন <expr>, যা অন্য কোনো রুলে আরও বিস্তারিতভাবে সংজ্ঞায়িত থাকে) একটি সিকোয়েন্স। যখন একই নন-টার্মিনালের একাধিক বৈধ এক্সপ্যানশন থাকে, সেগুলো | চিহ্ন দিয়ে আলাদা করে একই রুলে লেখা হয় — প্রতিটি |-এর দুই পাশ একটি করে বিকল্প (alternative)।

২ · Worked example — একটি arithmetic expression গ্রামার

নিচের গ্রামারটি একটি ক্লাসিক, স্ট্যান্ডার্ড টেক্সটবুক উদাহরণ — সাধারণ গাণিতিক এক্সপ্রেশনের সিনট্যাক্স বর্ণনা করে। এই ঠিক গ্রামারটি নোট করে রাখুন — এটি এই কোর্সের M5 (পার্সিং) মডিউলের প্রায় প্রতিটি পাঠে বারবার পুনরায় ব্যবহৃত হবে।

$$\langle expr \rangle ::= \langle expr \rangle\ {+}\ \langle term \rangle \mid \langle expr \rangle\ {-}\ \langle term \rangle \mid \langle term \rangle$$ $$\langle term \rangle ::= \langle term \rangle\ {*}\ \langle factor \rangle \mid \langle term \rangle\ {/}\ \langle factor \rangle \mid \langle factor \rangle$$ $$\langle factor \rangle ::= ({\ }\langle expr \rangle{\ }) \mid \text{number}$$

<expr> ::= <expr> + <term> | ... | <term> <term> ::= <term> * <factor> | ... | <factor> <factor> ::= ( <expr> ) | number বন্ধনী থাকলে <expr>-এ ফিরে যায়
তিনটি নন-টার্মিনাল স্তরে সাজানো — expr সবচেয়ে "আলগা" বাঁধন (যোগ/বিয়োগ), factor সবচেয়ে "শক্ত" বাঁধন (সংখ্যা বা বন্ধনী সহ পুরো এক্সপ্রেশন)। এই স্তরায়ন L14-এ precedence/ambiguity সমস্যা সমাধানের চাবিকাঠি।
লক্ষ্য করুন <factor>-এর রুলে ( <expr> ) — অর্থাৎ একটি বন্ধনীযুক্ত সাব-এক্সপ্রেশনের ভেতরে আবার সম্পূর্ণ <expr> ব্যবহার করা যায়। এই রিকার্সিভ সংজ্ঞাই একটি গ্রামারকে যেকোনো গভীরতার নেস্টেড এক্সপ্রেশন (যেমন ((1+2)*3)) বর্ণনা করার ক্ষমতা দেয় — একটি finite সংখ্যক রুল দিয়ে একটি infinite ভাষা বর্ণনা করার ঠিক সেই কৌশল যা L11-এ উল্লেখ করা হয়েছিল।

৩ · EBNF (Extended BNF) — নোটেশনাল সুবিধা, নতুন ক্ষমতা নয়

EBNFExtended BNFপ্লেইন BNF-এর উপর সুবিধাজনক শর্টহ্যান্ড যোগ করা একটি নোটেশন — কোনো নতুন এক্সপ্রেসিভ পাওয়ার যোগ করে না, শুধু পড়া/লেখা সহজ করে। সাধারণ কিছু প্যাটার্ন (ঐচ্ছিক অংশ, পুনরাবৃত্তি) প্লেইন BNF-এ বারবার একই রিকার্সিভ প্যাটার্ন লিখতে বাধ্য করে। EBNF তিনটি শর্টহ্যান্ড যোগ করে —

[X] — ঐচ্ছিক
X শূন্যবার অথবা ঠিক একবার — যেমন [ else-block ]।
{X} — পুনরাবৃত্তি
X শূন্যবার অথবা তার বেশিবার — যেমন { statement }।
গ্রুপিং ( )
একাধিক সিম্বলকে একসাথে গ্রুপ করে |, [ ] বা { }-এর সাথে ব্যবহার করা যায়।

গুরুত্বপূর্ণ কথা — EBNF কোনো নতুন এক্সপ্রেসিভ পাওয়ার যোগ করে না। EBNF-এ লেখা যেকোনো কিছু যান্ত্রিকভাবে প্লেইন BNF-এ রূপান্তরযোগ্য। যেমন {X}-কে একটি রিকার্সিভ প্লেইন-BNF রুল দিয়ে ঠিক সমতুল্যভাবে লেখা যায় —

list ::= { item }  ⟺  list ::= item list | ε

(এখানে ε মানে খালি স্ট্রিং — "কিছুই না।") নিচের কোড সেলে এই যান্ত্রিক রূপান্তরটি আসলে বাস্তবায়ন করে দেখানো হবে।

Python
# EBNF-এর { item } (পুনরাবৃত্তি) কে সমতুল্য প্লেইন-BNF রুলে রূপান্তর

def ebnf_repetition_to_bnf(nonterminal, item_symbol):
    """
    EBNF শর্টহ্যান্ড { item } -কে প্লেইন-BNF রুলে রূপান্তর করে।
    রুল = (lhs, [alt1, alt2, ...]) -- প্রতিটি alt একটি সিম্বলের টাপল, () মানে ε
    list ::= item list | ε
    """
    return (nonterminal, [
        (item_symbol, nonterminal),  # বিকল্প ১: item তারপর বাকি list
        ()                           # বিকল্প ২: ε -- তালিকা এখানেই শেষ
    ])

def matches_ebnf_repetition(tokens, item_symbol):
    # EBNF { item } -- সরাসরি: প্রতিটি টোকেন item-এর সাথে মেলে কিনা
    return all(tok == item_symbol for tok in tokens)

def matches_bnf_rule(tokens, item_symbol):
    # জেনারেট করা প্লেইন-BNF রুল (list ::= item list | ε) রিকার্সিভভাবে যাচাই
    if not tokens:
        return True  # বিকল্প ২: ε প্রযোজ্য
    if tokens[0] == item_symbol:
        return matches_bnf_rule(tokens[1:], item_symbol)  # বিকল্প ১
    return False

rule = ebnf_repetition_to_bnf("list", "item")

lhs, alternatives = rule
print("জেনারেট করা প্লেইন-BNF রুল:")
for i, alt in enumerate(alternatives):
    rhs = " ".join(alt) if alt else "ε"
    print(f"  {lhs} {'::=' if i == 0 else '|  '} {rhs}")

test_cases = [[], ["item"], ["item", "item"], ["item", "item", "item"], ["item", "x"]]

print("\nEBNF { item } বনাম জেনারেট করা প্লেইন-BNF রুল -- তুলনা:")
for tc in test_cases:
    ebnf_ok = matches_ebnf_repetition(tc, "item")
    bnf_ok = matches_bnf_rule(tc, "item")
    same = "একমত ✓" if ebnf_ok == bnf_ok else "MISMATCH ✗"
    print(f"  {str(tc):32s} EBNF={ebnf_ok!s:5s} BNF={bnf_ok!s:5s} {same}")

    
DSA কোর্সের সাথে সম্পর্ক

লক্ষ্য করুন matches_bnf_rule ফাংশনটি একটি রিকার্সিভ ফাংশন — ঠিক যেভাবে Data Structures & Algorithms কোর্সে রিকার্সিভ অ্যালগরিদম শেখানো হয়েছে। BNF রুলের রিকার্সিভ গঠন (একটি নন-টার্মিনাল নিজের সংজ্ঞায় নিজেকে উল্লেখ করা) এবং রিকার্সিভ প্রোগ্রামের গঠন প্রায় হুবহু একই রকম — M5/L22-এ (রিকার্সিভ ডিসেন্ট পার্সিং) এই সংযোগটি আরও স্পষ্ট হবে।

মূল কথা · Key takeaway

BNF হলো গ্রামার লেখার ভিত্তি নোটেশন — ::= ও |। EBNF শুধু সুবিধাজনক শর্টহ্যান্ড ([X], {X}) যোগ করে, কোনো নতুন ক্ষমতা নয়। এই পাঠের arithmetic গ্রামার (<expr>/<term>/<factor>) মনে রাখুন — এটি এই কোর্সের বাকি অংশে বারবার ফিরে আসবে।

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

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

প্র ০১ <factor> ::= ( <expr> ) | number রুলে <expr> আবার ব্যবহার করা হয়েছে কেন এটি সমস্যা নয় — এটি কি অসীম লুপ তৈরি করে না?

এটি রিকার্সিভ, কিন্তু অসীম লুপ নয় — কারণ প্রতিবার <factor>-এর এই বিকল্পটি প্রয়োগ করতে হলে ইনপুটে অবশ্যই একটি literal ( টার্মিনাল থাকতে হয়। প্রতিটি রিকার্সিভ কল ইনপুট স্ট্রিং-এর একটি অংশ "খরচ" করে (একটি বন্ধনী)। যেহেতু ইনপুট স্ট্রিং সবসময় ফাইনাইট, বন্ধনীও ফাইনাইটবার নেস্টেড হতে পারে — তাই রিকার্শন অবশ্যম্ভাবীভাবে থামে। এটিই একটি গ্রামারে "উৎপাদনশীল রিকার্শন" (প্রতিটি রিকার্সিভ ধাপে অন্তত একটি টার্মিনাল consume হয়) বনাম L22-এ আলোচিত সমস্যাযুক্ত "left recursion"-এর মূল পার্থক্য।

প্র ০২ যদি EBNF কোনো নতুন এক্সপ্রেসিভ পাওয়ার না-ই যোগ করে, তাহলে কেন প্র্যাকটিসে মানুষ প্লেইন BNF-এর বদলে EBNF ব্যবহার করে?

পাঠযোগ্যতা (readability) ও লেখার সুবিধার জন্য (L04-এর ভাষায়: writability)। { statement } একবার পড়েই বোঝা যায় "শূন্য বা তার বেশি স্টেটমেন্ট" — অথচ সমতুল্য প্লেইন-BNF রুল (stmt-list ::= statement stmt-list | ε) পড়তে একটু বেশি মানসিক প্রচেষ্টা লাগে, বিশেষ করে বড় গ্রামারে যেখানে এরকম বহু জায়গায় পুনরাবৃত্তি/ঐচ্ছিকতা আছে। যেহেতু দুটোই সমতুল্য, ভাষা-নির্দিষ্টকরণের (specification) সময় মানুষ যেটা পড়তে সহজ সেটাই বেছে নেয় — এক্সপ্রেসিভ পাওয়ার একই থাকে বলে এই বেছে নেওয়া কোনো "সীমাবদ্ধতা" তৈরি করে না।

প্র ০৩ উপরের কোড সেলে ["item", "x"] টেস্ট কেসে EBNF ও BNF উভয়েই False ফেরত দেয় কেন — "item" তো মিলেছে?

{ item } মানে "শূন্য বা তার বেশি item, এবং শুধুই item" — তালিকার প্রতিটি টোকেন অবশ্যই item হতে হবে, শুধু প্রথমটি মিললেই যথেষ্ট নয়। matches_ebnf_repetition-এ all(...) ব্যবহার করা হয়েছে বলে একটি অমিল থাকলেই পুরো ফলাফল False হয়ে যায়। matches_bnf_rule-এও একই যুক্তি — "item" মিলে গেলে বাকি অংশে (["x"]) রিকার্সিভ কল হয়, এবং সেখানে "x" != "item" হওয়ায় সরাসরি False ফেরত আসে। দুটো ফাংশনই স্বাধীনভাবে লেখা হলেও একই সিদ্ধান্তে পৌঁছায় — কারণ তারা একই ভাষা বর্ণনা করছে।

অনুশীলন

  1. হাতে-কলমে লিখুন: L12-এর arithmetic গ্রামার ব্যবহার করে <factor>-এর জন্য একটি নতুন নন-টার্মিনাল <factor> ::= - <factor> | ( <expr> ) | number যোগ করে ইউনারি মাইনাস (যেমন -5) সাপোর্ট করার BNF রুল লিখুন।

    রুলটি ঠিক এভাবেই লেখা যায়: <factor> ::= - <factor> | ( <expr> ) | number। লক্ষ্য করুন এটিও রিকার্সিভ (<factor> নিজের সংজ্ঞায় নিজেকে উল্লেখ করছে) কিন্তু উৎপাদনশীল — প্রতিটি রিকার্শন একটি - টার্মিনাল consume করে, তাই ---5-এর মতো ইনপুটও (তিনবার ইউনারি মাইনাস) সসীম ধাপে টার্মিনেট হয়।

  2. পরীক্ষা করুন: উপরের কোড সেলে test_cases-এ ["item", "item", "x", "item"] যোগ করে Run চেপে দেখুন ফলাফল কী আসে এবং কেন।

    দুটো ফাংশনই False ফেরত দেবে। কারণ তৃতীয় টোকেন "x" item নয় — যদিও এর পরে আবার একটি বৈধ "item" আছে, একটিমাত্র অ-item টোকেনও পুরো স্ট্রিংকে { item }-এর ভাষার বাইরে ফেলে দেয়, কারণ এই ভাষায় item ছাড়া অন্য কিছু থাকার অনুমতি নেই।

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

আগের পাঠ
L11 · ফরমাল ল্যাঙ্গুয়েজ ও গ্রামার পরিচিতি