পাঠ ২৯ · ৫৮-এর মধ্যে · মডিউল ৬
Home / Courses / Concepts of Programming Languages & Compiler Design / স্কোপ রেজোলিউশন

কম্পাইলারে স্কোপ রেজোলিউশন

Scope resolution in compilers
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • স্কোপের সংজ্ঞা এবং নেস্টেড স্কোপ ও শ্যাডোয়িং কীভাবে কাজ করে
  • স্কোপ স্ট্যাক — L28-এর ফ্ল্যাট সিম্বল টেবিলকে একাধিক নেস্টেড স্কোপে সম্প্রসারণ করা
  • enter_scope()/exit_scope() কীভাবে push/pop দিয়ে বাস্তবায়িত হয়
  • ScopedSymbolTable বাস্তবায়ন করে শ্যাডোয়িং সরাসরি প্রমাণ করা

১ · স্কোপ কী

স্কোপScopeপ্রোগ্রামের টেক্সটের সেই অঞ্চল যার ভেতরে একটি ডিক্লেয়ার করা নাম বৈধ/দৃশ্যমান থাকে — স্ট্যাটিক্যালি/লেক্সিক্যালি স্কোপড ভাষায় (যা আজকের অধিকাংশ ভাষা) এটি সম্পূর্ণভাবে সোর্স কোডের টেক্সটে নামের অবস্থান দেখেই নির্ধারণ করা যায়। এই পাঠের ফোকাস কম্পাইলার এটি মেকানিক্যালি কীভাবে বাস্তবায়ন করে তার উপর — কোন স্কোপিং নিয়ম বেছে নেওয়া উচিত, সেই ভাষা-ডিজাইন প্রশ্নটি গভীরভাবে আলোচিত হবে M8/L38-এ।

২ · নেস্টেড স্কোপ ও শ্যাডোয়িং

একটি ভেতরের স্কোপ (যেমন একটি ফাংশনের বডি, বা একটি ব্লক) এমন একটি নাম ডিক্লেয়ার করতে পারে যা ইতিমধ্যে একটি বাইরের স্কোপে বিদ্যমান — ভেতরের ডিক্লারেশন বাইরেরটিকে সাময়িকভাবে শ্যাডোShadowingএকটি ভেতরের/নেস্টেড স্কোপে একটি নাম পুনরায় ডিক্লেয়ার করলে, ভেতরের স্কোপ চলাকালীন বাইরের স্কোপের একই নামের ডিক্লারেশন সাময়িকভাবে ঢাকা পড়ে যায় — বাইরেরটি স্থায়ীভাবে মুছে যায় না, ভেতরের স্কোপ শেষ হলে আবার দৃশ্যমান হয়। (hide) করে — গুরুত্বপূর্ণ: এটি বাইরের ডিক্লারেশন স্থায়ীভাবে ধ্বংস করে না, শুধু ভেতরের স্কোপ চলাকালীন সাময়িকভাবে ঢেকে রাখে। ভেতরের স্কোপ শেষ হলেই বাইরের ডিক্লারেশন আবার দৃশ্যমান হয়ে যায়।

৩ · স্কোপ স্ট্যাক — বাস্তবায়ন কৌশল

L28-এর একটিমাত্র ফ্ল্যাট সিম্বল টেবিলকে এখন সম্প্রসারণ করা হচ্ছে — বর্তমানে-খোলা প্রতিটি স্কোপের জন্য একটি করে সিম্বল টেবিল রেখে, সবগুলোকে একটি স্ট্যাকে (Python list) রাখা হয়:

  • একটি নতুন স্কোপে ঢোকা (যেমন একটি ফাংশন বডি, একটি ব্লক শুরু) মানে স্ট্যাকে একটি নতুন খালি টেবিল push করা।
  • insert(name, type_) সবসময় বর্তমান/সবচেয়ে উপরের (topmost) স্কোপের টেবিলে যোগ হয় — এখনও একই স্কোপে ডুপ্লিকেট REJECT করে, L28-এর মতোই।
  • lookup(name) স্ট্যাকের একদম উপর (ভেতরের/সাম্প্রতিকতম স্কোপ) থেকে শুরু করে নিচের (বাইরের/গ্লোবাল স্কোপ) দিকে খোঁজে, এবং প্রথম যে ম্যাচ পায় সেটিই ফেরত দেয় — এটিই শ্যাডোয়িং সঠিকভাবে বাস্তবায়ন করে, কারণ ভেতরের ডিক্লারেশন সবসময় আগে পাওয়া যায়।
  • একটি স্কোপ থেকে বেরোনো মানে তার টেবিলটি স্ট্যাক থেকে সম্পূর্ণভাবে pop করে দেওয়া — এর ডিক্লারেশনগুলো আবার অদৃশ্য হয়ে যায়, কম্পাইলারের হিসাব-নিকাশ থেকেও মুক্ত হয়ে যায়।
গ্লোবাল স্কোপ: x = int (স্ট্যাক নিচে) enter_scope() → নতুন টেবিল push ভেতরের স্কোপ: x = string (শ্যাডো করছে) lookup("x") → উপর থেকে খোঁজে → string exit_scope() → pop → lookup("x") → int
স্ট্যাকের উপরে সবচেয়ে ভেতরের স্কোপ — lookup সবসময় উপর থেকে নিচে খোঁজে, প্রথম ম্যাচ শ্যাডোয়িংয়ের সঠিক ফলাফল দেয়।

৪ · ScopedSymbolTable — শ্যাডোয়িং প্রমাণ করা

নিচের কোড সেলে একটি ScopedSymbolTable ক্লাস বাস্তবায়ন করা হয়েছে, L28-এর সিম্বল টেবিলের একটি স্ট্যাক হিসেবে। একটি বাইরের স্কোপে x ডিক্লেয়ার করা হচ্ছে, তারপর একটি ভেতরের স্কোপে ভিন্ন মানের একটি নতুন x ডিক্লেয়ার করা হচ্ছে (শ্যাডোয়িং), এবং শেষে ভেতরের স্কোপ থেকে বেরিয়ে দেখানো হচ্ছে বাইরের x আবার দৃশ্যমান হয়ে গেছে।

Python
class ScopedSymbolTable:
    """L28-এর ফ্ল্যাট সিম্বল টেবিলের একটি স্ট্যাক -- নেস্টেড স্কোপ ও শ্যাডোয়িং সামলায়।"""

    def __init__(self):
        self._scopes = [{}]  # স্ট্যাকের নিচে সবসময় একটি গ্লোবাল স্কোপ

    def enter_scope(self):
        self._scopes.append({})

    def exit_scope(self):
        if len(self._scopes) == 1:
            raise RuntimeError("গ্লোবাল স্কোপ থেকে বের হওয়া যায় না")
        self._scopes.pop()

    def insert(self, name, type_):
        current = self._scopes[-1]  # সবসময় সবচেয়ে উপরের (বর্তমান) স্কোপে
        if name in current:
            raise ValueError(
                f"সেমান্টিক এরর: '{name}' এই স্কোপে ইতিমধ্যে ডিক্লেয়ার করা হয়েছে"
            )
        current[name] = {"type": type_}

    def lookup(self, name):
        for scope in reversed(self._scopes):  # উপর (ভেতর) থেকে নিচে (বাইরে)
            if name in scope:
                return scope[name]
        raise ValueError(f"সেমান্টিক এরর: '{name}' ডিক্লেয়ার করা হয়নি")

    def depth(self):
        return len(self._scopes)


st = ScopedSymbolTable()

print("--- বাইরের (গ্লোবাল) স্কোপে x ডিক্লেয়ার ---")
st.insert("x", "int")
print(f"lookup('x') = {st.lookup('x')}   (স্কোপ গভীরতা: {st.depth()})")

print("\n--- ভেতরের স্কোপে ঢোকা এবং শ্যাডোয়িং x ডিক্লেয়ার ---")
st.enter_scope()
st.insert("x", "string")
print(f"lookup('x') = {st.lookup('x')}   (স্কোপ গভীরতা: {st.depth()})")
print("-> ভেতরের 'string' x প্রথমে পাওয়া গেছে, বাইরের 'int' x শ্যাডো হয়ে গেছে")

print("\n--- ভেতরের স্কোপ থেকে বেরিয়ে যাওয়া ---")
st.exit_scope()
print(f"lookup('x') = {st.lookup('x')}   (স্কোপ গভীরতা: {st.depth()})")
print("-> বাইরের 'int' x আবার দৃশ্যমান -- শ্যাডো সাময়িক ছিল, স্থায়ী নয়")

print("\n--- বোনাস: ভেতরের স্কোপে একটি নতুন নাম, বাইরের স্কোপে অদৃশ্য ---")
st.enter_scope()
st.insert("y", "float")
print(f"ভেতরের স্কোপে lookup('y') = {st.lookup('y')}")
st.exit_scope()
try:
    st.lookup("y")
except ValueError as e:
    print(f"বাইরের স্কোপে lookup('y') -> সঠিকভাবে REJECT: {e}")

    
লক্ষ্য করুন — lookup()-এর for scope in reversed(self._scopes) লাইনটিই পুরো শ্যাডোয়িং যুক্তির ভিত্তি। এটি স্ট্যাকের একদম শেষ (সবচেয়ে সাম্প্রতিক/ভেতরের) এন্ট্রি থেকে শুরু করে খোঁজে — তাই একটি ভেতরের স্কোপের ডিক্লারেশন সবসময় বাইরের একই-নামের ডিক্লারেশনের আগে পাওয়া যাবে, যতক্ষণ ভেতরের স্কোপটি স্ট্যাকে বিদ্যমান থাকে।
মূল কথা · Key takeaway

স্কোপ রেজোলিউশন কম্পাইলারে একটি সহজ, সুন্দর ধারণা দিয়ে বাস্তবায়িত হয় — সিম্বল টেবিলের একটি স্ট্যাক। push করে স্কোপে ঢোকা, pop করে বের হওয়া, আর lookup সবসময় উপর থেকে নিচে খোঁজা — এই তিনটি নিয়মই নেস্টেড স্কোপ ও শ্যাডোয়িংয়ের পুরো আচরণ সঠিকভাবে ব্যাখ্যা করে, কোনো বিশেষ-কেস ছাড়াই।

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

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

প্র ০১ যদি lookup() স্ট্যাক নিচ (বাইরে) থেকে উপরে (ভেতরে) খুঁজত, বদলে উপর থেকে নিচে খোঁজার — তাহলে কী ভুল হতো?

এটি শ্যাডোয়িং সম্পূর্ণ উল্টে দিত — সবসময় সবচেয়ে বাইরের (প্রথম পাওয়া) ডিক্লারেশন ফেরত আসত, ভেতরের সাম্প্রতিক ডিক্লারেশন নয়। উপরের কোড সেলের উদাহরণে, ভেতরের স্কোপে lookup('x') ভুলভাবে বাইরের "int" ফেরত দিত, "string" নয় — এটি আসলে প্রোগ্রামের আসল আচরণের বিপরীত, কারণ বাস্তব ভাষায় সবসময় সবচেয়ে ভেতরের/সাম্প্রতিক ডিক্লারেশনই প্রযোজ্য হয়।

প্র ০২ উপরের কোড সেলে exit_scope() একটি স্কোপের টেবিলটি সম্পূর্ণভাবে pop করে ফেলে দেয়, শুধু "নিষ্ক্রিয়" (deactivate) করে রাখে না। এটি কেন গুরুত্বপূর্ণ?

যদি পুরনো স্কোপের টেবিলগুলো রয়ে যেত (শুধু নিষ্ক্রিয় চিহ্নিত), তাহলে মেমরি ব্যবহার প্রোগ্রামের আকারের সাথে সাথে অপ্রয়োজনীয়ভাবে বাড়তেই থাকত — একটি বড় প্রোগ্রামে হাজার হাজার ফাংশন/ব্লক থাকলে, প্রতিটি স্কোপের পুরনো টেবিল স্মৃতিতে জমে যেত। সম্পূর্ণভাবে pop করা কম্পাইলারের বুককিপিং পরিষ্কার রাখে এবং নিশ্চিত করে lookup() কখনো ভুলবশত একটি বন্ধ হয়ে যাওয়া স্কোপের নাম খুঁজে পাবে না।

প্র ০৩ এই পাঠের insert() এখনও শুধু বর্তমান/সবচেয়ে উপরের স্কোপে ডুপ্লিকেট চেক করে, পুরো স্ট্যাক জুড়ে নয়। এই সিদ্ধান্তটি কেন সঠিক — এটিই কি শ্যাডোয়িংকে বৈধ করে তোলে?

হ্যাঁ, ঠিক এটিই। যদি insert() পুরো স্ট্যাক জুড়ে ডুপ্লিকেট চেক করত (শুধু বর্তমান স্কোপে নয়), তাহলে ভেতরের স্কোপে x ডিক্লেয়ার করার চেষ্টা করলেই এরর হতো (কারণ বাইরের স্কোপে ইতিমধ্যে x আছে) — যা শ্যাডোয়িংকে সম্পূর্ণ অসম্ভব করে দিত, যদিও শ্যাডোয়িং একটি বৈধ, সাধারণ ভাষার বৈশিষ্ট্য। শুধু বর্তমান/সবচেয়ে-উপরের স্কোপে ডুপ্লিকেট চেক করা নিশ্চিত করে ডুপ্লিকেট সেই একই স্কোপের মধ্যেই আটকানো হয় (যা এখনও একটি বাস্তব ভুল), অথচ ভিন্ন (নেস্টেড) স্কোপে শ্যাডোয়িং করা বৈধ থাকে।

অনুশীলন

  1. চিন্তা করুন: একটি প্রোগ্রামে তিন স্তরের নেস্টেড স্কোপ থাকলে (গ্লোবাল → ফাংশন → ব্লক), এবং শুধু গ্লোবাল ও ব্লক স্তরে x ডিক্লেয়ার করা থাকলে (মাঝের ফাংশন স্তরে নেই), ব্লক স্তরে lookup('x') কী ফেরত দেবে?

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

  2. পরীক্ষা করুন: উপরের কোড সেলের শেষে st.exit_scope()-এর পরে st.exit_scope() আরেকবার কল করে দেখুন কী ঘটে (মনে রাখুন এই মুহূর্তে স্ট্যাকে শুধু গ্লোবাল স্কোপ বাকি আছে)।

    এটি RuntimeError: গ্লোবাল স্কোপ থেকে বের হওয়া যায় না রেইজ করবে — কারণ exit_scope()-এর ভেতরে if len(self._scopes) == 1 চেক আছে যা গ্লোবাল স্কোপকে (স্ট্যাকের একদম নিচের, সবসময় বিদ্যমান স্তর) কখনো pop হতে দেয় না। এই সুরক্ষা ছাড়া, একটি ভুল করে অতিরিক্ত exit_scope() কল স্ট্যাককে সম্পূর্ণ খালি করে ফেলত, এবং পরের যেকোনো insert/lookup কল IndexError-এ ক্র্যাশ করত।

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

আগের পাঠ
সিম্বল টেবিল