Compiler Design chapter 1(anser by gemini)

بخش ۱: استراتژی‌های اجرای زبان‌های برنامه‌نویسی (مدل‌های معماری)

در دنیای کامپیوتر، کد سطح بالایی که ما می‌نویسیم برای سخت‌افزار قابل فهم نیست. سه استراتژی اصلی برای ترجمه و اجرای برنامه‌ها وجود دارد:

۱. کامپایلرها (Compilers)

  • تعریف: کامپایلر یک پردازش اولیه (Preprocessing) گسترده روی کد انجام می‌دهد و کل کد منبع (Source Code) را یک‌جا به کد ماشین (باینری یا همان فایل‌های قابل اجرا مثل .exe یا .bin) ترجمه می‌کند. پس از کامپایل، برنامه برای اجرا دیگر نیازی به کد اصلی یا خود کامپایلر ندارد.

  • ویژگی‌ها: سرعت اجرای بسیار بالا، تشخیص خطاها قبل از اجرا (در زمان کامپایل). اما برای هر سیستم‌عامل (ویندوز، لینوکس، مک) باید مجدداً کامپایل شود.

  • زبان‌های معروف: C, C++, Rust, Go.

  • مثال ملموس: فرض کن یک کتاب انگلیسی را به طور کامل به فارسی ترجمه کنی و به صورت یک کتاب جدید چاپ کنی. حالا خواننده بدون نیاز به مترجم یا کتاب اصلی، می‌تواند سریعاً آن را بخواند.

۲. مفسرها (Interpreters)

  • تعریف: مفسر کدها را بدون پردازش اولیه بزرگ، خط به خط در زمان اجرا (Runtime) می‌خواند، ترجمه می‌کند و فوراً اجرا می‌کند. هیچ فایل خروجی مستقلی تولید نمی‌شود و برای اجرای برنامه، حضور مفسر روی سیستم همیشه الزامی است.

  • ویژگی‌ها: توسعه و تست سریع (چون زمان کامپایل نداریم) ، قابلیت اجرای آسان روی پلتفرم‌های مختلف. اما سرعت اجرای آن به دلیل تکرار فرآیند ترجمه در زمان اجرا کمتر است.

  • زبان‌های معروف: Python, PHP, Ruby, JavaScript.

  • مثال ملموس: فرض کن یک سخنران انگلیسی در حال سخنرانی است و یک مترجم همزمان، جمله به جمله صحبت‌های او را به فارسی ترجمه می‌کند. سرعت این کار قطعاً از خواندن یک کتابِ از قبل ترجمه شده کمتر است.

۳. مدل‌های ترکیبی / جاست این تایم (Hybrid / JIT / Bytecode)

  • تعریف: این زبان‌ها ابتدا کد منبع را به یک فرمت میانی به نام بایت‌کد (Bytecode) کامپایل می‌کنند. سپس یک ماشین مجازی (VM) در زمان اجرا، این بایت‌کد را تفسیر می‌کند یا با استفاده از تکنولوژی JIT (Just-In-Time) بخش‌های پرتکرار کد را دقیقاً در همان لحظه اجرا به کد ماشین تبدیل می‌کند تا سرعت بالا رود.

  • زبان‌های معروف: Java (اجرا روی JVM)، C# (اجرا روی CLR).

  • مثال ملموس: فرض کن کتاب انگلیسی ابتدا به یک زبان میانی ساده (مثل اسپرانتو) ترجمه شود و سپس افراد در کشورهای مختلف با ماشین‌های ترجمه مخصوص خودشان آن را به زبان بومی خود تبدیل کنند.


بخش ۲: تاریخچه و علت پیدایش کامپایلرها

  • سال ۱۹۵۴: شرکت IBM کامپیوتر مدل 704 را معرفی کرد. در آن زمان یک مشکل بزرگ وجود داشت: هزینه نوشتن نرم‌افزار از هزینه خرید سخت‌افزار بیشتر شده بود! زیرا تمام برنامه‌ها به زبان اسمبلی (Assembly) نوشته می‌شدند که بسیار سخت و زمان‌بر بود.

  • راهکار اول (Speedcoding): توسط جان باکوس در سال ۱۹۵۳ ابداع شد. نوشتن کد را راحت‌تر کرد اما ۳۰٪ از حافظه بسیار محدود ماشین‌های آن زمان را اشغال می‌کرد.

  • انقلاب FORTRAN 1: در سال ۱۹۵۷ اولین نسخه زبان فورترن منتشر شد. این زبان دارای نحو شبیه به ریاضی و سطح بالا بود و برای اولین بار فرآیند کامپایل اتوماتیک را معرفی کرد که سرعت برنامه‌نویسی را به شدت بالا برد.


بخش ۳: ساختار و فازهای کامپایلر (Structure of Compilers)

یک کامپایلر فرآیند ترجمه را به دو بخش کلی Front-End (تحلیل کد) و Back-End (تولید کد) تقسیم می‌کند. به طور دقیق‌تر، این فرآیند شامل ۵ فاز اصلی است:

فاز ۱: تحلیل لغوی (Lexical Analysis یا Scanning)

  • وظیفه: کد ورودی را که به صورت یک رشته از کاراکترهاست می‌خواند، فضاهای خالی (Whitespace) و کامنت‌ها را حذف می‌کند و کاراکترها را به واحدهای معناداری به نام توکن (Token) تبدیل می‌کند.

  • انواع توکن‌ها:

    1. Keywords (کلمات کلیدی): کلمات رزرو شده مثل if, while, int.

    2. Identifiers (شناسه‌ها): نام متغیرها و توابع مثل count یا sum.

    3. Literals (مقادیر ثابت): اعداد یا رشته‌ها مثل 42 یا "Hello".

    4. Operators (عملگرها): مثل +, =, <, ++.

    5. Separators (جداکننده‌ها): مثل ;, ,, {}.

  • مثال: کد int x = 5; تبدیل می‌شود به توکن‌های زیر: [KEYWORD: int], [IDENTIFIER: x], [OPERATOR: =], [LITERAL: 5], [SEPARATOR: ;]

فاز ۲: تحلیل نحو (Parsing یا Syntax Analysis)

  • وظیفه: توکن‌های تولید شده در فاز قبل را می‌گیرد و بررسی می‌کند که آیا چیدمان آن‌ها بر اساس قوانین گرامری زبان درست است یا خیر. خروجی این فاز یک ساختار درختی به نام درخت تجزیه (Parse Tree) یا درخت نحو انتزاعی (AST) است. این فاز خطاهای نحوی (مثل جا افتادن سمیکالن یا پرانتز) را کشف می‌کند.

  • مثال ملموس در زبان انسان: جمله "علی سیب خورد" از نظر نحوی درست است. اما جمله "سیب علی خورد" ساختار نحوی غلطی دارد.

  • مثال در کد: برای قطعه کد محاسبه فاکتوریل، پارسر ساختاری درختی ایجاد می‌کند که عملگرها و شروط (مانند if n == 0) را به صورت شاخ و برگ‌های یک درخت مدل‌سازی می‌کند تا اولویت اجرا (مثل ضرب قبل از جمع) مشخص شود.

فاز ۳: تحلیل معنایی (Semantic Analysis)

  • وظیفه: فراتر از ظاهر و ساختار کد، به معنا و منطق آن نگاه می‌کند. وظایف اصلی آن عبارتند از: کنترل نوع‌ها (Type Checking)، بررسی اعلان متغیرها قبل از استفاده و بررسی قوانین حوزه متغیرها (Scope Rules).

  • مثال ملموس در زبان انسان: جمله "صندلی به من آب داد" از نظر نحوی (نهاد + مفعول + فعل) کاملاً درست است، اما از نظر معنایی بی‌معنی است (چون صندلی بی‌جان است و نمی‌تواند آب بدهد).

  • مثال در کد: * اگر بنویسی int x = "Hello"; پارسر خطایی نمی‌گیرد (چون ساختار متغیر=مقدار درست است)، اما تحلیل‌گر معنایی خطای عدم تطابق نوع (Type Mismatch) می‌دهد.

  • مثال حوزه (Scope): در کدی مثل شکل صفحه ۳۵ جزوه، اگر یک متغیر int i = 3; در بلوک بیرونی و int i = 4; در بلوک داخلی تعریف شود، تحلیل‌گر معنایی تشخیص می‌دهد که دستور cout << i درون بلوک داخلی باید مقدار 4 را چاپ کند.

فاز ۴: بهینه‌سازی (Optimization)

  • وظیفه: کد میانی ایجاد شده را بدون تغییر دادن نتیجه نهایی برنامه، ویرایش و بازنویسی می‌کند تا سریع‌تر اجرا شود، حافظه کمتری مصرف کند یا حجم کمتری داشته باشد.

  • مثال: فرض کن در کد نوشته‌ای: x = y * 0;. کامپایلر در فاز بهینه‌سازی می‌فهمد که هر چیزی ضربدر صفر شود حاصلش صفر است، پس کل این عبارت را خط می‌زند و آن را به x = 0; تبدیل می‌کند تا محاسبات ضرب در زمان اجرا حذف شود.

فاز ۵: تولید کد (Code Generation)

  • وظیفه: در آخرین مرحله، کد بهینه‌شده به زبان مقصد که معمولاً زبان اسمبلی (Assembly) یا کد ماشین پلتفرم هدف است، ترجمه می‌شود. در این فاز، تخصیص ثبات‌ها (Registers) و انتخاب دستورالعمل‌های پردازنده انجام می‌شود.

بخش ۴: مفهوم بازنمایی میانی (Intermediate Representation - IR)

کامپایلرهای مدرن مستقیماً کد منبع را به کد ماشین تبدیل نمی‌کنند. آن‌ها کد را به یک یا چند زبان میانی داخلی (Internal) که به آن IR می‌گویند تبدیل می‌کنند.

  • چرا IR مهم است؟ اگر ما بخواهیم برای $N$ زبان برنامه‌نویسی مختلف (مثل پایتون، سی، جاوا) برای $M$ نوع سخت‌افزار مختلف (مثل اینتل، ARM، مک مینو) کامپایلر بسازیم، بدون IR نیاز به $N \times M$ کامپایلر داریم. اما با وجود IR، بخش Front-end هر زبان کد را به یک IR مشترک تبدیل می‌کند و بخش Back-end فقط کافی است IR را به سخت‌افزار هدف تبدیل کند (نیاز به $N + M$ ساختار).

  • سطوح بالایی IR به زبان برنامه‌نویسی نزدیک‌ترند و مفاهیمی مثل حلقه‌ها و کلاس‌ها را حفظ می‌کنند، اما سطوح پایین‌تر IR به سخت‌افزار نزدیک‌تر شده و ثبات‌ها و چیدمان حافظه را عریان می‌کنند.


پاسخ به کوئیز آخر فصل (صفحه ۴۱)

بیا با هم به سوالات این کوئیز بر اساس درس پاسخ دهیم تا مطمئن شویم متوجه شده‌ای:

  1. کدام فاز کامپایلر مسئول تشخیص خطای "متغیر تعریف نشده" (Undeclared Variable) است؟

    • پاسخ: فاز تحلیل معنایی (Semantic Analysis)؛ زیرا این فاز وظیفه دارد بررسی کند آیا شناسه‌ها قبل از استفاده، اعلام (Declare) شده‌اند یا خیر.
  2. اگر تابعی تعریف شده باشد که ۳ پارامتر می‌پذیرد اما با ۵ آرگومان فراخوانی شود، کدام فاز این عدم تطابق تعداد پارامتر (Arity Mismatch) را تشخیص می‌دهد؟

    • پاسخ: فاز تحلیل معنایی (Semantic Analysis)؛ چون این فاز بررسیِ منطق، امضای توابع و سازگاری فراخوانی‌ها را برعهده دارد.
  3. کدام فاز کامپایلر مسئول حل قوانین حوزه (Scope Rules) است؟ (مثلاً تشخیص اینکه نام یک متغیر به کدام اعلان در کد اشاره دارد)

    • پاسخ: فاز تحلیل معنایی (Semantic Analysis)؛ این فاز با ساخت جدول نمادها (Symbol Table)، متغیرهای درون بلوک‌های مختلف را تفکیک و متصل می‌کند.

مطالبی که مطالعه کردید تمام توسط هوش مصنوعی Google Gemini ترجمه و تهیه شده. در صورت نیاز به توضیح بیشتر قسمتی یا اصلاح قسمتی به بنده پیام بدید.

saleh askari
saleh askari

خیلی ممنونم بابت مطالعه این وبلاگ