# یک فایل متنی چطور به «برنامه» تبدیل میشود؟
5040
let، اولویت * نسبت به + و رفتار حلقهٔ while را ما تعریف میکنیم. اگر برنامهای بتواند این قواعد را بخواند و اجرا کند، صاحب یک زبان برنامهنویسی شدهایم؛ هرچند زبانمان هنوز کوچک باشد.
# مفسر میسازیم یا کامپایلر؟
ساختار برنامه را میخواند و معنای آن را اجرا میکند.
برنامه را به نمایش دیگری مانند کد ماشین یا IR تبدیل میکند.
# نقشهٔ راه زبان ما
.mini# اول قرارداد زبان را مشخص کنیم
true و falselet و تغییر مقدار آنif/else و حلقهٔ whileprint; تمام میشود. بدنهٔ شرط و حلقه داخل { } قرار میگیرد. شرطها به پرانتز اجباری نیاز ندارند. متغیرها در این نسخه همگی در یک محیط سراسری نگهداری میشوند؛ یعنی Block Scope نداریم.
# مرحله اول — Lexer: متن را به Token تبدیل کنیم
10 فقط متن نیست؛ Lexer مقدار عددی آن را هم ذخیره میکند. همچنین let ابتدا شبیه یک Identifier خوانده میشود و بعد جدول Keywordها مشخص میکند که یک کلمهٔ رزروشده است.
minilang.py است. برای کوتاه و قابلفهم ماندن پروژه، شناسهها را با حرف انگلیسی یا _ آغاز میکنیم، کامنتها با // شروع میشوند و شمارهٔ خط هر Token را برای پیامهای خطا نگه میداریم:
= و == را با انتخاب الگوی بلندتر میفهمد. ترتیب الگوها مهم است: اگر = زودتر تطبیق داده میشد، عبارت == ممکن بود اشتباه به دو Token جدا تقسیم شود.
# مرحله دوم — Grammar: چه ترتیبی معتبر است؟
| یعنی «یکی از این حالتها»، علامت ? یعنی بخش اختیاری و * یعنی صفر یا چند بار تکرار. این * علامت ضرب زبان ما نیست؛ فقط بخشی از نشانهگذاری Grammar است.
factor عملگرهای ضرب و تقسیم را میخواند، در حالی که term جمع و تفریق را از Factorها میسازد. بنابراین در عبارت 2 + 3 * 4 ابتدا 3 * 4 یک زیرساختار میشود و بعد با ۲ جمع میشود. اولویت عملگرها نتیجهٔ یک if مخفی نیست؛ مستقیماً داخل Grammar نوشته شده است.
# مرحله سوم — AST: شکل برنامه را نگه داریم، نه شکل متن را
let result = 2 + 3 * 4; درخت مفهومی چنین سلسلهمراتبی دارد:
Node نمایش میدهیم. در یک کامپایلر بزرگ معمولاً کلاسهای جداگانهای مانند BinaryExpression، VariableDeclaration و WhileStatement دیده میشود؛ اما مدل عمومی برای پروژهٔ ما کافی است:
# مرحله چهارم — Parser: از Tokenها درخت بسازیم
statement نوع دستور را تشخیص میدهد و تابع block تا رسیدن به } دستورها را جمع میکند:
10 - 3 - 2 باید بهصورت (10 - 3) - 2 ساخته شود. حلقهٔ تابع term گره قبلی را هر بار فرزند چپ گره جدید میکند و همین موضوع چپهمبستگی عملگرها را میسازد.
# مرحله پنجم — Interpreter: به درخت معنا بدهیم
let x = 10; مقدار ۱۰ را با کلید x ذخیره میکند. هر بار که Node متغیر x ارزیابی شود، مفسر مقدار فعلی را از همان Dictionary میخواند. اگر نام وجود نداشته باشد، بهجای حدس زدن مقدار، خطای Runtime میدهیم.
false و عدد صفر نادرست هستند و true و عدد غیرصفر درست محسوب میشوند. عملگرهای حسابی فقط عدد میپذیرند؛ چون در Python مقدار Boolean زیرکلاس int است، عمداً Boolean را جداگانه رد میکنیم تا عبارتی مانند true + 1 در MiniLang بهشکل تصادفی معتبر نشود.
while شرط باید در هر تکرار دوباره ارزیابی شود. اگر مقدار شرط را فقط یک بار قبل از حلقه حساب میکردیم، تغییر متغیر n داخل بدنه هیچ اثری نداشت و حلقهٔ فاکتوریل هرگز تمام نمیشد.
# مرحله ششم — قطعات را به هم وصل کنیم
run همان Pipeline مقاله را در سه خط اجرا میکند: Tokenize، Parse و Interpret. بخش Command Line نیز فایل ورودی را با UTF-8 میخواند و خطاهای قابل انتظار را بدون نمایش یک Traceback طولانی گزارش میکند:
minilang.py قرار دهید و برنامهٔ ابتدای مقاله را در factorial.mini ذخیره کنید:
n و result را ایجاد کرده، شرط حلقه را چند بار سنجیده، ضرب و انتساب را انجام داده و در پایان شاخهٔ درست if را اجرا کرده است.
# اگر برنامه اشتباه باشد چه میشود؟
print @; — Lexer نمیتواند @ را Token کندlet x = 10 — علامت ; جا افتاده استprint 1 / 0; — ساختار معتبر است، اما اجرا ممکن نیست1 / 0 از نظر Grammar کاملاً معتبر است؛ مشکل فقط زمانی آشکار میشود که Interpreter بخواهد آن را اجرا کند. در مقابل، جا افتادن ; قبل از اجرا و در مرحله Parse تشخیص داده میشود.
# این زبان هنوز چه چیزهایی ندارد؟
Lexer، Grammar، Recursive-Descent Parser، AST، متغیر، محاسبه، شرط، حلقه، خروجی و خطا.
String، Function، Scope، Array، Module، Type Checker، Bytecode، Optimizer، Garbage Collector و Native Code.
if در بیرون آن هم باقی میماند. همچنین مرحلهٔ Semantic Analysis جداگانهای نداریم و خطاهایی مانند متغیر تعریفنشده هنگام اجرا شناسایی میشوند. اینها باگ پنهان نیستند؛ تصمیمهای سادهساز پروژهاند.
return است.
# چرا ساختن یک زبان کوچک ارزش دارد؟
while یعنی تکرار و نام متغیر به کجا اشاره میکند. اما هیچکدام طبیعی نیستند؛ همه قراردادهاییاند که سازندهٔ زبان تعریف و پیادهسازی کرده است.
while را نفهمید. ما برنامهای ساختیم که توافق کرد هنگام دیدن آن چه رفتاری انجام دهد. زبان برنامهنویسی در اصل قراردادی دقیق میان انسان و یک پیادهسازی است—قراردادی که اگر کوچکترین ابهامی داشته باشد، برنامه دیگر قابل پیشبینی نخواهد بود.
فقط برنامهای که ما نوشتیم، هنگام دیدنش توافق کرد چه کاری انجام دهد.
زبان برنامهنویسی یعنی همین:
قراردادی دقیق که به متن ساده، معنا میدهد.
