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

টাইপ চেকিং বেসিকস

Type checking basics
৮ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টাইপ চেকিং কী এবং এটি কেন একটি বটম-আপ ট্রি-ওয়াকিং প্রক্রিয়া
  • একটি ছোট, প্রতিনিধিত্বমূলক টাইপ-চেকিং রুলসেট (arithmetic, comparison, string concatenation)
  • L28-L29-এর সিম্বল টেবিল কীভাবে আইডেন্টিফায়ারের টাইপ সরবরাহ করে
  • একটি বাস্তব type_check() ফাংশন — বৈধ এক্সপ্রেশন গ্রহণ, অবৈধ এক্সপ্রেশন স্পষ্টভাবে প্রত্যাখ্যান

১ · টাইপ চেকিং কী

টাইপ চেকিংType Checkingসেমান্টিক অ্যানালাইসিসের সেই প্রক্রিয়া যা যাচাই করে প্রোগ্রামের প্রতিটি অপারেশন সামঞ্জস্যপূর্ণ টাইপের অপারেন্ডে প্রয়োগ হচ্ছে কিনা। L02-এ আমরা দেখেছিলাম x = "hello" + 5 সিনট্যাক্টিকালি সম্পূর্ণ বৈধ (একটি অ্যাসাইনমেন্ট, একটি প্লাস-এক্সপ্রেশন) — কিন্তু সেমান্টিক্যালি প্রশ্নবিদ্ধ, কারণ একটি স্ট্রিং ও একটি সংখ্যা যোগ করার কোনো সুনির্দিষ্ট অর্থ নেই। টাইপ চেকিং হলো ঠিক এই ধরনের সমস্যা মেকানিক্যালি ধরার প্রক্রিয়া।

২ · একটি বটম-আপ ট্রি-ওয়াকিং প্রক্রিয়া

টাইপ চেকিং L14/L22-এর পার্স ট্রি/AST-এর উপর বটম-আপ (নিচ থেকে উপরে) কাজ করে —

  • পাতা (leaf): একটি লিটারেল (যেমন 5) বা একটি আইডেন্টিফায়ার রেফারেন্সের টাইপ সরাসরি জানা যায় — লিটারেলের টাইপ তাৎক্ষণিক, আর আইডেন্টিফায়ারের টাইপ আসে L28-L29-এর সিম্বল টেবিল থেকে lookup() করে।
  • ভেতরের নোড (অপারেশন): প্রথমে তার সন্তানদের টাইপ (রিকার্সিভলি) গণনা করা হয়, তারপর যাচাই করা হয় সেই টাইপগুলো অপারেশনের জন্য সামঞ্জস্যপূর্ণ কিনা — সামঞ্জস্যপূর্ণ হলে ফলাফলের টাইপ গণনা করা হয়, নাহলে একটি টাইপ এরর রিপোর্ট হয়, ঠিক যেখানে সমস্যাটি ধরা পড়েছে।
পাতা: ("lit", "hello") → STRING পাতা: ("lit", 5) → INT ভেতরের নোড: ("binop", "+", ...) STRING + INT সামঞ্জস্যপূর্ণ নয় টাইপ এরর -- এই নোডেই রিপোর্ট হয়
সন্তানদের টাইপ আগে গণনা হয়, তারপর অভিভাবক নোড সেগুলো যাচাই করে — এই কারণে এরর ঠিক যে সাব-এক্সপ্রেশনে সমস্যা, সেখানেই ধরা পড়ে।

৩ · টাইপ-চেকিং রুল

Arithmetic (+, -, *, /)
উভয় অপারেন্ড numeric (INT বা FLOAT) হতে হবে, ফলাফলও numeric — যদি যেকোনো একটি অপারেন্ড FLOAT হয়, ফলাফল FLOAT হয় (একটি সহজ টাইপ-প্রমোশন রুল)।
Comparison (<, >, ==, ...)
উভয় অপারেন্ডের টাইপ সামঞ্জস্যপূর্ণ হতে হবে, ফলাফল সবসময় BOOLEAN।
String concatenation
দুটি STRING অপারেন্ডের মধ্যে + আলাদাভাবে বৈধ (concatenation) — কিন্তু INT+STRING প্রত্যাখ্যাত, ঠিক L02-এর মূল উদাহরণের মতো।

৪ · type_check() বাস্তবায়ন — বৈধ ও অবৈধ কেস

নিচের কোড সেলে type_check(node, symtab) ফাংশনটি একটি নেস্টেড-টাপল AST-এর উপর রিকার্সিভলি টাইপ গণনা/যাচাই করে। প্রথমে তিনটি বৈধ এক্সপ্রেশন (একটি সংখ্যাগত এক্সপ্রেশন, সিম্বল টেবিল থেকে আসা একটি আইডেন্টিফায়ার, এবং একটি স্ট্রিং কনক্যাটেনেশন) সফলভাবে টাইপ-চেক হচ্ছে, তারপর "hello" + 5 ইচ্ছাকৃতভাবে REJECT হচ্ছে — এরর মেসেজে ঠিক কোন সাব-এক্সপ্রেশনটি ব্যর্থ হয়েছে তা স্পষ্টভাবে দেখানো হচ্ছে।

Python
class SymbolTable:
    """L28-এর সিম্বল টেবিল -- এখানে শুধু আইডেন্টিফায়ারের টাইপ সরবরাহ করতে ব্যবহৃত।"""
    def __init__(self):
        self._table = {}
    def insert(self, name, type_):
        self._table[name] = {"type": type_}
    def lookup(self, name):
        if name not in self._table:
            raise ValueError(f"সেমান্টিক এরর: '{name}' ডিক্লেয়ার করা হয়নি")
        return self._table[name]


NUMERIC = {"INT", "FLOAT"}

def type_check(node, symtab):
    """AST-এর উপর বটম-আপ টাইপ চেক -- প্রতিটি নোডের টাইপ ফেরত দেয়, অথবা TypeError রেইজ করে।"""
    tag = node[0]

    if tag == "lit":
        value = node[1]
        if isinstance(value, bool):
            return "BOOL"
        if isinstance(value, int):
            return "INT"
        if isinstance(value, float):
            return "FLOAT"
        if isinstance(value, str):
            return "STRING"
        raise TypeError(f"অজানা লিটারেল: {value!r}")

    if tag == "id":
        name = node[1]
        return symtab.lookup(name)["type"]

    if tag == "binop":
        op = node[1]
        left_type = type_check(node[2], symtab)
        right_type = type_check(node[3], symtab)

        if op in ("+", "-", "*", "/"):
            if op == "+" and left_type == "STRING" and right_type == "STRING":
                return "STRING"  # স্ট্রিং কনক্যাটেনেশন আলাদাভাবে বৈধ
            if left_type in NUMERIC and right_type in NUMERIC:
                return "FLOAT" if "FLOAT" in (left_type, right_type) else "INT"
            raise TypeError(
                f"টাইপ এরর: '{op}' অপারেটর {left_type} ও {right_type}-এর মধ্যে প্রযোজ্য নয় "
                f"(ব্যর্থ নোড: {node!r})"
            )

        if op in ("<", ">", "==", "<=", ">="):
            if left_type != right_type:
                raise TypeError(
                    f"টাইপ এরর: তুলনা অপারেটর '{op}' ভিন্ন টাইপ {left_type} ও {right_type}-এর মধ্যে "
                    f"প্রযোজ্য নয় (ব্যর্থ নোড: {node!r})"
                )
            return "BOOL"

        raise TypeError(f"অজানা অপারেটর: {op!r}")

    raise TypeError(f"অজানা নোড টাইপ: {tag!r}")


symtab = SymbolTable()
symtab.insert("x", "INT")

test_cases = [
    ("(3 + 4) * 2  -- সব INT",
     ("binop", "*", ("binop", "+", ("lit", 3), ("lit", 4)), ("lit", 2))),
    ("x + 5  -- সিম্বল টেবিল থেকে x-এর টাইপ (INT)",
     ("binop", "+", ("id", "x"), ("lit", 5))),
    ('"hi" + "there"  -- STRING কনক্যাটেনেশন',
     ("binop", "+", ("lit", "hi"), ("lit", "there"))),
]

print("--- বৈধ এক্সপ্রেশন ---")
for label, tree in test_cases:
    result_type = type_check(tree, symtab)
    print(f"{label:38s} -> {result_type}")

print('\n--- অবৈধ এক্সপ্রেশন: "hello" + 5 ---')
bad_tree = ("binop", "+", ("lit", "hello"), ("lit", 5))
try:
    type_check(bad_tree, symtab)
    print("(এটা প্রিন্ট হওয়ার কথা না -- এরর ধরা পড়া উচিত ছিল)")
except TypeError as e:
    print(f"সঠিকভাবে REJECT হয়েছে -> {e}")

    
লক্ষ্য করুন — "hello" + 5-এর এরর মেসেজে সরাসরি ব্যর্থ নোডটি (("binop", "+", ("lit", "hello"), ("lit", 5))) প্রিন্ট হচ্ছে। বড় একটি এক্সপ্রেশনের ভেতরে এই সাব-এক্সপ্রেশনটি অনেক গভীরে থাকলেও বটম-আপ ট্রি-ওয়াক সেই ঠিক নোডেই এররটি ধরত — কারণ প্রতিটি binop নোড নিজের সন্তানদের টাইপ যাচাই করে নিজের এরর রিপোর্ট করে, পুরো ট্রির কথা না ভেবেই।
মূল কথা · Key takeaway

টাইপ চেকিং একটি সরল রিকার্সিভ ধারণা — প্রতিটি নোড "আমার সন্তানরা কী টাইপ, এবং সেগুলো কি আমার জন্য গ্রহণযোগ্য?" প্রশ্নের উত্তর দেয়। এই একই প্যাটার্ন — সন্তানদের ফলাফল কম্বাইন করে নিজের ফলাফল গণনা করা — L22-এর এভালুয়েটরেও দেখা গিয়েছিল, শুধু এবার মান-এর বদলে টাইপ প্রবাহিত হচ্ছে। এই সংযোগটিই পরের পাঠ (L31) স্পষ্টভাবে নাম দেবে।

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

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

প্র ০১ টাইপ চেকিং কেন অবশ্যই বটম-আপ হতে হবে — টপ-ডাউন (রুট থেকে শুরু করে) টাইপ চেক করলে কী সমস্যা হতো?

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

প্র ০২ কোড সেলে STRING+STRING বৈধ কিন্তু INT+STRING নয়। এই দুটো নিয়মকে একটি একক শর্তে (একই if-এ) মিলিয়ে ফেলা কি সম্ভব, নাকি আলাদা রাখা জরুরি?

কোড সেলে এগুলো ইচ্ছাকৃতভাবে আলাদা রাখা হয়েছে — প্রথমে বিশেষ কেস (STRING+STRING) চেক করা হয়, তারপর সাধারণ কেস (উভয়ই numeric) চেক করা হয়, নাহলে এরর। এই ক্রমটি গুরুত্বপূর্ণ — যদি প্রথমে numeric-চেক করা হতো এবং STRING সেই চেকে ব্যর্থ হয়ে সরাসরি এরর দিত, তাহলে STRING+STRING কখনো তার বৈধ concatenation সুযোগ পেত না। অর্ডার আলাদা রাখা একটি ইচ্ছাকৃত ডিজাইন সিদ্ধান্ত, দুর্ঘটনাক্রমে নয়।

প্র ০৩ যদি x-এর টাইপ সিম্বল টেবিলে "FLOAT" হিসেবে ইনসার্ট করা হতো (INT-এর বদলে), তাহলে x + 5-এর ফলাফলের টাইপ কী হতো, এবং কেন?

ফলাফল হতো FLOAT — কারণ arithmetic রুল অনুযায়ী left_type in NUMERIC and right_type in NUMERIC সত্য হলে, ফলাফল হয় "FLOAT" if "FLOAT" in (left_type, right_type) else "INT"। এখানে left_type হতো "FLOAT" (x-এর টাইপ) এবং right_type হতো "INT" (লিটারেল 5-এর টাইপ) — যেহেতু দুটোর মধ্যে অন্তত একটি FLOAT, ফলাফল FLOAT হবে। এটিই এই পাঠের টাইপ-প্রমোশন রুল — FLOAT সবসময় "জেতে" যখন INT ও FLOAT একসাথে ব্যবহৃত হয়।

অনুশীলন

  1. চিন্তা করুন: type_check()-এ comparison অপারেটরের রুল হলো উভয় অপারেন্ডের টাইপ ঠিক এক হতে হবে (left_type != right_type হলে এরর)। এটি কি 3 < 4.5-এর মতো (INT বনাম FLOAT) একটি স্বাভাবিক তুলনাকেও REJECT করে দেবে? এটি কি একটি সমস্যা?

    হ্যাঁ, উপরের সরল রুল অনুযায়ী 3 < 4.5 REJECT হবে, কারণ INT != FLOAT। বাস্তব ভাষাগুলো সাধারণত comparison-এও arithmetic-এর মতো numeric-প্রমোশন যোগ করে (উভয়ই numeric হলে গ্রহণযোগ্য, টাইপ হুবহু এক না হলেও)। এই পাঠের কোড সেল ইচ্ছাকৃতভাবে সরল রাখা হয়েছে যাতে মূল ধারণাটি (comparison সামঞ্জস্যপূর্ণ টাইপ চায়, BOOL ফেরত দেয়) স্পষ্ট থাকে — একটি বাস্তব কম্পাইলার এই রুলকে numeric টাইপের জন্য আরও নমনীয় করে সম্প্রসারণ করত, ঠিক যেভাবে arithmetic রুলে NUMERIC সেট ব্যবহার করা হয়েছে।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন টেস্ট কেস যোগ করুন — ("binop", "<", ("lit", 3), ("lit", "five")) (৩-এর সাথে একটি স্ট্রিং তুলনা করা) — এটি type_check()-এ চালিয়ে দেখুন এরর মেসেজ কী দেখায়।

    এটি এরর দেবে: টাইপ এরর: তুলনা অপারেটর '<' ভিন্ন টাইপ INT ও STRING-এর মধ্যে প্রযোজ্য নয় (ব্যর্থ নোড: ('binop', '<', ('lit', 3), ('lit', 'five')))। এটি দেখায় comparison রুলও ঠিক arithmetic রুলের মতোই ব্যর্থ নোডটি স্পষ্টভাবে চিহ্নিত করে রিপোর্ট করে — টাইপ চেকিং কোডের প্রতিটি শাখাই (arithmetic, comparison) একই "নোড-স্তরের এরর" নীতি অনুসরণ করে।

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

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