ইন্টারমিডিয়েট রিপ্রেজেন্টেশন ও থ্রি-অ্যাড্রেস কোড
এই পাঠে যা শিখবেন
- 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$$
সোর্স এক্সপ্রেশনে ঠিক ৩টি অপারেশন (একটি যোগ, একটি বিয়োগ, একটি গুণ) — এবং এখানে ঠিক ৩টি থ্রি-অ্যাড্রেস ইনস্ট্রাকশন তৈরি হলো, একটি সরাসরি এক-এক মিল যা স্পষ্টভাবে উল্লেখযোগ্য।
৪ · কোড: generate_3ac ও নিজে-এক্সিকিউট করে যাচাই
নিচের কোড সেলে generate_3ac একটি এক্সপ্রেশন AST (নেস্টেড টাপল, M5/L22-এর পার্স-ট্রি কাঠামোর
পুনর্ব্যবহার) recursively হেঁটে 3AC ইনস্ট্রাকশন emit করে। এরপর execute_3ac সেই জেনারেটেড
কোডটিই a, b, c, d-এর concrete মান দিয়ে চালিয়ে ফলাফল সরাসরি মূল এক্সপ্রেশনের সাথে মিলিয়ে দেখায়।
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 থেকে — সেই জেনারেটেড ইনস্ট্রাকশন তালিকাটিই চালিয়ে,
মূল নেস্টেড এক্সপ্রেশনের সরাসরি ইভালুয়েশনের সাথে বিট-বাই-বিট মিলিয়ে।
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
একটি ইনস্ট্রাকশন-বাই-ইনস্ট্রাকশন এক্সিকিউশন মডেল) স্পষ্টভাবে দেখানোর সুযোগ নষ্ট করে দিত।
অনুশীলন
-
পরীক্ষা করুন: উপরের কোডে
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পাওয়া যাবে — assertionresult_from_3ac == direct_resultএখনও পাস করবে, কারণgenerate_3ac/execute_3ac-এর লজিক নির্দিষ্ট কোনো মানের উপর নির্ভর করে না, যেকোনো ইনপুটের জন্য একইভাবে কাজ করে। -
চিন্তা করুন: এই লেসনের AST-তে যদি আরেক স্তর নেস্টিং যোগ করা হতো — যেমন
((a+b)*(c-d)) + e— জেনারেটেড 3AC-তে মোট কতগুলো ইনস্ট্রাকশন থাকত, এবং শেষ ইনস্ট্রাকশনটি কেমন দেখাত?মূল এক্সপ্রেশনে মোট ৪টি অপারেশন (যোগ, বিয়োগ, গুণ, তারপর আরেকটি যোগ) — তাই ৪টি ইনস্ট্রাকশন হবে:
t1=a+b,t2=c-d,t3=t1*t2(এই তিনটি এই লেসনের কোডের মতোই), এবং সবশেষে চতুর্থ ইনস্ট্রাকশনt4=t3+e— সবচেয়ে বাইরের অপারেশন (এখানে সবচেয়ে-শেষ যোগ) সবসময় জেনারেটেড তালিকার সবচেয়ে শেষ ইনস্ট্রাকশন হয়, কারণ সেটির ফলাফলের জন্য তার সাব-এক্সপ্রেশনগুলোর ফলাফল আগে দরকার।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ পরের পাঠে — এই একই 3AC-কে বাস্তব টার্গেট মেশিন ইনস্ট্রাকশনে রূপান্তর করা হবে।
- Computer Architecture: ইনস্ট্রাকশন ফরম্যাট ও এনকোডিং সহোদর কোর্স 3AC-এর পরের গন্তব্য — এই পাঠের ইনস্ট্রাকশন ফরম্যাটেই কোড জেনারেশন (M12/L52) আসলে নেমে আসে।
- Data Structures & Algorithms কোর্স সঙ্গী কোর্স AST-এর উপর recursive tree-traversal — এই কোর্সের tree ডেটা স্ট্রাকচার ও ট্রাভার্সাল প্যাটার্নের সরাসরি প্রয়োগ।