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

সিনট্যাক্স-ডাইরেক্টেড ট্রান্সলেশন

Syntax-directed translation
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • সিনট্যাক্স-ডাইরেক্টেড ট্রান্সলেশন (SDT) কী এবং এটি কীভাবে L22, L30-এর কাজকে একীভূত করে
  • সিনথেসাইজড অ্যাট্রিবিউট — বটম-আপ তথ্য প্রবাহ
  • ইনহেরিটেড অ্যাট্রিবিউট — টপ-ডাউন/সাইডওয়ে তথ্য প্রবাহ
  • S-attributed বনাম L-attributed গ্রামার — সংক্ষিপ্ত পরিচিতি
  • একটি কোড সেলে দুই ধরনের অ্যাট্রিবিউট ফ্লো একসাথে বাস্তবায়ন করা

১ · SDT — একটি একীভূতকারী ফ্রেমওয়ার্ক

সিনট্যাক্স-ডাইরেক্টেড ট্রান্সলেশনSyntax-Directed Translation (SDT)একটি সাধারণ ফ্রেমওয়ার্ক যেখানে গ্রামারের প্রতিটি প্রোডাকশনের সাথে একটি সিমান্টিক অ্যাকশন (যেকোনো গণনা — টাইপ চেকিং, কোড জেনারেশন, মান এভালুয়েশন) সংযুক্ত থাকে, যাতে পার্সিং বা ট্রি-ওয়াকিংয়ের একটি স্বাভাবিক উপজাত হিসেবে সিমান্টিক প্রসেসিং ঘটে। লক্ষ্য করুন — এটি কোনো নতুন কৌশল নয়, বরং একটি নাম যা আমরা ইতিমধ্যে যা করেছি তার সাধারণ প্যাটার্নটি চিহ্নিত করে —

  • L22-এর ট্রি-এভালুয়েটর: প্রতিটি গ্রামার রুলের সাথে "এর মান গণনা করো" অ্যাকশন সংযুক্ত।
  • L30-এর টাইপ চেকার: প্রতিটি গ্রামার রুলের সাথে "এর টাইপ গণনা/যাচাই করো" অ্যাকশন সংযুক্ত।
  • (M12-এ আসছে) কোড জেনারেটর: প্রতিটি গ্রামার রুলের সাথে "এর জন্য ইন্সট্রাকশন তৈরি করো" অ্যাকশন সংযুক্ত।

এই সবগুলোই আসলে SDT-এর নির্দিষ্ট ইনস্ট্যান্স — একই ধারণার ভিন্ন ভিন্ন প্রয়োগ।

২ · সিনথেসাইজড অ্যাট্রিবিউট — তথ্য উপরের দিকে

সিনথেসাইজড অ্যাট্রিবিউটSynthesized Attributeএকটি নন-টার্মিনালের অ্যাট্রিবিউট যা তার সন্তানদের অ্যাট্রিবিউট থেকে গণনা করা হয় — তথ্য পার্স ট্রির উপরের দিকে/বটম-আপ প্রবাহিত হয়। এর সবচেয়ে পরিচিত উদাহরণ — একটি এক্সপ্রেশনের গণনাকৃত মান, যা তার সাব-এক্সপ্রেশনগুলোর মান থেকে সিনথেসাইজ (synthesize) হয় — এটি L22-এর এভালুয়েটর প্যাটার্নের সরাসরি পুনরাবৃত্তি।

৩ · ইনহেরিটেড অ্যাট্রিবিউট — তথ্য নিচের দিকে

ইনহেরিটেড অ্যাট্রিবিউটInherited Attributeএকটি নন-টার্মিনালের অ্যাট্রিবিউট যা তার অভিভাবক বা সিবলিং-এর অ্যাট্রিবিউট থেকে গণনা করা হয় — তথ্য পার্স ট্রির নিচের দিকে/সাইডওয়ে প্রবাহিত হয়। এর একটি বাস্তব উদাহরণ — একটি ডিক্লেয়ার করা ভ্যারিয়েবলের টাইপ, যা ডিক্লারেশন স্টেটমেন্ট থেকে "নিচে" প্রবাহিত হয়ে সেই স্টেটমেন্টের ভেতরের প্রতিটি ভ্যারিয়েবল-ব্যবহারে সংযুক্ত হতে হয় — যেমন int a, b, c;-তে int টাইপটি একবারই লেখা হয়েছে, কিন্তু এটি a, b, c — তিনটি নামেই "নিচে" প্রবাহিত হয়ে সংযুক্ত হতে হবে।

decl: int a, b, c; expr: (2 + 3) * 4 ইনহেরিটেড: int ↓ a, b, c সিনথেসাইজড: 20 ↑ compute_value() → 20
একটি ডিক্লারেশনের টাইপ নিচের দিকে (ইনহেরিটেড) প্রবাহিত হয়; একটি এক্সপ্রেশনের মান উপরের দিকে (সিনথেসাইজড) প্রবাহিত হয় — দুটোই একই ট্রি-ওয়াকিং যন্ত্রের অংশ।

৪ · S-attributed বনাম L-attributed গ্রামার

একটি গ্রামার যা শুধু সিনথেসাইজড অ্যাট্রিবিউট ব্যবহার করে তাকে S-attributed বলা হয় — এটি সম্পূর্ণভাবে বটম-আপ এভালুয়েট করা যায় (যেমন L24-এর বটম-আপ পার্সিংয়ে, প্রতিটি reduce-এ একটি অ্যাকশন সংযুক্ত করে)। যে গ্রামার ইনহেরিটেড অ্যাট্রিবিউটও ব্যবহার করে তাকে L-attributed বলা হয় — এর জন্য একটি নির্দিষ্ট বাম-থেকে-ডানে (left-to-right) এভালুয়েশন অর্ডার প্রয়োজন হয় (পূর্ণ ডেরিভেশন এখানে আলোচনা করা হচ্ছে না, শুধু নামটি পরিচিত করানো হচ্ছে)।

M5, M6 ও M12-এর সাথে সম্পর্ক

এই পাঠ M6-এর সমাপ্তি — এটি দেখায় L22-এর এভালুয়েটর ও L30-এর টাইপ চেকার আসলে একই অন্তর্নিহিত ধারণার (গ্রামার প্রোডাকশনে অ্যাকশন সংযুক্ত করা) দুটো ভিন্ন প্রয়োগ। M12-এর কোড জেনারেটরও ঠিক এই একই ফ্রেমওয়ার্কের আরেকটি ইনস্ট্যান্স হবে — শুধু অ্যাকশনটি হবে "ইন্সট্রাকশন তৈরি করো"।

৫ · কোড সেল — দুই ধরনের অ্যাট্রিবিউট ফ্লো একসাথে

নিচের কোড সেলে দুটো আলাদা ফাংশন — একটি সিনথেসাইজড অ্যাট্রিবিউট গণনা করছে (একটি এক্সপ্রেশন ট্রির মান, বটম-আপ), আরেকটি ইনহেরিটেড অ্যাট্রিবিউট প্রচার করছে (একটি ডিক্লারেশনের টাইপ, টপ-ডাউন) — দুটোই আলাদা দিক থেকে তথ্য প্রবাহিত করছে, কিন্তু উভয়ই SDT-এর অংশ।

Python
class SymbolTable:
    """L28-এর সিম্বল টেবিল -- ইনহেরিটেড টাইপ অ্যানোটেশন এখানে সংরক্ষণ করা হবে।"""
    def __init__(self):
        self._table = {}
    def insert(self, name, type_):
        self._table[name] = {"type": type_}
    def lookup(self, name):
        return self._table[name]


# --- সিনথেসাইজড অ্যাট্রিবিউট: বটম-আপ, L22/L30-এর প্যাটার্নের পুনরাবৃত্তি ---
def compute_value(node):
    """একটি এক্সপ্রেশন নোডের মান তার সন্তানদের মান থেকে সিনথেসাইজ করে (উপরের দিকে প্রবাহ)।"""
    if node[0] == "num":
        return node[1]
    _, op, left, right = node
    left_value = compute_value(left)     # সন্তানের অ্যাট্রিবিউট আগে গণনা
    right_value = compute_value(right)
    return {"+": left_value + right_value,
            "-": left_value - right_value,
            "*": left_value * right_value,
            "/": left_value / right_value}[op]


# --- ইনহেরিটেড অ্যাট্রিবিউট: টপ-ডাউন, ডিক্লারেশন থেকে প্রতিটি নামে প্রবাহিত ---
def propagate_type_context(decl_node, symtab):
    """একটি ডিক্লারেশনের টাইপ তার প্রতিটি ভ্যারিয়েবলে "নিচে" প্রচার করে (নিচের দিকে প্রবাহ)।"""
    _, declared_type, names = decl_node
    inherited_annotations = {}
    for name in names:
        inherited_annotations[name] = declared_type  # অভিভাবক থেকে সন্তানে টাইপ প্রবাহিত
        symtab.insert(name, declared_type)
    return inherited_annotations


# --- সিনথেসাইজড দিক: (2 + 3) * 4 -- একটি এক্সপ্রেশনের মান বটম-আপ গণনা ---
expr_tree = ("binop", "*", ("binop", "+", ("num", 2), ("num", 3)), ("num", 4))
synthesized_value = compute_value(expr_tree)

print("--- সিনথেসাইজড অ্যাট্রিবিউট (বটম-আপ) ---")
print(f"এক্সপ্রেশন: (2 + 3) * 4")
print(f"সিনথেসাইজড মান: {synthesized_value}")

# --- ইনহেরিটেড দিক: int a, b, c; -- একটি টাইপ টপ-ডাউন প্রচার ---
decl_tree = ("decl", "int", ["a", "b", "c"])
symtab = SymbolTable()
annotations = propagate_type_context(decl_tree, symtab)

print("\n--- ইনহেরিটেড অ্যাট্রিবিউট (টপ-ডাউন) ---")
print(f"ডিক্লারেশন: int a, b, c;")
for name, inherited_type in annotations.items():
    confirmed = symtab.lookup(name)["type"]
    print(f"  {name} <- ইনহেরিটেড টাইপ: {inherited_type}  (সিম্বল টেবিলে নিশ্চিত: {confirmed})")

print("\n--- দুই ফ্লো একসাথে ---")
print(f"সিনথেসাইজড ফলাফল ('উপরে' গণনা):  expr মান = {synthesized_value}")
print(f"ইনহেরিটেড ফলাফল ('নিচে' প্রচার): {len(annotations)}টি নাম int টাইপ পেয়েছে")

    
লক্ষ্য করুন — compute_value() সন্তান থেকে অভিভাবকের দিকে তথ্য নিয়ে যায় (রিকার্সিভ কল আগে হয়, ফলাফল পরে কম্বাইন হয়) — এটিই সিনথেসাইজড অ্যাট্রিবিউটের সংজ্ঞা। বিপরীতে, propagate_type_context() একটি একক মান (declared_type) নিয়ে সেটি একাধিক সন্তানে (প্রতিটি নাম) বিতরণ করে — অভিভাবক থেকে সন্তানের দিকে তথ্য যাচ্ছে, ঠিক ইনহেরিটেড অ্যাট্রিবিউটের সংজ্ঞা অনুযায়ী।
মূল কথা · Key takeaway

সিনট্যাক্স-ডাইরেক্টেড ট্রান্সলেশন এই কোর্সের M5-M6-এ যা কিছু শেখানো হয়েছে (এভালুয়েশন, টাইপ চেকিং) তার একটি একক ছাতার নিচে একীভূতকরণ — গ্রামার প্রোডাকশনে অ্যাকশন সংযুক্ত করা, যেখানে তথ্য উপরে (সিনথেসাইজড) অথবা নিচে (ইনহেরিটেড) প্রবাহিত হতে পারে। M6 এখানেই শেষ — এখন থেকে ধরে নেওয়া হবে প্রোগ্রাম সিনট্যাক্টিকালি ও সিমান্টিক্যালি বৈধ, এবং M7 থেকে টাইপ সিস্টেমের গভীর ভাষা-ডিজাইন প্রশ্নে প্রবেশ করা হবে।

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

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

প্র ০১ L22-এর এভালুয়েটর ও L30-এর টাইপ চেকার — দুটোই বটম-আপ ট্রি-ওয়াকিং প্যাটার্ন অনুসরণ করে। SDT-এর ভাষায় এই দুটোর মধ্যে ঠিক কী মিল আছে?

দুটোই সিনথেসাইজড অ্যাট্রিবিউটের উদাহরণ — উভয়ই সন্তানদের ফলাফল (একটির ক্ষেত্রে মান, অন্যটির ক্ষেত্রে টাইপ) প্রথমে রিকার্সিভলি গণনা করে, তারপর সেই ফলাফলগুলো কম্বাইন করে অভিভাবক নোডের নিজস্ব ফলাফল গণনা করে। SDT-এর দৃষ্টিকোণ থেকে এই দুটো ভিন্ন কাজ (এভালুয়েশন, টাইপ চেকিং) আসলে একই কাঠামোগত প্যাটার্নের দুটো ভিন্ন "অ্যাকশন" — শুধু গ্রামার প্রোডাকশনে সংযুক্ত অ্যাকশনটাই আলাদা।

প্র ০২ উপরের কোড সেলের propagate_type_context()-এ যদি names লিস্টটি খালি হতো, তাহলে ফাংশনটি কী ফেরত দিত এবং সিম্বল টেবিলের কী অবস্থা হতো?

for name in names লুপটি একবারও চলত না, তাই inherited_annotations একটি খালি dict থেকে যেত এবং symtab-এ কোনো নতুন নাম যোগ হতো না — ফাংশনটি খালি {} ফেরত দিত। এটি ইনহেরিটেড অ্যাট্রিবিউট প্রচারের একটি স্বাভাবিক সীমা-কেস (edge case): প্রচার করার মতো কোনো সন্তান না থাকলে, কিছুই প্রচারিত হয় না — কোনো এরর হয় না, শুধু কাজ করার কিছু থাকে না।

প্র ০৩ একটি গ্রামার যা শুধু সিনথেসাইজড অ্যাট্রিবিউট ব্যবহার করে (S-attributed) কেন L24-এর বটম-আপ পার্সিংয়ে সরাসরি এভালুয়েট করা সহজ, কিন্তু ইনহেরিটেড অ্যাট্রিবিউট থাকলে (L-attributed) তা সহজ নয়?

বটম-আপ পার্সিং (L24) একটি reduce ঘটার সাথে সাথেই সেই মুহূর্তে একটি নোডের সব সন্তান স্ট্যাকে ইতিমধ্যে সম্পূর্ণভাবে প্রসেস হয়ে গেছে — তাই একটি সিনথেসাইজড অ্যাকশন (যা শুধু সন্তানদের ফলাফলের উপর নির্ভর করে) সেই reduce-মুহূর্তেই সরাসরি এভালুয়েট করা যায়, বাড়তি কোনো সমন্বয় ছাড়াই। কিন্তু একটি ইনহেরিটেড অ্যাট্রিবিউটের জন্য তথ্য অভিভাবক/সিবলিং থেকে আসতে হয় — বটম-আপ পার্সিং-এ reduce-মুহূর্তে অভিভাবক এখনো "তৈরি" হয়নি, তাই তথ্যটি এখনও পাওয়া যায় না। এই কারণেই ইনহেরিটেড অ্যাট্রিবিউট বাস্তবায়নে একটি নির্দিষ্ট, সতর্কভাবে বেছে নেওয়া এভালুয়েশন অর্ডার (L-attributed-এর সংজ্ঞা অনুযায়ী) প্রয়োজন হয়।

অনুশীলন

  1. চিন্তা করুন: L07-এর Shape/Rectangle/Circle-এর area() মেথড — এটি কি সিনথেসাইজড অ্যাট্রিবিউটের একটি উদাহরণ, নাকি ইনহেরিটেড? কারণ ব্যাখ্যা করুন।

    সিনথেসাইজড — একটি shape-এর area() সেই shape-এর নিজস্ব ডেটা (যেমন Rectangle-এর width/height, বা Circle-এর radius) থেকে সরাসরি গণনা করা হয়, কোনো অভিভাবক বা বাইরের প্রসঙ্গ থেকে তথ্য "নিচে" নামিয়ে আনার প্রয়োজন হয় না। এটি একটি "লিফ-লেভেল" গণনা যার ফলাফল সরাসরি সেই নোডের নিজের ডেটা থেকেই তৈরি হয় — সিনথেসাইজড অ্যাট্রিবিউটের একটি সরল রূপ, যদিও এখানে সন্তান-থেকে-অভিভাবক প্রবাহ ছাড়াই সরাসরি একটি নোডেই ঘটছে।

  2. পরীক্ষা করুন: উপরের কোড সেলে decl_tree-এর মান ("decl", "float", ["p", "q"]) করে Run চেপে দেখুন ইনহেরিটেড অ্যাট্রিবিউট আউটপুট কীভাবে বদলায়।

    আউটপুট হবে: p <- ইনহেরিটেড টাইপ: float (সিম্বল টেবিলে নিশ্চিত: float) এবং q <- ইনহেরিটেড টাইপ: float (সিম্বল টেবিলে নিশ্চিত: float) — দুটো নাম, প্রতিটিতে "float" টাইপ প্রচারিত হয়েছে। শেষ লাইনে "২টি নাম int টাইপ পেয়েছে"-এর বদলে "২টি নাম float টাইপ পেয়েছে" দেখাবে, কারণ এই সারাংশ লাইনটি len(annotations) ব্যবহার করে, যা এখন ২ (আগে ছিল ৩, "a, b, c" থেকে কমে "p, q" হওয়ায়)।

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

আগের পাঠ
টাইপ চেকিং বেসিকস