পাঠ ০৫ · ৫৮-এর মধ্যে · মডিউল ১
Home / Courses / Concepts of Programming Languages & Compiler Design / কম্পাইলার ফেজ

কম্পাইলার ফেজ ওভারভিউ

Compiler phases overview
৯ মিনিট পড়া শুরু · Beginner Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ছয়টি কম্পাইলার ফেজের প্রতিটির সুনির্দিষ্ট ইনপুট ও আউটপুট
  • ফ্রন্ট-এন্ড বনাম ব্যাক-এন্ড বিভাজন এবং এটি কেন গুরুত্বপূর্ণ
  • LLVM-এর মতো বাস্তব ইনফ্রাস্ট্রাকচারে এই বিভাজনের প্রয়োগ
  • Python দিয়ে একটি সম্পূর্ণ ৬-ফেজ পাইপলাইন চেইন করে বাস্তবায়ন

১ · সোর্স কোড থেকে মেশিন কোড — পূর্ণাঙ্গ পাইপলাইন

L01-এ আমরা এই পাইপলাইনের একটি সংক্ষিপ্ত প্রিভিউ দেখেছিলাম — এখন সম্পূর্ণ নির্ভুলতার সাথে দেখা যাক। এই পাঠটি পুরো কোর্সের মানচিত্র — প্রতিটি ফেজ এই কোর্সের একটি ভবিষ্যৎ মডিউলের বিষয়বস্তু।

সোর্স কোড (raw টেক্সট) ফ্রন্ট-এন্ড (ভাষা-নির্ভর) ১. লেক্সিক্যাল অ্যানালাইসিস (M4) → টোকেন ২. পার্সিং (M5) → পার্স ট্রি (AST) ৩. সিমান্টিক অ্যানালাইসিস (M6) → অ্যানোটেটেড AST ব্যাক-এন্ড (টার্গেট-মেশিন-নির্ভর) ৪. IR জেনারেশন (M12) → ইন্টারমিডিয়েট কোড ৫. কোড অপ্টিমাইজেশন (M12) → উন্নত IR ৬. টার্গেট কোড জেনারেশন (M12) → মেশিন কোড
প্রথম তিনটি ফেজ ("ফ্রন্ট-এন্ড") শুধু সোর্স ভাষার উপর নির্ভর করে; শেষ তিনটি ফেজ ("ব্যাক-এন্ড") শুধু টার্গেট মেশিনের উপর নির্ভর করে — এই বিভাজনই কম্পাইলার নির্মাণের সবচেয়ে গুরুত্বপূর্ণ স্থাপত্য সিদ্ধান্ত।

২ · প্রতিটি ফেজের সুনির্দিষ্ট ইনপুট ও আউটপুট

  • ১. লেক্সিক্যাল অ্যানালাইসিস (M4): ইনপুট — raw ক্যারেক্টার স্ট্রিম; আউটপুট — টোকেন স্ট্রিম (L01/L16-এর টোকেনাইজার প্রিভিউ)।
  • ২. সিনট্যাক্স অ্যানালাইসিস / পার্সিং (M5): ইনপুট — টোকেন স্ট্রিম; আউটপুট — পার্স ট্রি / অ্যাবস্ট্রাক্ট সিনট্যাক্স ট্রি (AST)।
  • ৩. সিমান্টিক অ্যানালাইসিস (M6): ইনপুট — AST; আউটপুট — অ্যানোটেটেড/ডেকোরেটেড AST (সিম্বল টেবিল পপুলেটেড, টাইপ চেক করা, স্কোপ রেজলভড)।
  • ৪. ইন্টারমিডিয়েট কোড জেনারেশন (M12/L51): ইনপুট — অ্যানোটেটেড AST; আউটপুট — একটি ইন্টারমিডিয়েট রিপ্রেজেন্টেশন (IR), সোর্স ভাষার চেয়ে সরল ও ইউনিফর্ম কিন্তু এখনও তুলনামূলক হাই-লেভেল/ মেশিন-স্বাধীন।
  • ৫. কোড অপ্টিমাইজেশন (M12/L53): ইনপুট — IR; আউটপুট — উন্নত IR (সিমান্টিক্যালি সমতুল্য কিন্তু বেশি এফিশিয়েন্ট)।
  • ৬. টার্গেট কোড জেনারেশন (M12/L52): ইনপুট — অপ্টিমাইজড IR; আউটপুট — একটি নির্দিষ্ট টার্গেট মেশিনের জন্য প্রকৃত মেশিন কোড বা অ্যাসেম্বলি (সরাসরি সংযোগ Computer Architecture কোর্সের ISA বিষয়বস্তুর সাথে)।

৩ · ফ্রন্ট-এন্ড বনাম ব্যাক-এন্ড

ফেজ ১-৩ কে বলা হয় ফ্রন্ট-এন্ডকম্পাইলারের ভাষা-নির্ভর অংশ — টার্গেট মেশিন থেকে সম্পূর্ণ স্বাধীন; একই ফ্রন্ট-এন্ড তাত্ত্বিকভাবে একাধিক টার্গেট মেশিন সাপোর্ট করতে পারে। — এটি ভাষা-নির্দিষ্ট, টার্গেট মেশিন থেকে স্বাধীন। ফেজ ৪-৬ কে বলা হয় ব্যাক-এন্ডকম্পাইলারের টার্গেট-মেশিন-নির্দিষ্ট অংশ — সোর্স ভাষার সিনট্যাক্স থেকে সম্পূর্ণ স্বাধীন; একই ব্যাক-এন্ড তাত্ত্বিকভাবে একাধিক সোর্স ভাষা সাপোর্ট করতে পারে। — এটি টার্গেট-মেশিন-নির্দিষ্ট, সোর্স ভাষার সিনট্যাক্স থেকে স্বাধীন। এই ফ্রন্ট-এন্ড/ব্যাক-এন্ড বিভাজনের কারণেই বাস্তব কম্পাইলার ইনফ্রাস্ট্রাকচার (যেমন LLVM, শুধু নাম হিসেবে উল্লেখ) একাধিক সোর্স ভাষা ও একাধিক টার্গেট মেশিন সমর্থন করতে পারে — নতুন একটি ভাষা যোগ করতে শুধু একটি নতুন ফ্রন্ট-এন্ড লিখলেই চলে, বিদ্যমান ব্যাক-এন্ডগুলো পুনর্ব্যবহার করা যায়, এবং উল্টোভাবেও।

মূল কথা · Key takeaway

একটি কম্পাইলার আসলে দুইটা স্বতন্ত্র কাজের সমষ্টি — "ভাষাটা বুঝা" (ফ্রন্ট-এন্ড) আর "মেশিনের জন্য কোড তৈরি করা" (ব্যাক-এন্ড) — এই দুটোর মাঝে সেতু হলো ইন্টারমিডিয়েট রিপ্রেজেন্টেশন (IR), যা কোনো নির্দিষ্ট সোর্স ভাষা বা টার্গেট মেশিনের প্রতি পক্ষপাতী নয়।

৪ · কোডে সম্পূর্ণ পাইপলাইন চেইন করা

নিচের কোড সেলে একটি CompilerPipeline ক্লাস ছয়টি ফেজকেই চেইন করে চালায় — প্রতিটি ফেজ একটি সরল ডেটা স্ট্রাকচারকে (স্ট্রিং → টোকেন লিস্ট → নেস্টেড টাপল ট্রি → অ্যানোটেটেড টাপল → ইনস্ট্রাকশন লিস্ট → ছোট অপ্টিমাইজড লিস্ট → চূড়ান্ত মেশিন-কোড স্ট্রিং) রূপান্তর করে ও লগ করে।

Python
# একটি টয় ইনপুট, ছয়টি ফেজের মধ্য দিয়ে ধাপে ধাপে চালানো হচ্ছে
class CompilerPipeline:
    def __init__(self):
        self.log = []

    def lexical_analysis(self, source):
        tokens = source.split()  # সরল -- স্পেস দিয়ে ভাগ (প্রকৃত লেক্সার M4-এ)
        self.log.append(("১. লেক্সিক্যাল অ্যানালাইসিস", tokens))
        return tokens

    def syntax_analysis(self, tokens):
        # ধরে নিচ্ছি ইনপুট সবসময় "operand op operand" প্যাটার্নের -- একটি নেস্টেড টাপল "ট্রি"
        tree = (tokens[1], tokens[0], tokens[2])
        self.log.append(("২. পার্সিং (সিনট্যাক্স অ্যানালাইসিস)", tree))
        return tree

    def semantic_analysis(self, tree):
        op, left, right = tree
        annotated = (op, left, "int", right, "int")  # টাইপ অ্যানোটেশন যোগ হলো
        self.log.append(("৩. সিমান্টিক অ্যানালাইসিস", annotated))
        return annotated

    def intermediate_code_generation(self, annotated):
        op, left, ltype, right, rtype = annotated
        ir = [f"LOAD {left}", f"LOAD {right}", f"{op.upper()} "]
        self.log.append(("৪. ইন্টারমিডিয়েট কোড জেনারেশন", ir))
        return ir

    def code_optimization(self, ir):
        # টয় অপ্টিমাইজেশন -- LOAD+LOAD+OP তিনটি ইনস্ট্রাকশনকে একটি COMPUTE-এ মার্জ করা হলো
        operand_a = ir[0].split()[1]
        operand_b = ir[1].split()[1]
        operator = ir[2].strip()
        optimized = [f"COMPUTE {operand_a} {operator} {operand_b}"]
        self.log.append(("৫. কোড অপ্টিমাইজেশন", optimized))
        return optimized

    def target_code_generation(self, optimized):
        machine_code = f"MOV R1, {optimized[0]}"
        self.log.append(("৬. টার্গেট কোড জেনারেশন", machine_code))
        return machine_code

    def run(self, source):
        tokens = self.lexical_analysis(source)
        tree = self.syntax_analysis(tokens)
        annotated = self.semantic_analysis(tree)
        ir = self.intermediate_code_generation(annotated)
        optimized = self.code_optimization(ir)
        machine_code = self.target_code_generation(optimized)
        return machine_code

pipeline = CompilerPipeline()
source = "a + b"
print(f"সোর্স ইনপুট: {source!r}\n")
result = pipeline.run(source)

for phase_name, output in pipeline.log:
    print(f"{phase_name}:")
    print(f"  {output}\n")

print(f"চূড়ান্ত আউটপুট (টার্গেট মেশিন কোড): {result}")

    
লক্ষ্য করুন প্রতিটি ফেজ শুধু আগের ফেজের আউটপুট-কে ইনপুট হিসেবে নেয় — কোনো ফেজই মূল সোর্স স্ট্রিং-এ সরাসরি ফিরে তাকায় না। এটিই ফেজ-ভিত্তিক পাইপলাইনের মূল সুবিধা: প্রতিটি ধাপ শুধু তার ঠিক আগের ধাপের সুনির্দিষ্ট, সুগঠিত আউটপুটের সাথে ডিল করে।

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

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

প্র ০১ ফ্রন্ট-এন্ড ও ব্যাক-এন্ড আলাদা করে ডিজাইন করার সুবিধা কী — LLVM-এর মতো ইনফ্রাস্ট্রাকচারের উদাহরণ দিয়ে ব্যাখ্যা করুন।

যদি ফ্রন্ট-এন্ড (ভাষা বোঝা) ও ব্যাক-এন্ড (মেশিন কোড তৈরি করা) একে অপরের সাথে জড়িয়ে থাকত, তাহলে N-টি ভাষা ও M-টি টার্গেট মেশিন সাপোর্ট করতে N×M আলাদা কম্পাইলার লিখতে হতো। কিন্তু IR-কে "সাধারণ ভাষা" হিসেবে ব্যবহার করে আলাদা রাখলে, শুধু N-টি ফ্রন্ট-এন্ড (প্রতিটি ভাষা → IR) ও M-টি ব্যাক-এন্ড (IR → প্রতিটি মেশিন) লিখলেই চলে — N+M কাজ, N×M নয়। LLVM ঠিক এই স্থাপত্যে তৈরি, যা এটিকে অনেক ভাষা (C, Rust, Swift, ইত্যাদি) ও অনেক টার্গেট মেশিন একসাথে সাপোর্ট করার ক্ষমতা দেয়।

প্র ০২ সিমান্টিক অ্যানালাইসিসের ইনপুট ও আউটপুট কী, এবং কেন এটি পার্সিংয়ের আগে না চলে পরে চলে?

সিমান্টিক অ্যানালাইসিসের ইনপুট হলো পার্স ট্রি/AST (পার্সিংয়ের আউটপুট), আর আউটপুট হলো একটি অ্যানোটেটেড AST (টাইপ চেক করা, সিম্বল টেবিল পপুলেটেড)। এটি পার্সিংয়ের পরে চলতে হয় কারণ টাইপ চেক করা বা স্কোপ রেজলভ করার জন্য একটি প্রোগ্রামের গঠন (কোনটা কোন এক্সপ্রেশনের অংশ, কোনটা কোন স্টেটমেন্টের ভেতরে) আগে থেকেই জানা দরকার — আর সেই গঠনগত তথ্যই পার্স ট্রি ধারণ করে। L02-এর ভাষায় বললে: সিমান্টিক্স (অর্থ) চেক করার আগে সিনট্যাক্স (গঠন) নিশ্চিত হতে হয়।

প্র ০৩ উপরের কোড সেলে pipeline.run("a + b") চালালে চূড়ান্ত আউটপুট কী হয়, এবং কেন প্রতিটি ফেজ শুধুই তার ঠিক-আগের ফেজের আউটপুটের উপর নির্ভর করে?

ধাপে ধাপে ট্রেস করলে: tokens = ['a', '+', 'b'] → tree = ('+', 'a', 'b') → annotated = ('+', 'a', 'int', 'b', 'int') → ir = ['LOAD a', 'LOAD b', '+ '] → optimized = ['COMPUTE a + b'] → চূড়ান্ত machine_code = 'MOV R1, COMPUTE a + b'। প্রতিটি মেথড (lexical_analysis, syntax_analysis, ইত্যাদি) তার প্যারামিটার হিসেবে শুধু আগের মেথডের রিটার্ন-ভ্যালু নেয় — কখনও মূল source স্ট্রিং-এ সরাসরি ফিরে তাকায় না। এটিই "ফেজ-ভিত্তিক" ডিজাইনের সংজ্ঞা: প্রতিটি ধাপ একটি সুনির্দিষ্ট, সংকীর্ণ ইনপুট/আউটপুট কন্ট্র্যাক্ট মেনে চলে।

অনুশীলন

  1. চিন্তা করুন: L02-এর SyntaxError ও TypeError এই ৬-ফেজ মডেলে কোন ফেজে ধরা পড়ে? প্রতিটির জন্য ফেজ নম্বর ও নাম লিখুন।

    SyntaxError ধরা পড়ে ফেজ ২-এ (পার্সিং/সিনট্যাক্স অ্যানালাইসিস) — টোকেন স্ট্রিম থেকে বৈধ পার্স ট্রি তৈরি করা যাচ্ছে না। TypeError ধরা পড়ে ফেজ ৩-এ (সিমান্টিক অ্যানালাইসিস) — পার্স ট্রি তৈরি হয়ে গেছে (গঠন ঠিক ছিল), কিন্তু টাইপ-চেকিং ধাপে অসামঞ্জস্য পাওয়া গেছে। এটিই সরাসরি নিশ্চিত করে কেন ফেজ ২ সবসময় ফেজ ৩-এর আগে চলে।

  2. পরীক্ষা করুন: উপরের কোড সেলে source-এর মান "x * y" করে Run চেপে দেখুন প্রতিটি ফেজের আউটপুট কীভাবে বদলায়।

    নতুন ট্রেস: tokens = ['x', '*', 'y'] → tree = ('*', 'x', 'y') → annotated = ('*', 'x', 'int', 'y', 'int') → ir = ['LOAD x', 'LOAD y', '* '] → optimized = ['COMPUTE x * y'] → চূড়ান্ত 'MOV R1, COMPUTE x * y'। শুধু অপারেটর ও অপারেন্ড বদলেছে — পাইপলাইনের গঠন ও ধাপের ক্রম অপরিবর্তিত।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ প্যারাডাইম, গ্রামার, লেক্সিং, পার্সিং, টাইপ সিস্টেম ও কোড জেনারেশন পর্যন্ত সম্পূর্ণ যাত্রা।
  • আগের পাঠ L04 ল্যাঙ্গুয়েজ ডিজাইন গোল ও ট্রেড-অফ — readability, writability, reliability ও efficiency।
  • পরের পাঠ L06 ইম্পারেটিভ প্রোগ্রামিং প্যারাডাইম — M2 মডিউলের শুরু, ভাষা-ডিজাইনের প্রথম বড় সিদ্ধান্ত।
  • Computer Architecture & Digital Logic কোর্স সহোদর কোর্স এই পাঠের ফেজ ৬ (টার্গেট কোড জেনারেশন) ঠিক সেই ISA/CPU আর্কিটেকচারকেই টার্গেট করে যা এই কোর্সে বিস্তারিত কভার হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture ও Programming Languages & Compiler Design — সব এক জায়গায়।
আগের পাঠ
ল্যাঙ্গুয়েজ ডিজাইন গোল ও ট্রেড-অফ