از کد منبع تا فایل اجرایی: کامپایلر زبان برنامه‌نویسی سلام مرحله به مرحله چگونه کار می‌کند

یک برنامه‌ی سلام را در مسیر کامپایلر دنبال کنید: توکن‌ها، درخت نحو، بررسی گونه‌ها و قاعده‌ها، کد C یا LLVM IR یا کد ماشینِ تولیدشده و پیوند نهایی، با خروجی واقعی هر مرحله.

در این مقاله
  1. نگاهی کلی به مراحل کامپایلر زبان برنامه‌نویسی سلام
  2. مرحله‌ی ۱: خواندن منبع و واژه‌کاوی
  3. مرحله‌ی ۲: تجزیه و ساختن درخت نحو
  4. مرحله‌ی ۳: تحلیل معنایی، جایی که بیشتر کار انجام می‌شود
  5. پیام‌های خطای کامپایلر زبان برنامه‌نویسی سلام چه شکلی دارند
  6. مرحله‌ی ۴: تولید کد با سه پشتوانه
  7. مرحله‌ی ۵: کامپایل، نهان‌سازی و پیوند
  8. توقف زودهنگام: salam check و salam inspect
  9. چرا زنجیره‌ی کامپایلر سلام این شکل را دارد
  10. پرسش‌های پرتکرار

کامپایلر برنامه‌ای است که کد منبع را می‌خواند، بررسی می‌کند که معنای درستی دارد و آن را به شکلی ترجمه می‌کند که ماشین بتواند اجرا کند. کامپایلر زبان برنامه‌نویسی سلام این کار را در پنج مرحله‌ی کلی انجام می‌دهد: متن را به توکن‌ها واژه‌کاوی می‌کند، توکن‌ها را به درخت نحو تجزیه می‌کند، درخت را از نظر گونه‌ها و اشتباه‌ها تحلیل می‌کند، از راه یکی از سه پشتوانه (Backend)، یعنی C، LLVM یا تولیدکننده‌ی کد بومی خودش برای x86-64، ARM و RISC-V، کد تولید می‌کند و در پایان نتیجه را به یک فایل اجرایی پیوند می‌دهد. این مقاله یک برنامه‌ی واقعی را در همه‌ی مرحله‌های کامپایلر سلام دنبال می‌کند و خروجی واقعی هر گام را نشان می‌دهد.

همه‌ی آنچه اینجا می‌بینید با فرمان salam قابل تکرار است. هر جا مقاله می‌گوید «کامپایلر فلان کار را می‌کند»، نام فایلی که این کار را انجام می‌دهد هم آمده تا بتوانید کد منبعش را روی گیت‌هاب بخوانید. برنامه‌های این مقاله فارسی‌اند و پیام‌های خطا هم همان‌هایی‌اند که کامپایلر برای کد فارسی چاپ می‌کند.

نگاهی کلی به مراحل کامپایلر زبان برنامه‌نویسی سلام

زنجیره‌ی کامپایل رشته‌ای از مرحله‌هاست که هر کدام یک بازنمایی از برنامه را به بازنمایی بعدی تبدیل می‌کند. کامپایلر سلام به خود زبان سلام نوشته شده است (نگاه کنید به مقاله‌ی کامپایلر خودمیزبان) و کد منبعش در پوشه‌ی compiler/ قرار دارد، با یک زیرپوشه برای هر مرحله. نقطه‌ی ورود، compiler/main.salam، کوتاه است: آرگومان‌های خط فرمان را می‌خواند و به cli.Run می‌سپارد. بقیه‌ی کارها از همین فراخوانی آغاز می‌شود.

مرحلهورودیخروجیپوشهاندازه (خط)
تحلیلگر واژگانی (Lexer)متن منبعتوکن‌هاcompiler/lexer/حدود ۲٬۸۰۰
تجزیه‌گر (Parser)توکن‌هادرخت نحو (AST)compiler/parser/حدود ۵٬۱۰۰
تحلیل معناییASTAST گونه‌دار و بررسی‌شدهcompiler/semantic/حدود ۴۰٬۷۰۰
پشتوانه‌ی CAST بررسی‌شدهکد منبع Ccompiler/codegen/حدود ۸٬۹۰۰
پشتوانه‌ی LLVMAST بررسی‌شدهLLVM IRcompiler/llvmgen/حدود ۱۱٬۹۰۰
پشتوانه‌ی بومیمتن LLVM IRکد ماشینcompiler/nativegen/حدود ۵۲٬۶۰۰
پیونددهنده‌ی داخلیفایل‌های شیءفایل اجراییcompiler/nativelink/حدود ۱۲٬۶۰۰
راننده (Driver)خط فرمانکل اجراcompiler/driver/حدود ۹٬۵۰۰

این عددها نشان می‌دهند کار اصلی کجاست. واژه‌کاوی و تجزیه بخش کوچک و شناخته‌شده‌ی ماجرا هستند. تحلیل معنایی هشت برابر تجزیه‌گر است، چون یک زبان سخت‌گیر سخت‌گیری‌اش را همین‌جا به دست می‌آورد. پشتوانه‌ی بومی بزرگ‌ترین بخش است، چون یک بهینه‌ساز کامل دارد و برای پنج خانواده‌ی پردازنده کد ماشین می‌نویسد. اگر مفسر، پشتوانه‌ی جاوااسکریپت، قالب‌بند، کارساز زبان (LSP) و زبان صفحه را هم بشماریم، کامپایلر حدود ۳۴۰ فایل منبع و نزدیک به ۲۰۰٬۰۰۰ خط است.

این برنامه را در طول مسیر دنبال می‌کنیم:

سلام
تابع مربع(ن: صحیح): صحیح:
    برگشت ن * ن
پایان

تابع ریشه:
    سرچاپ مربع(۷)
پایان
خروجی49

مرحله‌ی ۱: خواندن منبع و واژه‌کاوی

پیش از آنکه تحلیلگر واژگانی کارش را شروع کند، راننده (compiler/driver/driver_front.salam) فایل را بار می‌کند و تصمیم می‌گیرد فایل با کدام بسته‌ی زبانی نوشته شده است. کد سلام می‌تواند انگلیسی یا فارسی باشد و کامپایلر از روی واژه‌های کلیدی‌ای که می‌بیند زبان را تشخیص می‌دهد، مگر آنکه فایل در یک توضیح سرآیند زبانش را اعلام کند. بسته‌ی زبانی تعیین می‌کند کدام واژه‌ها کلیدی‌اند: func در یک فایل و تابع در فایلی دیگر. از این نقطه به بعد هر دو زبان توکن‌های یکسانی تولید می‌کنند، و برای همین بقیه‌ی کامپایلر هیچ‌وقت لازم ندارد بداند شما به کدام زبان نوشته‌اید. مقاله‌ی یک زبان، دو خط جنبه‌ی فارسی این ماجرا را عمیق‌تر بررسی می‌کند.

راننده جدول پرچم‌های کامپایل شرطی را هم برای مقصد می‌سازد: SALAM_OS_LINUX، SALAM_OS_WINDOWS، SALAM_ARCH_ARM64 و مانند این‌ها، به‌علاوه‌ی هر چه با -D بدهید. در کد فارسی همین ثابت‌ها نام فارسی دارند (مثلاً سلام سیستم لینوکس). این جدول درست پس از تجزیه به کار می‌آید.

تحلیلگر واژگانی (compiler/lexer/) سپس یک بار متن را از اول تا آخر می‌پیماید و آن را به توکن‌ها می‌بُرد: واژه‌های کلیدی، شناسه‌ها، لیترال‌ها، عملگرها و پایان دستورها. عددها، رشته‌ها (از جمله رشته‌های خام با بک‌تیک و بلوک‌های متنی چندخطی سلام)، نویسه‌های گریز و شناسه‌ها هر کدام فایل جداگانه‌ای در آن پوشه دارند. جریان توکن‌های هر فایل را با salam inspect می‌بینید:

ترمینال
salam inspect app.salam --emit-tokens

برای یک ریشهی سه‌خطی که ایکس := ۱ + ۲ را تعریف و ایکس را چاپ می‌کند، جریان توکن‌ها پانزده توکن است؛ اینجا فقط گونه‌ی آن‌ها آمده:

متن
KEYWORD_FUNC IDENTIFIER COLON IDENTIFIER COLON_ASSIGN INT_LITERAL PLUS
INT_LITERAL STMT_END KEYWORD_PRINTLN IDENTIFIER STMT_END KEYWORD_END
STMT_END EOF

این دقیقاً همان دنباله‌ای است که نسخه‌ی انگلیسی همین برنامه تولید می‌کند. تفاوت فقط در متن اصلی هر توکن است که در XML نگه داشته می‌شود: توکن نخست گونه‌ی KEYWORD_FUNC و متن تابع دارد، و لیترال‌ها متن ۱ و ۲ را با رقم فارسی نگه می‌دارند. هر توکن در XML سطر و ستون خودش را هم ثبت می‌کند و به همین دلیل هر پیام خطایی در مرحله‌های بعد می‌تواند دقیقاً به نویسه‌ی مقصر اشاره کند.

مرحله‌ی ۲: تجزیه و ساختن درخت نحو

تجزیه‌گر فهرست تخت توکن‌ها را به درختی تبدیل می‌کند که تودرتویی برنامه را نشان می‌دهد: این فراخوانی آرگومانِ آن سرچاپ است، این بلوک مال آن تابع است. تجزیه‌گر سلام (compiler/parser/) یک تجزیه‌گر نزولی بازگشتی دست‌نویس است. دستورها و تعریف‌ها هر کدام تابع خودشان را دارند و عبارت‌ها با روش Pratt تجزیه می‌شوند: هر عملگر یک توان چسبندگی چپ و راست دارد (+ و - با ۶۰، *، / و % با ۷۰، مقایسه‌ها با ۵۰، و با ۳۰، یا با ۲۰) و یک حلقه در parse_expr_bp برای هر ترکیبی از عملگرها درخت درست را می‌سازد. اولویت عملگرها در یک جدول کوچک نگه داشته می‌شود، نه در دوجین قاعده‌ی دستوری.

نتیجه یک درخت نحو انتزاعی (AST) است. AST سلام حدود ۷۰ گونه گره دارد (FunctionDef، VariableDecl، Call، Literal و مانند این‌ها). گره‌ها در صفحه‌های بزرگ ذخیره می‌شوند و با اندیس عددی به هم ارجاع می‌دهند، نه با اشاره‌گر. این کار درخت را فشرده و پیمایشش را ارزان نگه می‌دارد؛ جزئیاتی مهم برای کامپایلری که هنگام ساختن خودش باید تمام خودش را در حافظه نگه دارد.

درست پس از تجزیه، راننده کد شرطی را هرس می‌کند. یک بلوک سطح بالا مانند اگر سلام سیستم ویندوز: (در انگلیسی if SALAM_OS_WINDOWS:) با جدول پرچم‌های مرحله‌ی ۱ ارزیابی می‌شود و شاخه‌هایی که به کار نمی‌آیند پیش از آنکه کسی گونه‌هایشان را بررسی کند حذف می‌شوند. به همین شکل یک فایل منبع می‌تواند نسخه‌ی ویندوز و لینوکس یک تابع بیرونی را بدون تداخل تعریف کند. درس زمان کامپایل نحوه‌ی استفاده از آن را نشان می‌دهد.

وقتی یک بسته در چند فایل داخل یک پوشه پخش شده باشد، آن فایل‌ها هم واژه‌کاوی و تجزیه می‌شوند و درخت‌هایشان پیش از تحلیل در یک برنامه ادغام می‌شود.

مرحله‌ی ۳: تحلیل معنایی، جایی که بیشتر کار انجام می‌شود

تحلیل معنایی مرحله‌ای است که تصمیم می‌گیرد آیا برنامه‌ای که از نظر نحوی درست است واقعاً معنا دارد یا نه. این بزرگ‌ترین بخش کامپایلر سلام است و در ۷۱ فایل در compiler/semantic/ پخش شده. نقطه‌ی ورود، تابع Run در sema_api.salam، این گام‌ها را به ترتیب طی می‌کند:

  1. بار کردن واردسازی‌ها به‌صورت بازگشتی، و بار کردن بسته‌ی core که هر برنامه‌ای بی‌آنکه بخواهد دریافت می‌کند (چیزهایی مانند Error و Option در آن است).
  2. گردآوری تعریف‌ها: هر ساختار، جداشمار، میانجی، تابع و ثابت پیش از بررسی هر بدنه‌ای در یک حوزه ثبت می‌شود، پس تابع‌ها می‌توانند با هر ترتیبی یکدیگر را فراخوانی کنند.
  3. بررسی ترتیب سطح بالا: اول واردسازی‌ها، بعد ثابت‌ها، بعد گونه‌ها، بعد تابع‌ها؛ قاعده‌ای که سلام برای خوانا ماندن فایل‌ها اجبار می‌کند.
  4. بررسی تک‌تک تابع‌ها: پیدا کردن نام‌ها، استنتاج گونه‌ی هر عبارت، بررسی هر فراخوانی در برابر امضای تابع، نمونه‌سازی جنریک‌ها، بررسی کامل بودن همخوان، ردگیری مقداردهی قطعی، و اجرای تحلیل بازه‌ی مقدارها و تحلیل مالکیت که در مقاله‌ی گونه‌بندی ایستا و مقاله‌ی مدیریت حافظه شرح داده شده‌اند.
  5. گزارش تابع‌ها و واردسازی‌های بی‌استفاده، که در سلام خطا هستند، نه هشدار.

تحلیل، درخت را بازنویسی هم می‌کند. نتیجه را با --emit-ast می‌بینید که درخت آراسته را همراه با گونه‌ها چاپ می‌کند. برای ایکس := ۱ + ۲ تعریف این شکل را دارد (عنصرهای مکان حذف شده‌اند):

متن
<node kind="VariableDecl" name="ایکس" type="i32" mut="false">
  <init>
    <node kind="Literal" type="i32" op="INT_LITERAL">
      <value kind="int">3</value>
    </node>
  </init>
</node>

سه اتفاق افتاده است. گونه‌ی ایکس به‌صورت i32 (همان صحیح سلام) استنتاج شده. تغییرپذیری‌اش false ثبت شده، پس هر مقداردهی بعدی به ایکس خطاست. و ۱ + ۲ پیش از آنکه هیچ پشتوانه‌ای آن را ببیند به ثابت 3 تا شده است. حلقه‌ها هم ساده‌سازی می‌شوند: یک هر م در مقدارها: روی آرایه یا برش، از تحلیل به شکل یک حلقه‌ی اندیسی با شمارنده‌ای که کامپایلر ساخته و نامش __ei1 است بیرون می‌آید.

پیام‌های خطای کامپایلر زبان برنامه‌نویسی سلام چه شکلی دارند

ارزش یک کامپایلر به اندازه‌ی ارزش پیام‌های خطای آن است. هر پیام سلام شدت، کد، فایل و مکان، سطر منبع با یک نشانگر ^ و اغلب یک سطر راهنما یا اصلاح دارد. چهار اشتباه رایج و آنچه salam check برای هر کدام روی کد فارسی چاپ می‌کند در ادامه آمده است (دو مورد آخر فقط با سطرهای نخست).

مقداردهی دوباره به متغیری که ناپایا تعریف نشده:

e1.salam
تابع ریشه:
    شمار := ۱۰
    شمار = شمار + ۱
    سرچاپ شمار
پایان
متن
خطا[خطا۰۱۳]: نمی‌توان متغیر تغییرناپذیر 'شمار' را دوباره مقداردهی کرد؛ برای این کار آن را 'ناپایا' تعریف کنید، یا به‌جای آن به عناصر یا فیلدهایش مقدار دهید
 --> e1.salam:3:5
  |
3 |     شمار = شمار + ۱
  |     ^
  |

یک غلط تایپی در نام. کامپایلر نزدیک‌ترین نام موجود در حوزه را پیشنهاد می‌کند و بعد متغیری را هم گزارش می‌کند که حالا بی‌استفاده مانده است:

متن
خطا[خطا۰۰۱]: شناسه‌ی ناشناخته 'مجمع'؛ منظورتان 'مجموع' بود؟
 --> e4.salam:3:11
  |
3 |     سرچاپ مجمع
  |           ^
  |
  = اصلاح: تغییر به 'مجموع'

فرستادن گونه‌ی نادرست، و اندیس‌گذاری فراتر از انتهای یک آرایه‌ی ثابت:

متن
خطا[خطا۰۱۲]: آرگومان ۲: نمی‌توان 'رشته' را به 'صحیح' پاس داد
 --> e2.salam:6:11

خطا[خطا۰۲۱]: اندیس ۵ برای آرایه‌ای به طول ۳ خارج از محدوده است
 --> e3.salam:3:11

همین پیام‌ها شکل‌های دیگری هم دارند. --error-style=gcc قالب تک‌سطری file:line:col: error: را می‌دهد (برای فایل فارسی به شکل e1.salam:3:5: خطا:) که ویرایشگرها و ابزارهای ساخت می‌فهمند، و --error-format=json برای هر پیام یک شیء JSON می‌دهد که برای اسکریپت‌ها و ابزارهای دیگر مناسب است. برای فایل فارسی، کد و متن پیام در JSON هم فارسی است:

متن
{"level":"error","code":"خطا۰۱۳","message":"نمی‌توان متغیر تغییرناپذیر 'شمار' را دوباره مقداردهی کرد؛ برای این کار آن را 'ناپایا' تعریف کنید، یا به‌جای آن به عناصر یا فیلدهایش مقدار دهید","file":"e1.salam","line":3,"column":5}

فایل منبع فارسی به‌طور پیش‌فرض پیام فارسی می‌گیرد و کد خطا با رقم فارسی نوشته می‌شود (خطا۰۱۳ همان E013 است). در جهت عکس، --error-lang=fa پیام‌های یک فایل انگلیسی را فارسی می‌کند و --error-lang=en پیام‌های یک فایل فارسی را انگلیسی؛ برای همین برنامه‌ی بالا این سطر را چاپ می‌کند: error[E013]: cannot reassign immutable variable 'شمار'.

کدها مشکل‌های مرتبط را گروه‌بندی می‌کنند، نه این‌که برای هر پیام یک نام باشند: برای نمونه E021 هم اندیس لیترالِ بیرون از محدوده را پوشش می‌دهد و هم شرط اگری را که منطقی نیست. پیام را هم مثل عدد بخوانید.

مرحله‌ی ۴: تولید کد با سه پشتوانه

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

  • C (compiler/codegen/) کد منبع C می‌نویسد و آن را به یک کامپایلر C می‌سپارد.
  • LLVM (compiler/llvmgen/) کد LLVM IR می‌نویسد که LLVM آن را بهینه می‌کند و به کد ماشین تبدیل می‌کند.
  • بومی (compiler/nativegen/) متن LLVM IR را که llvmgen تولید کرده می‌خواند، اما به جای فراخوانی LLVM بهینه‌ساز خودش را اجرا می‌کند و مستقیم کد ماشین x86-64، AArch64، x86 سی‌ودوبیتی، ARM سی‌ودوبیتی یا RISC-V می‌نویسد.

یک پشتوانه‌ی جاوااسکریپت برای مرورگر و یک مفسر هم وجود دارد که در مقاله‌ی salam run و salam exec آمده‌اند. خواندن خروجی پشتوانه‌ی C از همه آسان‌تر است، پس این خروجی آن برای مربع و ریشه است (از salam build sq.salam --emit-c، با برنامه‌ی بالا که در sq.salam ذخیره شده؛ این گزینه فایل‌های تولیدشده را در .salam-build/ نگه می‌دارد):

C
static int32_t _Salam_main__d9_85_d8_b1_d8_a8_d8_b9_i32(int32_t _d9_86) {
    (void)_d9_86;
    return ((int32_t)((uint32_t)(_d9_86) * (uint32_t)(_d9_86)));
}

int main(int argc, char** argv) {
    salam_set_args(argc, argv);
    salam_mod_sq_init();
    (void)({ int64_t __po1 = (int64_t)(_Salam_main__d9_85_d8_b1_d8_a8_d8_b9_i32(7)); salam_out_lock(); salam_out_i64(__po1); salam_out_put_("\n", 1); SALAM_OUT_END1(); salam_out_unlock(); });
    return 0;
}

سه نکته ارزش دیدن دارد. نام تابع با بسته و گونه‌ی پارامترهایش دگرگون‌سازی (Name Mangling) شده است، و به همین شکل تابع‌های هم‌نام با امضای متفاوت و تابع‌های هم‌نام در بسته‌های گوناگون از هم جدا می‌مانند. نام فارسی هم بایت به بایت به شکل UTF-8 در نام C نوشته شده: مربع شده است _d9_85_d8_b1_d8_a8_d8_b9 و پارامتر ن شده است _d9_86، پس کد C تولیدشده با هر کامپایلر C قابل کامپایل است. و ضرب از راه uint32_t انجام می‌شود: در C سرریز عدد علامت‌دار رفتار تعریف‌نشده است، پس ساخت توسعه ضرب را بی‌علامت انجام می‌دهد و نتیجه را برمی‌گرداند تا روی هر کامپایلر C همان نتیجه‌ی چرخشی به دست آید. ساخت --release این تضمین را در ازای سرعت کنار می‌گذارد. مقاله‌ی سه راه برای تولید کد این پشتوانه‌ها را با جزئیات مقایسه می‌کند.

مرحله‌ی ۵: کامپایل، نهان‌سازی و پیوند

مرحله‌ی آخر به پشتوانه بستگی دارد.

با پشتوانه‌ی C، هر فایل منبع، و هر بخش از کتابخانه‌ی استاندارد که به کار رفته، جفت فایل خودش را دارد (salam_mod_<name>.c و .h). راننده آن‌ها را به‌صورت موازی و با یک نخ کارگر برای هر هسته‌ی پردازنده کامپایل می‌کند و فایل‌های شیء را نگه می‌دارد. ساخت بعدی فقط آنچه تغییر کرده را دوباره کامپایل می‌کند و گزارش ساخت هم همین را می‌گوید (اینجا برای یک برنامه‌ی کوچک غربال اعداد اول):

متن
[DRIVER][INFO] compiling 1 object file in parallel, reused 9 cached objects
[DRIVER][INFO] built executable: sieve

اگر هیچ چیز تغییر نکرده باشد، salam build عبارت up to date را چاپ می‌کند و کاری انجام نمی‌دهد. -B ساخت کامل از نو را اجبار می‌کند.

با پشتوانه‌ی LLVM، کل برنامه یک ماژول LLVM می‌شود. نسخه‌های اصلی منتشرشده برای لینوکس، مک و ویندوز، LLVM و پیونددهنده‌اش lld را درون فایل salam با خود دارند، پس بهینه‌سازی و پیوند را در همان فرایند و بدون هیچ ابزار بیرونی انجام می‌دهند. ساخت به‌طور پیش‌فرض با -O2 و با --release با -O3 بهینه می‌شود.

با پشتوانه‌ی بومی، کامپایلر خودش فایل شیء را (ELF، COFF یا Mach-O) می‌نویسد و در بیشتر موارد با پیونددهنده‌ی داخلی خودش پیوند می‌دهد. گزارش ساخت یک برنامه‌ی «سلام دنیا» کل پیوند را در یک سطر نشان می‌دهد:

متن
built-in link: 5 objects, 15 live sections, 28 symbols, 1 GOT loads relaxed, 4 merge bytes to 4, 11488 bytes

یعنی یک فایل اجرایی ۱۱٬۴۸۸ بایتی، بدون اجرای هیچ کامپایلر، اسمبلر یا پیونددهنده‌ی بیرونی.

توقف زودهنگام: salam check و salam inspect

بیشتر وقت‌ها به فایل اجرایی نیازی ندارید؛ می‌خواهید بدانید کد درست است یا نه. دو فرمان زنجیره را زودتر متوقف می‌کنند:

  • salam check همه چیز را تا پیش از تولید کد، همراه با بسته‌های واردشده، اجرا می‌کند، پیام‌ها را چاپ می‌کند و اگر خطایی باشد با وضعیت ۱ خارج می‌شود. این همان چرخه‌ی سریعی است که بعد از هر ویرایش اجرا می‌کنید؛ کارساز زبان هم همین تحلیل را درون ویرایشگر شما اجرا می‌کند. هم یک فایل را می‌پذیرد و هم یک پوشه‌ی کامل را.
  • salam inspect یک مرحله را به شکل XML بیرون می‌ریزد: --emit-tokens، --emit-ast یا --emit-symbol (جدول نمادها). این ابزار برای یاد گرفتن این است که کامپایلر کد شما را چگونه می‌بیند، و راهنمای انتقال کد پروژه آن را برای مقایسه‌ی یک مرحله‌ی بازپیاده‌سازی‌شده با یک کامپایلر مرجع، خروجی در برابر خروجی، پیشنهاد می‌کند.
ترمینال
salam check src/
salam check app.salam --error-format=json
salam inspect app.salam --emit-ast --xml-out=ast.xml

salam check همه چیز را نمی‌گیرد. مشکلی که فقط در کد تولیدشده، کامپایلر C یا پیونددهنده بروز می‌کند (برای نمونه یک کتابخانه‌ی سیستمی گم‌شده) به یک salam build واقعی نیاز دارد، و متن راهنمای فرمان هم همین را می‌گوید.

چرا زنجیره‌ی کامپایلر سلام این شکل را دارد

چند تصمیم طراحی بیشتر این ساختار را توضیح می‌دهد:

  • یک پیشانه (Front End)، چند پشتوانه. چون واژه‌کاوی، تجزیه و تحلیل مشترک‌اند، یک قابلیت تازه‌ی زبان یک بار بررسی می‌شود و به همه‌ی پشتوانه‌ها می‌رسد. پشتوانه‌ی بومی یک گام جلوتر می‌رود و خروجی پشتوانه‌ی LLVM را دوباره به کار می‌گیرد، پس این دو نمی‌توانند در معنای یک برنامه اختلاف داشته باشند.
  • بررسی پیش از کد. سلام بسیاری از چیزهایی را که کامپایلرهای دیگر درباره‌شان هشدار می‌دهند (متغیرهای بی‌استفاده، شاخه‌های مرده، مقداردهی دوباره به مقدار تغییرناپذیر، اندیسی که قطعاً بیرون از محدوده است) خطا می‌داند و پیش از تولید هر چیزی آن‌ها را پیدا می‌کند. درس قاعده‌ها مهم‌ترین‌هایشان را فهرست کرده است.
  • دو زبان، یک درخت. بسته‌ی زبانی در همان ابتدا انتخاب می‌شود. پس از واژه‌کاوی، برنامه‌های انگلیسی و فارسی یک درخت و یک مجموعه بررسی دارند؛ بسته‌ی زبانی فقط تعیین می‌کند کدام شکل نوشتاری نام‌ها پذیرفته شود و پیام‌ها به چه زبانی باشند. این همان چیزی است که برنامه‌نویسی فارسی در سلام را به یک زبان کامل تبدیل می‌کند، نه به لایه‌ای ترجمه‌شده روی زبانی دیگر.
  • همه چیز قابل وارسی است. توکن‌ها، AST، جدول نمادها، C تولیدشده، LLVM IR (salam llvm) و اسمبلی بومی (--emit-asm) همه قابل چاپ‌اند. برای دانشجوها، این ویژگی کامپایلر سلام را به یک کتاب درسی زنده و باز تبدیل می‌کند.

اگر می‌خواهید این مراحل را روی کد خودتان ببینید، کامپایلر را از صفحه‌ی نصب نصب کنید یا از ویرایشگر آنلاین استفاده کنید که همین کامپایلر را در مرورگر اجرا می‌کند.

پرسش‌های پرتکرار

مراحل اصلی کامپایلر چیست؟

واژه‌کاوی (متن به توکن)، تجزیه (توکن به درخت نحو)، تحلیل معنایی (گونه‌ها و قاعده‌ها)، تولید کد (درخت به زبانی سطح پایین‌تر یا کد ماشین) و پیوند (فایل‌های شیء به فایل اجرایی). بسیاری از کامپایلرها میان تحلیل و تولید کد یک مرحله‌ی بهینه‌سازی هم دارند؛ در سلام این مرحله درون پشتوانه‌های LLVM و بومی قرار دارد.

کامپایلر زبان سلام با چه زبانی نوشته شده است؟

با خود سلام. کامپایلر از یک پیاده‌سازی قدیمی‌تر به زبان C منتقل شد؛ آن پیاده‌سازی در اوت ۲۰۲۶ از مخزن حذف شد و از آن پس کامپایلر خودش را می‌سازد.

آیا سلام به C کامپایل می‌شود؟

می‌تواند. پشتوانه‌ی C یکی از سه پشتوانه است؛ دو تای دیگر LLVM و تولیدکننده‌ی کد بومی‌اند که مستقیم کد ماشین می‌نویسد. نسخه‌های رسمی منتشرشده وقتی LLVM را با خود دارند به‌طور پیش‌فرض از آن استفاده می‌کنند، و می‌توانید هر پشتوانه‌ای را با --backend=c، --backend=llvm یا --backend=native انتخاب کنید.

چطور ببینم کامپایلر با کد فارسی من چه می‌کند؟

برای پیشانه از salam inspect --emit-tokens، --emit-ast یا --emit-symbol استفاده کنید، برای C تولیدشده از salam build --emit-c، برای LLVM IR از salam llvm و برای کد ماشین به شکل اسمبلی از salam build --backend=native --emit-asm. همه‌ی این فرمان‌ها برای فایل فارسی هم دقیقاً همان‌طور کار می‌کنند.

کامپایلر و سرعت

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

کامپایلر سلام به زبان سلام نوشته شده است. این روایت چگونگی رسیدن به این نقطه است: هر نسخه چطور از نسخه‌ی پیشین بوت‌استرپ می‌شود، خودمیزبانی برای یک زبان جوان چه سودی دارد و چه هزینه‌ای.

خواندن مقاله
کامپایلر و سرعت

گونه‌بندی ایستا در زبان برنامه‌نویسی سلام: گرفتن باگ‌ها پیش از اجرای برنامه

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

خواندن مقاله