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

ইন্টারমিডিয়েট রিপ্রেজেন্টেশন ও থ্রি-অ্যাড্রেস কোড

Intermediate representations & three-address code
৯ মিনিট পড়া উচ্চ · Advanced Python কোডসহ সম্পূর্ণ বাংলায়

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

  • IR কী, এবং কেন এটি কম্পাইলারের front-end/back-end বিভাজনের জন্য গুরুত্বপূর্ণ
  • থ্রি-অ্যাড্রেস কোডের সুনির্দিষ্ট সংজ্ঞা ও এর প্রতিটি ইনস্ট্রাকশনের আকৃতি
  • একটি নেস্টেড এক্সপ্রেশনকে টেম্পোরারি ভ্যারিয়েবলসহ 3AC-তে রূপান্তরের পদ্ধতি
  • একটি বাস্তব generate_3ac ফাংশন লেখা এবং জেনারেটেড কোড এক্সিকিউট করে তার সঠিকতা যাচাই করা

১ · Intermediate Representation কী ও কেন দরকার

Intermediate RepresentationIntermediate Representation (IR)সোর্স-ভাষার AST ও টার্গেট মেশিন কোডের মাঝামাঝি একটি রূপ — সোর্সের চেয়ে সরল ও উদার, কিন্তু নির্দিষ্ট টার্গেট মেশিন থেকে মোটামুটি স্বাধীন। (IR) হলো প্রোগ্রামের এমন একটি রূপ যা হাই-লেভেল সোর্স-ভাষার AST (M5-এর আউটপুট) এবং লো-লেভেল টার্গেট মেশিন কোডের (Computer Architecture কোর্সের ISA, M12/L52-এর চূড়ান্ত টার্গেট) ঠিক মাঝামাঝি — L05-এর পাইপলাইন-ওভারভিউয়ের ৪র্থ ধাপের সুনির্দিষ্ট বাস্তবায়ন, যা এতদিন পিছিয়ে রাখা হয়েছিল যতক্ষণ না দরকারি AST/সিমান্টিক-অ্যানালাইসিসের ভিত্তি (M5-M7) প্রস্তুত হয়।

IR কেন দরকার — L05-এর front-end/back-end বিভাজনের সুবিধা এখন বাস্তবে দেখা যাক: IR ইচ্ছাকৃতভাবে সোর্স ভাষার চেয়ে সরল ও অভিন্ন (optimize করা সহজ, M12/L53) কিন্তু কোনো নির্দিষ্ট টার্গেট মেশিন থেকে মোটামুটি স্বাধীন — একটি একক IR নীতিগতভাবে একাধিক ভিন্ন টার্গেট আর্কিটেকচারে অনুবাদযোগ্য। এটাই সরাসরি বাস্তব-জগতের প্রাসঙ্গিকতা — LLVM-এর মতো কম্পাইলার ইনফ্রাস্ট্রাকচার (L05-এ উল্লেখিত) ঠিক এভাবেই একাধিক সোর্স-ভাষা/টার্গেট-মেশিন কম্বিনেশন সমর্থন করে।

২ · থ্রি-অ্যাড্রেস কোড (3AC)

থ্রি-অ্যাড্রেস কোডThree-Address Code (3AC)একটি IR ফর্ম যেখানে প্রতিটি ইনস্ট্রাকশনে সর্বোচ্চ তিনটি অপারেন্ড/অ্যাড্রেস থাকে — সাধারণত result = operand1 OP operand2 আকারে। হলো এই কোর্সের ব্যবহৃত স্ট্যান্ডার্ড, কংক্রিট IR ফর্ম — এর সংজ্ঞায়ক বৈশিষ্ট্য: প্রতিটি ইনস্ট্রাকশনে সর্বোচ্চ তিনটি অপারেন্ড/অ্যাড্রেস থাকে — সাধারণত result = operand1 OP operand2 আকারে (এক ইনস্ট্রাকশনে ঠিক একটি অপারেশন, নেস্টেড সোর্স-এক্সপ্রেশনের বিপরীতে যেখানে অনেকগুলো অপারেশন একসাথে গাঁথা থাকতে পারে) — একটি সত্যিকারের সরল, অভিন্ন ইনস্ট্রাকশন আকৃতি, নেস্টেড সোর্স এক্সপ্রেশনের চেয়ে — সরাসরি motivation: সরল ইনস্ট্রাকশন পরবর্তী ধাপগুলোর (অপ্টিমাইজেশন, কোড জেনারেশন) জন্য যান্ত্রিকভাবে বিশ্লেষণ ও রূপান্তর করা সহজ করে তোলে।

৩ · ওয়ার্কড এক্সাম্পল — (a + b) * (c - d)

নেস্টেড সোর্স এক্সপ্রেশন (a + b) * (c - d)-কে 3AC-তে অনুবাদ করা যাক — প্রতিটি ইন্টারমিডিয়েট উপ-ফলাফলের জন্য একটি টেম্পোরারি ভ্যারিয়েবল চালু করে:

$$t_1 = a + b \qquad t_2 = c - d \qquad t_3 = t_1 \times t_2$$

সোর্স এক্সপ্রেশনে ঠিক ৩টি অপারেশন (একটি যোগ, একটি বিয়োগ, একটি গুণ) — এবং এখানে ঠিক ৩টি থ্রি-অ্যাড্রেস ইনস্ট্রাকশন তৈরি হলো, একটি সরাসরি এক-এক মিল যা স্পষ্টভাবে উল্লেখযোগ্য।

AST: (a + b) * (c - d) generate_3ac() → t1=a+b, t2=c-d, t3=t1*t2 execute_3ac() → t3-এর মান, সরাসরি ইভালুয়েশনের সাথে মেলানো
L05-এর পাইপলাইনের ৪র্থ ধাপ, এখন কংক্রিট — AST থেকে 3AC জেনারেট করে সেই কোডই সরাসরি এক্সিকিউট করে সঠিকতা যাচাই করা হচ্ছে।

৪ · কোড: generate_3ac ও নিজে-এক্সিকিউট করে যাচাই

নিচের কোড সেলে generate_3ac একটি এক্সপ্রেশন AST (নেস্টেড টাপল, M5/L22-এর পার্স-ট্রি কাঠামোর পুনর্ব্যবহার) recursively হেঁটে 3AC ইনস্ট্রাকশন emit করে। এরপর execute_3ac সেই জেনারেটেড কোডটিই a, b, c, d-এর concrete মান দিয়ে চালিয়ে ফলাফল সরাসরি মূল এক্সপ্রেশনের সাথে মিলিয়ে দেখায়।

Python
def generate_3ac(node, temp_counter):
    """একটি এক্সপ্রেশন AST (নেস্টেড টাপল) থেকে থ্রি-অ্যাড্রেস কোড emit করে।

    node আকৃতি: ("var", নাম)  অথবা  ("op", অপারেটর, বাম_নোড, ডান_নোড)
    রিটার্ন করে: (এই সাব-এক্সপ্রেশনের ফলাফল রাখা অপারেন্ড, ইনস্ট্রাকশন-লিস্ট, পরবর্তী temp_counter)
    """
    if node[0] == "var":
        return node[1], [], temp_counter

    _, op, left, right = node
    left_operand, left_instrs, temp_counter = generate_3ac(left, temp_counter)
    right_operand, right_instrs, temp_counter = generate_3ac(right, temp_counter)

    temp_name = f"t{temp_counter}"
    temp_counter += 1
    this_instr = (temp_name, left_operand, op, right_operand)

    all_instrs = left_instrs + right_instrs + [this_instr]
    return temp_name, all_instrs, temp_counter


# সোর্স এক্সপ্রেশন: (a + b) * (c - d)
ast = ("op", "*",
       ("op", "+", ("var", "a"), ("var", "b")),
       ("op", "-", ("var", "c"), ("var", "d")))

final_temp, instructions, _ = generate_3ac(ast, temp_counter=1)

print("জেনারেটেড থ্রি-অ্যাড্রেস কোড:")
for target, op1, op, op2 in instructions:
    print(f"  {target} = {op1} {op} {op2}")

expected = [("t1", "a", "+", "b"), ("t2", "c", "-", "d"), ("t3", "t1", "*", "t2")]
assert instructions == expected
print(f"\nহাতে-ডেরাইভ করা প্রত্যাশিত 3AC-এর সাথে হুবহু মিলেছে: {instructions == expected}")


def execute_3ac(instructions, env):
    """জেনারেটেড 3AC ইনস্ট্রাকশনগুলো ক্রমান্বয়ে এক্সিকিউট করে -- eval/exec ছাড়াই।"""
    env = dict(env)
    for target, op1, op, op2 in instructions:
        v1, v2 = env[op1], env[op2]
        if op == "+":
            env[target] = v1 + v2
        elif op == "-":
            env[target] = v1 - v2
        elif op == "*":
            env[target] = v1 * v2
        elif op == "/":
            env[target] = v1 / v2
        else:
            raise ValueError(f"অজানা অপারেটর: {op}")
    return env[final_temp]


sample_values = {"a": 3, "b": 5, "c": 10, "d": 4}
result_from_3ac = execute_3ac(instructions, sample_values)
direct_result = (sample_values["a"] + sample_values["b"]) * (sample_values["c"] - sample_values["d"])

print(f"\nনমুনা মান: {sample_values}")
print(f"3AC এক্সিকিউট করে ফলাফল      = {result_from_3ac}")
print(f"সরাসরি এক্সপ্রেশন ইভালুয়েট করে ফলাফল = {direct_result}")
assert result_from_3ac == direct_result
print(f"যাচাই: দুটো ফলাফল অভিন্ন -> {result_from_3ac == direct_result}")

    
লক্ষ্য করুন — generate_3ac কখনো নিজে কোনো মান গণনা করে না, এটি শুধু ইনস্ট্রাকশনের একটি তালিকা তৈরি করে (কোড জেনারেশন থেকে এক্সিকিউশন সম্পূর্ণ আলাদা ধাপ)। সঠিকতার আসল প্রমাণ আসে execute_3ac থেকে — সেই জেনারেটেড ইনস্ট্রাকশন তালিকাটিই চালিয়ে, মূল নেস্টেড এক্সপ্রেশনের সরাসরি ইভালুয়েশনের সাথে বিট-বাই-বিট মিলিয়ে।
মূল কথা · Key takeaway

3AC একটি নেস্টেড এক্সপ্রেশনকে সরল, একক-অপারেশন ইনস্ট্রাকশনের একটি ফ্ল্যাট তালিকায় রূপান্তর করে — প্রতিটি ইন্টারমিডিয়েট ফলাফলের নিজস্ব নাম (temporary) থাকে। এই ফ্ল্যাট, uniform গঠনই কম্পাইলারের পরের ধাপগুলোকে (M12/L52-এর কোড জেনারেশন, M12/L53-এর অপ্টিমাইজেশন) সহজ করে তোলে — কোনো নেস্টেড গঠন বিশ্লেষণ করতে হয় না, শুধু একটি সরল ইনস্ট্রাকশন-তালিকা ধরে হাঁটতে হয়।

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

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

প্র ০১ উপরের কোডে generate_3ac প্রথমে বাম সাব-এক্সপ্রেশন (a+b) নাকি ডান সাব-এক্সপ্রেশন (c-d) থেকে ইনস্ট্রাকশন তৈরি করে, এবং এটি জেনারেটেড তালিকার অর্ডারে কীভাবে দেখা যায়?

কোডে left_operand, left_instrs, temp_counter = generate_3ac(left, ...) ডান দিকের কল করার আগেই চলে — তাই বাম সাব-এক্সপ্রেশন (a+b) প্রথমে প্রসেস হয়, এবং এর ফলাফল t1 নাম পায়। তারপর ডান সাব-এক্সপ্রেশন (c-d) প্রসেস হয়ে t2 নাম পায়। জেনারেটেড তালিকায় এই অর্ডারই দেখা যায়: t1=a+b সবার আগে, তারপর t2=c-d, তারপর দুটোকে একত্র করা t3=t1*t2 সবার শেষে।

প্র ০২ যদি সোর্স এক্সপ্রেশন হতো a + b * c (একটি single-level নয়, বরং ভিন্ন অপারেটর প্রায়োরিটির নেস্টেড AST — অর্থাৎ ("op","+",("var","a"),("op","*",("var","b"),("var","c")))), তাহলে জেনারেটেড 3AC কী হতো?

generate_3ac বাম সাব-নোড (("var","a")) প্রসেস করবে, যা সরাসরি "a" রিটার্ন করে (কোনো নতুন ইনস্ট্রাকশন ছাড়াই, কারণ এটি ইতিমধ্যে একটি সরল ভ্যারিয়েবল)। তারপর ডান সাব-নোড (b*c) প্রসেস হয়ে একটি ইনস্ট্রাকশন t1 = b * c তৈরি করবে। সবশেষে এই দুটোকে যোগ করার জন্য t2 = a + t1। ফলাফল: [t1=b*c, t2=a+t1] — মাত্র ২টি ইনস্ট্রাকশন, কারণ মূল এক্সপ্রেশনে মাত্র ২টি অপারেশন আছে (একটি গুণ, একটি যোগ) — সেই "N অপারেশন → N ইনস্ট্রাকশন" নিয়মটি এখানেও ঠিক বজায় থাকে।

প্র ০৩ execute_3ac কেন eval() বা Python-এর নিজস্ব অপারেটর-পার্সিং ব্যবহার না করে প্রতিটি অপারেটরের জন্য স্পষ্ট if/elif শাখা লিখেছে?

এই কোর্সের নিয়ম অনুযায়ী (programming-languages-compilers/CLAUDE.md) eval(), exec(), বা compile() কখনোই মূল লেক্সিং/পার্সিং/এক্সিকিউশন লজিকের শর্টকাট হিসেবে ব্যবহার করা যাবে না — শিক্ষণীয় লক্ষ্যই হলো টেকনিকটি নিজ হাতে বাস্তবায়ন করা, Python-এর নিজস্ব কম্পাইলার/ইন্টারপ্রেটারে কাজ চাপিয়ে দেওয়া নয়। eval("v1 + v2") ব্যবহার করলে আসলে Python-এর নিজস্ব এক্সপ্রেশন-এক্সিকিউশন ইঞ্জিনকেই কাজটা করতে দেওয়া হতো, যা এই লেসনের মূল বিষয়বস্তু (3AC একটি ইনস্ট্রাকশন-বাই-ইনস্ট্রাকশন এক্সিকিউশন মডেল) স্পষ্টভাবে দেখানোর সুযোগ নষ্ট করে দিত।

অনুশীলন

  1. পরীক্ষা করুন: উপরের কোডে sample_values-এর মান পাল্টে {"a": 2, "b": 0, "c": 7, "d": 7} করে চালিয়ে দেখুন result_from_3ac ও direct_result এখনও মেলে কিনা।

    হাতে হিসাব: (2+0)*(7-7) = 2*0 = 0। জেনারেটেড 3AC এক্সিকিউট করলেও ঠিক t1=2, t2=0, t3=0 পাওয়া যাবে — assertion result_from_3ac == direct_result এখনও পাস করবে, কারণ generate_3ac/execute_3ac-এর লজিক নির্দিষ্ট কোনো মানের উপর নির্ভর করে না, যেকোনো ইনপুটের জন্য একইভাবে কাজ করে।

  2. চিন্তা করুন: এই লেসনের AST-তে যদি আরেক স্তর নেস্টিং যোগ করা হতো — যেমন ((a+b)*(c-d)) + e — জেনারেটেড 3AC-তে মোট কতগুলো ইনস্ট্রাকশন থাকত, এবং শেষ ইনস্ট্রাকশনটি কেমন দেখাত?

    মূল এক্সপ্রেশনে মোট ৪টি অপারেশন (যোগ, বিয়োগ, গুণ, তারপর আরেকটি যোগ) — তাই ৪টি ইনস্ট্রাকশন হবে: t1=a+b, t2=c-d, t3=t1*t2 (এই তিনটি এই লেসনের কোডের মতোই), এবং সবশেষে চতুর্থ ইনস্ট্রাকশন t4=t3+e — সবচেয়ে বাইরের অপারেশন (এখানে সবচেয়ে-শেষ যোগ) সবসময় জেনারেটেড তালিকার সবচেয়ে শেষ ইনস্ট্রাকশন হয়, কারণ সেটির ফলাফলের জন্য তার সাব-এক্সপ্রেশনগুলোর ফলাফল আগে দরকার।

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

পূর্ববর্তী পাঠ
রিকার্শন ইমপ্লিমেন্টেশন