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

টাইপ সেফটি ও সাউন্ডনেস

Type safety & soundness
৭ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • টাইপ সেফটির সংজ্ঞা এবং কেন এটি টাইপ চেকিং (M6-M7) করার পুরো যুক্তির ভিত্তি
  • টাইপ সাউন্ডনেস — Progress ও Preservation প্রোপার্টি সুনির্দিষ্টভাবে
  • একটি রিয়েল ছোট এভালুয়েটরে preservation যাচাই করে দেখা
  • একটি আনসাউন্ড রুল কীভাবে preservation-লঙ্ঘন হিসেবে ধরা পড়ে
  • প্রতিটি বাস্তব ভাষা কি পুরোপুরি সাউন্ড? — একটি সৎ উত্তর

১ · টাইপ সেফটি কী ও কেন গুরুত্বপূর্ণ

টাইপ সেফটিType Safetyএকটি ভাষা টাইপ-সেফ যদি well-typed প্রোগ্রামের পক্ষে রানটাইমে সিমান্টিকালি অর্থহীন কোনো অপারেশন করা অসম্ভব হয়। বলতে বোঝায় — একটি ভাষা টাইপ-সেফ যদি well-typed প্রোগ্রামের (যারা L30-এর টাইপ চেকার পাস করেছে) পক্ষে রানটাইমে এমন কোনো অপারেশন করা অসম্ভব হয় যা আসলে জড়িত ভ্যালুর জন্য সিমান্টিকালি অর্থহীন (যেমন একটি integer-এর raw বিট প্যাটার্নকে যেন সেটা একটি বৈধ মেমরি পয়েন্টার, এভাবে ট্রিট করা)। এই ধারণাটিই আসলে বলে দেয় M6-M7-এর সমস্ত টাইপ-চেকিং প্রচেষ্টা কেন করা হয় — যদি একটি ভাষা টাইপ-সেফ না হতো, তাহলে টাইপ চেকার পাস করা কোনো বাস্তব রানটাইম গ্যারান্টিই দিত না।

২ · টাইপ সাউন্ডনেস — ফরমাল সংস্করণ

টাইপ সাউন্ডনেসType Soundnessটাইপ সেফটির ফরমাল, প্রমাণযোগ্য সংস্করণ — Progress ও Preservation, দুটো প্রোপার্টি একসাথে প্রতিষ্ঠিত। হলো টাইপ সেফটির ফরমাল সংস্করণ, প্রায়ই একটি পরিচিত স্লোগানে সংক্ষেপিত — "well-typed programs don't go wrong।" এটি দুটো প্রোপার্টি একসাথে কাজ করে প্রতিষ্ঠিত হয় —

Progress
একটি well-typed এক্সপ্রেশন হয় ইতিমধ্যে একটি চূড়ান্ত ভ্যালু, নয়তো এটি আরেকটি এভালুয়েশন ধাপ নিতে পারে — এটি কখনো "আটকে" যায় না, কোনো সংজ্ঞায়িত পরবর্তী পদক্ষেপ ছাড়াই।
Preservation
যদি একটি well-typed এক্সপ্রেশন একটি এভালুয়েশন ধাপ নেয়, তাহলে ফলাফলও এখনো একই টাইপের well-typed — এভালুয়েশন এগিয়ে যাওয়ার সাথে সাথে টাইপ সংরক্ষিত থাকে।

ফরমাল নোটেশনে, preservation বলা হয় — যদি $e : \tau$ এবং $e \to e'$ (এক্সপ্রেশন $e$ এক ধাপ এভালুয়েট হয়ে $e'$ হয়), তাহলে $e' : \tau$ (একই টাইপ)। progress বলা হয় — যদি $e : \tau$, তাহলে হয় $e$ একটি ভ্যালু, নয়তো কোনো $e'$ আছে যার জন্য $e \to e'$।

৩ · কোড দিয়ে যাচাই — (2+3)*4-এর প্রতিটি ধাপে Preservation

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

Python
# AST নোড: ('num', v) | ('str', v) | ('binop', op, left, right) | ('cast', subexpr)

def is_value(e):
    """একটি নোড কি ইতিমধ্যে একটি চূড়ান্ত ভ্যালু (আর কোনো ধাপ প্রয়োজন নেই)?"""
    return e[0] in ('num', 'str')


def type_of(e):
    kind = e[0]
    if kind == 'num':
        return 'int'
    if kind == 'str':
        return 'str'
    if kind == 'binop':
        _, op, left, right = e
        lt, rt = type_of(left), type_of(right)
        if lt == 'int' and rt == 'int' and op in ('+', '-', '*'):
            return 'int'
        raise TypeError(f"টাইপ এরর: '{op}' {lt} ও {rt}-এর মধ্যে প্রযোজ্য নয়")
    if kind == 'cast':
        # *** ইচ্ছাকৃতভাবে ভুল/আনসাউন্ড রুল -- সত্যিকারে subexpression কী হয়ে যাবে তা যাচাই না করেই দাবি করে ফলাফল সবসময় 'int' ***
        return 'int'
    raise ValueError(f"অজানা নোড: {e}")


def step(e):
    """স্মল-স্টেপ এভালুয়েটর -- একটি একক এভালুয়েশন ধাপ নিয়ে (নতুন_এক্সপ্রেশন, ধাপ_নেওয়া_হয়েছে_কিনা) ফেরত দেয়।"""
    kind = e[0]
    if kind in ('num', 'str'):
        return e, False  # ইতিমধ্যে ভ্যালু -- আর কোনো ধাপ নেই
    if kind == 'binop':
        _, op, left, right = e
        if not is_value(left):
            new_left, _ = step(left)
            return ('binop', op, new_left, right), True
        if not is_value(right):
            new_right, _ = step(right)
            return ('binop', op, left, new_right), True
        lv, rv = left[1], right[1]
        result = {'+': lv + rv, '-': lv - rv, '*': lv * rv}[op]
        return ('num', result), True
    if kind == 'cast':
        _, sub = e
        if not is_value(sub):
            new_sub, _ = step(sub)
            return ('cast', new_sub), True
        # "cast" আসলে যা করে: সংখ্যাটিকে তার স্ট্রিং রিপ্রেজেন্টেশনে রূপান্তর করে -- str ফেরত দেয়, int নয়!
        v = sub[1]
        return ('str', str(v)), True
    raise ValueError(f"অজানা নোড: {e}")


print("=== (2+3)*4 -- প্রতিটি ধাপে Progress ও Preservation যাচাই ===")
expr = ('binop', '*', ('binop', '+', ('num', 2), ('num', 3)), ('num', 4))
print(f"শুরু: {expr}   টাইপ: {type_of(expr)}\n")

step_no = 0
while not is_value(expr):
    t_before = type_of(expr)                 # ধাপের আগে টাইপ
    new_expr, stepped = step(expr)
    assert stepped, "PROGRESS লঙ্ঘিত! well-typed এক্সপ্রেশন আটকে গেছে।"   # Progress
    t_after = type_of(new_expr)               # ধাপের পরে টাইপ
    step_no += 1
    print(f"ধাপ {step_no}: {expr}  →  {new_expr}   (টাইপ: {t_before} → {t_after})")
    assert t_before == t_after, "PRESERVATION লঙ্ঘিত! টাইপ পরিবর্তিত হয়ে গেছে।"  # Preservation
    expr = new_expr

print(f"\nচূড়ান্ত ভ্যালু: {expr}  (টাইপ: {type_of(expr)})")
assert expr == ('num', 20)
print("সব ধাপে Progress ও Preservation ধরে রাখা হয়েছে -- (2+3)*4 নিরাপদে 20-এ এভালুয়েট হয়েছে।\n")


print("=== ইচ্ছাকৃত আনসাউন্ড 'cast' রুল -- Preservation লঙ্ঘন ধরা ===")
cast_expr = ('cast', ('num', 7))
t_before = type_of(cast_expr)
print(f"'cast' নিয়মের naive স্ট্যাটিক টাইপ: {t_before}   (এই দাবিটি টাইপ চেকার যাচাই ছাড়াই মেনে নিয়েছে!)")

new_expr, _ = step(cast_expr)
t_after = type_of(new_expr)
print(f"এভালুয়েশনের পর প্রকৃত এক্সপ্রেশন: {new_expr}   প্রকৃত টাইপ: {t_after}")

if t_before != t_after:
    print(f"\n>>> SOUNDNESS ভায়োলেশন ধরা পড়েছে! Preservation দাবি করে টাইপ '{t_before}' থাকা উচিত ছিল, "
          f"কিন্তু এভালুয়েশনের পর প্রকৃত টাইপ '{t_after}' -- 'cast' নিয়মটি আনসাউন্ড, ঠিক যেমনটা ইচ্ছাকৃতভাবে বানানো হয়েছিল।")
else:
    print("(এটা প্রিন্ট হওয়ার কথা না)")

    
type_of-এর 'cast' শাখাটি লক্ষ্য করুন — এটি সাবএক্সপ্রেশন আসলে কী মান ধারণ করে তা একেবারেই পরীক্ষা না করেই সবসময় 'int' রিটার্ন করে। এই একটি সরলীকৃত কিন্তু বাস্তব উদাহরণ কীভাবে একটি টাইপ চেকারের নিয়ম দেখতে সঠিক মনে হতে পারে (এটি প্রোগ্রাম কম্পাইল/গ্রহণ করে দেয়) অথচ প্রকৃতপক্ষে সাউন্ড নয় — এবং কীভাবে preservation-এর মতো একটি পোস্ট-স্টেপ চেক ঠিক এই ধরনের ফাঁক ধরিয়ে দিতে পারে।

৪ · সততার সাথে — কোনো ভাষাই নিখুঁতভাবে সাউন্ড নয়

প্রতিটি বাস্তব ভাষাই কি পুরোপুরি টাইপ-সাউন্ড? সৎভাবে বললে — না। কিছু বহুল-ব্যবহৃত ভাষায় পরিচিত, ডকুমেন্টেড "টাইপ হোল" আছে। একটি সুপরিচিত উদাহরণ: Java-এর array covariance — ঐতিহাসিকভাবে এটি এমন কিছু অ্যারে-অপারেশনের অনুমতি দিত যা স্ট্যাটিক টাইপ চেকার পাস করে যায়, কিন্তু রানটাইমে একটি ArrayStoreException ছুড়তে পারে — একটি প্রকৃত, ডকুমেন্টেড সাউন্ডনেস গ্যাপ। এটি একটি সচেতন ট্রেড-অফ — Java-এর ক্ষেত্রে, অ্যারে ব্যবহারে বাড়তি নমনীয়তার বিনিময়ে এই ঝুঁকি নেওয়া হয়েছে। মূল কথা — সাউন্ডনেসকে একটি সব-অথবা-কিছুই এবসোলিউট হিসেবে না দেখে, একটি ভাষা-ডিজাইনার সচেতনভাবে গ্রহণ করা ট্রেড-অফ হিসেবে দেখা উচিত।

মূল কথা · Key takeaway

Progress নিশ্চিত করে একটি well-typed প্রোগ্রাম কখনো "আটকে" যাবে না; Preservation নিশ্চিত করে এভালুয়েশন এগিয়ে যাওয়ার সাথে সাথে টাইপ বদলে যাবে না। দুটো একসাথে মানেই — টাইপ চেকার যা প্রতিশ্রুতি দেয়, রানটাইম তা সত্যিই রক্ষা করে। M6-M7-এর পুরো টাইপ-চেকিং প্রচেষ্টা (L30 থেকে L35 পর্যন্ত) আসলে এই একটি গ্যারান্টির উপরই দাঁড়িয়ে — এবং, উপরের cast-উদাহরণ যেমন দেখাল, এই গ্যারান্টি স্বয়ংক্রিয়ভাবে আসে না — এটি প্রতিটি টাইপ নিয়ম সাবধানে ডিজাইন করেই অর্জন করতে হয়।

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

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

প্র ০১ উপরের কোডে 'cast'-এর type_of নিয়মটি ঠিক কীভাবে "আনসাউন্ড" — এটিকে "সাউন্ড" করতে কী পরিবর্তন করতে হবে?

এটি আনসাউন্ড কারণ এটি সাবএক্সপ্রেশন (sub) বা step()-এ আসলে কী ঘটবে তা একেবারেই না দেখে অন্ধভাবে 'int' রিটার্ন করে — টাইপ চেকার ও এভালুয়েটরের মধ্যে কোনো সংযোগ নেই। একে সাউন্ড করতে হলে, type_of-কে step-এর প্রকৃত রূপান্তরের সাথে সামঞ্জস্যপূর্ণ হতে হবে — যেমন, যদি "cast" আসলে সংখ্যাকে স্ট্রিং-এ রূপান্তর করে, তাহলে type_of(('cast', sub))-এরও 'str' রিটার্ন করা উচিত (অথবা এই ভুল সিমান্টিক্সের "cast" অপারেশনটিকেই একেবারে বাতিল করা উচিত)।

প্র ০২ Progress প্রোপার্টি কি একটি এক্সপ্রেশনের মধ্যে ডিভিশন-বাই-জিরোর মতো রানটাইম এরর প্রতিরোধ করে?

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

প্র ০৩ Java-এর array covariance-এর মতো একটি "টাইপ হোল" থাকা কেন সবসময় খারাপ ডিজাইন নয় — L04-এর ভাষায় ব্যাখ্যা করুন।

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

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোড সেলে expr-কে ('binop', '-', ('num', 10), ('binop', '*', ('num', 2), ('num', 3))) (অর্থাৎ 10 - (2*3)) দিয়ে বদলে দিন এবং Run চাপুন — কয়টি ধাপে এটি একটি চূড়ান্ত ভ্যালুতে পৌঁছায়, এবং সেই ভ্যালুটি কী?

    প্রথম ধাপে ভেতরের (2*3) সাব-এক্সপ্রেশনটি এভালুয়েট হয়ে ('num', 6) হয়ে যায় (কারণ step() সবসময় left সাব-এক্সপ্রেশনকে আগে ভ্যালুতে না আনা পর্যন্ত এগোয় না, এখানে বাম দিক ('num',10) ইতিমধ্যে ভ্যালু বলে ডান দিকেই এগোয়), ফলে এক্সপ্রেশন হয়ে যায় 10 - 6। দ্বিতীয় ধাপে এটি এভালুয়েট হয়ে ('num', 4)-এ পৌঁছায় — মোট ২টি ধাপ, চূড়ান্ত ভ্যালু ৪, এবং প্রতিটি ধাপেই টাইপ int সংরক্ষিত থাকে (preservation ধরে রাখা হয়)।

  2. চিন্তা করুন: Python-এর একটি সাধারণ রানটাইম এরর, যেমন list index out of range, কি টাইপ সাউন্ডনেসের লঙ্ঘন?

    সাধারণত না — এটি টাইপ এরর নয়, এটি একটি বাউন্ডস এরর, যা এই পাঠের প্র-০২-এর উত্তরে বর্ণিত ডিভিশন-বাই-জিরোর মতোই একটি ক্যাটাগরি। একটি অ্যারে-ইনডেক্সিং অপারেশনের টাইপ নিয়ম সাধারণত শুধু চেক করে "ইনডেক্সটি কি একটি integer" — নির্দিষ্ট ভ্যালুটি বৈধ রেঞ্জের মধ্যে আছে কিনা তা নয় (এটি রানটাইমে নির্ধারিত হয়, কম্পাইল-টাইমে সাধারণত নয়)। তাই এই ধরনের এরর টাইপ সাউন্ডনেসের সংজ্ঞার বাইরে পড়ে, যদিও এটিও একটি বাস্তব, গুরুত্বপূর্ণ রানটাইম নিরাপত্তা সমস্যা।

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

আগের পাঠ
L35 · পলিমরফিজম — প্যারামেট্রিক, অ্যাড-হক ও সাবটাইপ