شاید تا حالا این جمله رو شنیده باشید: «کامپایلر زبان اکس با خود زبان اکس نوشته شده». اولین باری که من این رو شنیدم، با خودم گفتم این که غیر ممکنه. مثل این میمونه که بگی یه نفر قبل از اینکه به دنیا بیاد، خودش رو به دنیا آورده. یا مثل همون معمای معروف مرغ و تخممرغه. چطور میشه یه ابزار، خودش رو بسازه؟ این سوالیه که امروز میخوایم با هم به جوابش برسیم. قراره وارد دنیای «خودمیزبانی» یا «سلف هاستینگ» بشیم و ببینیم این ایده چطور کار میکنه، چرا اینقدر مهمه و اصلا چه ارزشی داره.
این موضوع فقط یه بحث تئوری نیست. درک کردنش به شما یه دید عمیقتر نسبت به نحوه ساخته شدن ابزارهایی که هر روز باهاشون کار میکنید میده؛ از سیستمعاملتون گرفته تا زبانهای برنامهنویسی که دوست دارید.
خودمیزبانی یعنی چی دقیقا؟
وقتی میگیم یه برنامه خودمیزبانه، یعنی اون برنامه با استفاده از زبانی نوشته شده که خودش قراره پردازشش کنه. مثلا یه کامپایلر خودمیزبان، کامپایلریه که سورس کدش به همون زبانی نوشته شده که برای کامپایل کردنش طراحی شده.
این ایده برای خیلی از زبانهای برنامهنویسی مثل Go یا C# یه هدف مهمه. کامپایلرهای این زبانها در نهایت با خود همون زبانها نوشته شدن. اما از طرف دیگه، زبانهایی مثل پایتون یا جاوا اسکریپت خودمیزبان نیستن. مثلا خود پایتون با زبان C نوشته شده و این موضوع هیچ مشکلی هم برای جامعه پایتون یا C ایجاد نکرده.
این مفهوم فقط به کامپایلرها محدود نمیشه. خیلی از نرمافزارهای دیگه هم خودمیزبان هستن:
- سیستمعاملها: یه سیستمعامل وقتی خودمیزبانه که ابزارهای لازم برای ساختن نسخههای جدیدش، روی خود همون سیستمعامل اجرا بشن. مثلا شما میتونید نسخه جدید ویندوز رو روی یه کامپیوتر که در حال حاضر ویندوز روش نصبه، بسازید و کامپایل کنید.
- برنامههای دیگه: کرنلها (هسته سیستمعامل)، اسمبلرها (برنامههایی که کد اسمبلی رو به کد ماشین تبدیل میکنن)، مفسرهای خط فرمان (مثل ترمینال) و حتی نرمافزارهای کنترل نسخه (مثل گیت) هم معمولا خودمیزبان هستن.
حالا این سوال پیش میاد: آیا خودمیزبانی یه جور نشونه بلوغ برای یه زبانه؟ یا فقط یه کار اضافیه که از اهداف اصلی منحرفمون میکنه؟ برای جواب دادن به این سوال، اول باید بزرگترین چالش این مسیر رو بشناسیم.
معمای مرغ و تخممرغ: مشکل بوتاسترپینگ
خب، رسیدیم به همون مشکل اصلی. اگه کامپایلر زبان اکس قراره با خود زبان اکس نوشته بشه، پس اولین نسخه این کامپایلر رو کی یا چی باید کامپایل کنه؟ ما که هنوز کامپایلری برای زبان اکس نداریم! این دقیقا همون مشکل «بوتاسترپینگ» یا به قول معروف، معمای مرغ و تخممرغه.
برای اینکه یه سیستم بتونه به مرحله خودمیزبانی برسه، اولش به یه سیستم دیگه نیاز داره تا توسعه پیدا کنه. فرض کنید دارید برای یه کامپیوتر یا سیستمعامل کاملا جدید برنامه مینویسید. شما هم به یه سیستم برای اجرای نرمافزارهای توسعه نیاز دارید و هم به خود اون نرمافزارها برای نوشتن و ساختن سیستمعامل. این یه دور باطله.
راه حل این مشکل استفاده از چیزی به اسم «کراس کامپایلر» (Cross Compiler) یا «کراس اسمبلر» هست. کراس کامپایلر یه کامپایلره که روی یه پلتفرم (مثلا ویندوز) اجرا میشه، اما کدی که تولید میکنه برای یه پلتفرم دیگه (مثلا یه کنسول بازی یا یه برد الکترونیکی) قابل اجراست. اینطوری میشه برای یه ماشینی که هنوز کامپایلر خودمیزبان نداره، نرمافزار ساخت.
وقتی نرمافزار اولیه ساخته شد، میشه اون رو با استفاده از ابزارهایی مثل فلش مموری، دیسک یا کابل JTAG به سیستم مقصد منتقل کرد. این روش دقیقا همون کاریه که برای ساختن نرمافزار برای کنسولهای بازی یا گوشیهای موبایل انجام میدن؛ چون این دستگاهها معمولا ابزارهای توسعه رو روی خودشون ندارن.
وقتی سیستم به اندازه کافی بالغ شد و تونست کد خودش رو کامپایل کنه، دیگه نیازی به اون سیستم اولیه نیست و وابستگی از بین میره. در این نقطه میگیم اون سیستم «خودمیزبان» شده.
فرآیند بوتاسترپینگ برای یک کامپایلر
این فرآیند برای کامپایلرها هم دقیقا به همین شکله. چون کامپایلرهای خودمیزبان هم با همون مشکل بوتاسترپینگ درگیرن، اولین نسخه از کامپایلر یه زبان جدید باید با یه زبان موجود نوشته بشه. توسعهدهندهها ممکنه از اسمبلی، C/C++ یا حتی زبانهای اسکریپتی مثل پایتون یا Lua برای ساختن نسخه اولیه استفاده کنن.
وقتی زبان جدید به اندازه کافی کامل و قدرتمند شد، توسعه کامپایلر به خود اون زبان منتقل میشه. از اون به بعد، کامپایلر میتونه خودش رو بسازه و کامپایل کنه.
یه مثال خیلی جالب، داستان ساخت کامپایلر زبان اسکالا (Scala) هست. نسخه اولیه کامپایلر اسکالا با جاوا نوشته شده بود. اما نسخههای بعدی (مثلا نسخه ۲) خودمیزبان شدن، یعنی با خود اسکالا نوشته شدن. یا مثلا زبان کافیاسکریپت (CoffeeScript) که در ابتدا کامپایلرش با زبان روبی (Ruby) نوشته شده بود.
پس اگه یه نفر به شما گفت «کامپایلر کافیاسکریپت با کافیاسکریپت نوشته شده»، این جمله یه کم گمراهکنندهست. جمله دقیقتر اینه:
- نسخه ۶ کامپایلر کافیاسکریپت با نسخه ۵ نوشته شده، نسخه ۷ با نسخه ۶، نسخه ۸ با نسخه ۷ و همینطور الی آخر.
این فرآیند مثل بالا رفتن از یه نردبونه. شما با استفاده از نسخه فعلی، یه نسخه جدید و بهتر از نردبون رو میسازید، بعد روی نردبون جدید میرید و نردبون قبلی رو دور میندازید. این چرخه همینطور ادامه پیدا میکنه.
یه مثال عملی (و یه کم خندهدار): زبان برنامهنویسی گلابی!
برای اینکه این فرآیند رو بهتر بفهمیم، بیاید با هم یه زبان برنامهنویسی خیالی و مسخره به اسم «گلابی» (Gluby) بسازیم و کامپایلرش رو خودمیزبان کنیم. این مثال رو رابرت هیتون برای آموزش همین مفهوم ساخته.
قوانین زبان گلابی خیلی سادهست:
- این زبان از نظر سینتکس دقیقا مثل زبان روبی (Ruby) هست.
- فقط یه فرق کوچیک داره: به جای علامت مساوی (
=
)، باید از کلمه انگلیسیEQUALS
استفاده کنید. - شما حق ندارید از کلمه
EQUALS
در هیچ جای دیگهای از برنامهتون استفاده کنید، وگرنه همه چیز خراب میشه!
حالا میخوایم برای این زبان یه کامپایلر خودمیزبان بنویسیم.
مرحله اول: نسخه صفر کامپایلر گلابی (نوشته شده با پایتون)
همونطور که گفتیم، برای شروع به یه زبان دیگه نیاز داریم. ما اینجا از پایتون استفاده میکنیم. پس نسخه صفر (v0) کامپایلر گلابی ما یه برنامه پایتونیه. کار این برنامه خیلی سادهست:
- یه فایل با پسوند
.gl
(فایل سورس گلابی) رو به عنوان ورودی میگیره. - تمام کلمات
EQUALS
رو پیدا میکنه و اونها رو با علامت=
جایگزین میکنه. - خروجی رو توی یه فایل جدید با پسوند
.blc
ذخیره میکنه.
این فایل خروجی در واقع یه کد روبی معتبره که میشه با مفسر روبی اجراش کرد. کد پایتونش چیزی شبیه این میشه:
from functools import reduce import sys if __name__ == '__main__': input_filename = sys.argv[1] input_filename_chunks = input_filename.split('.') output_filename = "%s.blc" % "".join(input_filename_chunks[0:-1]) # "=" is 61 in ASCII swaps = [(chr(61), 'EQUALS')] with open(input_filename) as input_f: with open(output_filename, 'w+') as output_f: # Read all the lines in the source code for l in input_f.readlines(): # Make the swaps new_l = reduce( (lambda running_l, sw: running_l.replace(swap[1], swap[0])), swaps, l ) # Write out a line to the compiled file output_f.write(new_l)
خب، ما الان یه کامپایلر اولیه داریم. این کامپایلر میتونه کد گلابی رو به کد روبی تبدیل کنه. حالا وقت مرحله بعده.
مرحله دوم: نسخه یک کامپایلر گلابی (نوشته شده با خود گلابی!)
حالا که زبان گلابی به اندازه کافی قدرتمند شده (چون تمام قدرت روبی رو به ارث برده)، میتونیم ازش برای بازنویسی خود کامپایلر استفاده کنیم. نسخه یک (v1) کامپایلر گلابی، با سینتکس خود گلابی نوشته میشه. یعنی به جای =
از EQUALS
استفاده میکنیم.
input_filename EQUALS ARGV[1] input_filename_chunks EQUALS input_filename.split('.') output_filename EQUALS "#{input_filename_chunks[0...-1].join('')}.blc" # Use ASCII 69 instead of "E" to make sure we don’t replace the string #"EQUALS" when compiling this program. swaps EQUALS [[61.chr, 69.chr + 'QUALS']] File.open(input_filename, "r") do |input_f| File.open(output_filename, "w+") do |output_f| input_f.each_line do |l| new_l EQUALS swaps.reduce(l) do |memo, swap| memo.gsub(swap[1], swap[0]) end end output_f.puts(new_l) end end
حالا اون لحظه جادویی فرا میرسه. ما این کد (که سورس نسخه ۱ کامپایلره) رو به کامپایلر نسخه صفر (همون برنامه پایتونی) میدیم. برنامه پایتونی تمام EQUALS
ها رو با =
عوض میکنه و یه فایل خروجی به ما میده. این فایل خروجی، یه برنامه روبی کاملا قابل اجراست.
و این برنامه قابل اجرا چیه؟ این خود کامپایلر نسخه یک گلابیه
از این لحظه به بعد، ما دیگه هیچ نیازی به اون کد پایتونی اولیه نداریم. میتونیم برای همیشه حذفش کنیم. چرا؟ چون الان یه کامپایلر جدید داریم که هم سورس کدش به زبان گلابیه و هم نسخه قابل اجراش موجوده. برای کامپایل کردن نسخههای بعدی کامپایلر (مثلا نسخه ۲)، از همین نسخه ۱ استفاده میکنیم. زبان گلابی رسما خودمیزبان شد!
سازنده این زبان خیالی در ادامه به شوخی میگه که داره روی نسخه ۲ کار میکنه و میخواد به جای کلمه end
از عبارت ALL_HAIL_ROBERT
استفاده کنه.
چرا این همه دردسر؟ مزایای خودمیزبانی چیه؟
خب، حالا که فهمیدیم خودمیزبانی چیه و چطور کار میکنه، سوال اصلی اینه: چرا یه تیم باید این همه هزینه و ریسک رو قبول کنه و کامپایلرش رو خودمیزبان کنه؟ این کار چه فایدهای داره؟
شرکت مایکروسافت یه تجربه بزرگ در این زمینه داره. کامپایلرهای زبان سیشارپ از نسخه ۱.۰ تا ۶.۰ با زبان C++ نوشته شده بودن. اما برای نسخه ۶.۰، تیم رو به دو بخش تقسیم کردن: یه تیم به توسعه ویژگیهای جدید روی همون کدبیس C++ ادامه داد و تیم دوم وظیفه داشت کل کامپایلر رو از اول با خود سیشارپ بازنویسی کنه. این یه تصمیم فوقالعاده پرهزینه و پرریسک برای یکی از مهمترین محصولات مایکروسافت بود. پس حتما دلایل خیلی خوبی براش داشتن. بیاید این دلایل رو بررسی کنیم:
۱. بهترین تست ممکن برای زبان (Dogfooding)
این یکی از مهمترین مزایای خودمیزبانیه که تقریبا همه بهش اشاره میکنن. اصطلاح «Dogfooding» یعنی «غذای سگ خودت رو بخوری». یعنی از محصولی که خودت تولید میکنی، خودت هم استفاده کنی.
- یه تست واقعی و پیچیده: یه کامپایلر برنامه خیلی پیچیدهایه. اگه یه زبان اونقدر قدرتمند باشه که بشه باهاش یه کامپایلر نوشت، این نشون میده که احتمالا برای نوشتن طیف وسیعی از برنامههای پیچیده دیگه هم مناسبه. این یه جور «آزمون تورنسل» برای زبان محسوب میشه.
- پیدا کردن سریع باگها و مشکلات عملکردی: وقتی توسعهدهندههای کامپایلر هر روز از کامپایلر خودشون برای کامپایل کردن خود کامپایلر استفاده میکنن، خیلی سریع با باگها، مشکلات عملکردی و نقاط ضعف زبان روبرو میشن. تیم سیشارپ میخواست کامپایلر جدیدشون بتونه برنامههای چند میلیون خطی رو به راحتی مدیریت کنه. داشتن خود کامپایلر به عنوان یه کیس تستی بزرگ، بهشون خیلی کمک کرد تا مشکلات رو سریع پیدا و حل کنن.
- بررسی جامع سازگاری: کامپایلر باید بتونه کد آبجکت خودش رو بازتولید کنه. این یه بررسی سازگاری خیلی کامله که نشون میده کامپایلر کارش رو درست انجام میده.
یه نویسنده در این مورد از تجربه شخصی خودش میگه. اون مجبور بوده با یه کامپایلر معروف که خودمیزبان نیست کار کنه و میگه تقریبا هر دو روز یک بار با خطای segfault (یه نوع کرش کردن برنامه) مواجه میشده. اون با خودش فکر میکنه که اگه طراحان اون کامپایلر مجبور بودن هر روز ازش استفاده کنن، حتما این مشکلات خیلی زودتر حل میشد.
۲. راحتی و بهرهوری تیم توسعه
- تخصص در یک زبان: وقتی کامپایلر خودمیزبانه، توسعهدهندهها و کسایی که باگها رو گزارش میدن، فقط کافیه به همون زبان مسلط باشن. این کار باعث میشه تعداد زبانهایی که تیم باید بدونه کمتر بشه. در حالت عادی، تیم باید زبان ماشین (یا اسمبلی)، زبان هدف (که کامپایلر براش ساخته میشه) و زبان سورس کامپایلر رو بلد باشه. اما در حالت خودمیزبان، این تعداد به دو زبان کاهش پیدا میکنه.
- استفاده از ویژگیهای سطح بالا: توسعهدهندهها میتونن از ویژگیهای راحتتر و سطح بالاتر زبان خودشون برای توسعه کامپایلر استفاده کنن. تیم سیشارپ همگی برنامهنویسهای متخصص سیشارپ بودن و شکی نبود که میتونن با این زبان خیلی بهرهوری بالایی داشته باشن. سیشارپ هم به طور خاص طوری طراحی شده بود که یه جایگزین امنتر و پربازدهتر برای کدبیسهای بزرگ C++ باشه.
۳. بهبود خودکار کامپایلر
این یکی از جالبترین مزایاست. هر بهبودی که در بخش بکاند (Backend) کامپایلر ایجاد میشه، نه تنها روی برنامههایی که کاربران مینویسن تاثیر مثبت داره، بلکه روی خود کامپایلر هم تاثیر میذاره. یعنی وقتی شما کامپایلر رو طوری بهینه میکنید که کد سریعتری تولید کنه، دفعه بعدی که خود کامپایلر رو با خودش کامپایل میکنید، نسخه جدید کامپایلر هم سریعتر اجرا میشه! این یه چرخه بهبود مستمر ایجاد میکنه.
۴. تاثیر در طراحی خود زبان
استفاده از زبان برای ساختن کامپایلر خودش، میتونه روی طراحی ویژگیهای جدید زبان هم تاثیر بذاره. البته تیم سیشارپ مراقب بود که در دام طراحی زبان فقط برای نیازهای تیم کامپایلر نیفته، اما در طول مسیر به چند تا مشکل برخوردن که باعث شد ویژگیهای جدیدی به زبان اضافه بشه.
برای مثال، آندرس هایلزبرگ (طراح اصلی سیشارپ) همیشه با اضافه کردن «فیلترهای استثنا» (exception filters) مخالف بود، در حالی که این ویژگی در زبان ویژوال بیسیک وجود داشت. اما تیم کامپایلر در حین کار چند تا مورد استفاده خیلی خوب براش پیدا کردن و تونستن آندرس رو قانع کنن که این ویژگی به سیشارپ هم اضافه بشه.
۵. مشارکت بیشتر جامعه
اگه کامپایلر با یه زبان شناختهشده و محبوب نوشته شده باشه، احتمال اینکه جامعه برنامهنویسها در توسعهش مشارکت کنن خیلی بیشتره. زبان سیشارپ میلیونها کاربر داشت. وقتی کامپایلر جدیدش به اسم «رازلین» (Roslyn) به صورت اوپن سورس منتشر شد، حمایت و مشارکت جامعه در ده سال گذشته برای این پروژه خیلی مفید بوده.
۶. ساخت اکوسیستم ابزارها
یکی از اهداف اصلی تیم سیشارپ از بازنویسی کامپایلر، فقط خودمیزبانی نبود. اونها میخواستن تحلیلگرهای لغوی، سینتکسی و معنایی کامپایلر رو به صورت یه سری سرویس در بیارن که هر کسی بتونه ازشون استفاده کنه، نه فقط تیم IDE. اونها میدونستن که داخل خود مایکروسافت نیاز زیادی به یه کتابخونه از سرویسهای کامپایلر وجود داره.
اونها پیشبینی کردن که یه اکوسیستم بزرگ از افزونههای تحلیل کد برای IDE ها به وجود میاد. و زبان طبیعی برای نوشتن این افزونهها چی بود؟ خود سیشارپ. پس منطقی بود که خود «کامپایلر به عنوان سرویس» هم با سیشارپ نوشته بشه.
نگاهی به تاریخ: اولینها و مهمترینها
ایده خودمیزبانی یه ایده جدیده نیست و تاریخچه جالبی داره. بیاید چند تا از موارد مهم رو با هم مرور کنیم.
- Lisp (۱۹۶۲): اولین کامپایلر خودمیزبان (به جز اسمبلرها) برای زبان لیسپ توسط هارت و لوین در MIT در سال ۱۹۶۲ نوشته شد. داستانش خیلی جالبه. اونها یه کامپایلر لیسپ رو با خود لیسپ نوشتن و اون رو داخل یه مفسر لیسپ که از قبل وجود داشت تست کردن. اونها اینقدر کامپایلر رو بهبود دادن تا به جایی رسید که تونست سورس کد خودش رو کامپایل کنه. در اون لحظه، کامپایلر رسما خودمیزبان شد. این تکنیک وقتی عملیه که از قبل یه مفسر برای اون زبان وجود داشته باشه.
- Unix و زبان C: کن تامپسون توسعه یونیکس رو در سال ۱۹۶۸ شروع کرد. اون برنامهها رو روی یه کامپیوتر بزرگ GE-635 مینوشت و کامپایل میکرد، بعد اونها رو به یه کامپیوتر کوچیکتر به اسم PDP-7 منتقل میکرد تا تستشون کنه. بعد از اینکه هسته اولیه یونیکس، مفسر فرمان، ویرایشگر، اسمبلر و چند تا ابزار دیگه کامل شدن، سیستمعامل یونیکس خودمیزبان شد. یعنی از اون به بعد میشد برنامهها رو روی خود همون PDP-7 نوشت، کامپایل کرد و تست کرد.
- TMG: یه داستان فوقالعاده دیگه مربوط به داگلاس مکایلروی هست. اون یه کامپایلر-کامپایلر (برنامهای که کامپایلر میسازه) به اسم TMG رو روی کاغذ با خود سینتکس TMG نوشت. بعدش به قول خودش «تصمیم گرفت تیکه کاغذش رو به تیکه کاغذش بده». یعنی خودش شخصا و به صورت دستی، فرآیند کامپایل رو روی کاغذ انجام داد و یه خروجی اسمبلی تولید کرد. بعد اون کد اسمبلی رو تایپ کرد و روی PDP-7 کن تامپسون اسمبل کرد و اجرا کرد!
- پروژه گنو (GNU): توسعه سیستم گنو به شدت به GCC (مجموعه کامپایلرهای گنو) و گنو ایمکس (یه ویرایشگر محبوب) وابسته است. این ابزارها امکان توسعه پایدار و مستقل نرمافزارهای آزاد رو برای پروژه گنو فراهم کردن.
امروزه زبانهای خیلی زیادی کامپایلرهای خودمیزبان دارن. این لیست فقط بخشی از اونهاست:
Ada | C | C++ | C# |
---|---|---|---|
Go | Haskell | Java | Kotlin |
Lisp | Python (PyPy) | Rust | Scala |
Swift | TypeScript | Zig | و خیلیهای دیگه… |
آیا خودمیزبانی همیشه بهترین راهه؟
با تمام مزایایی که گفتیم، خودمیزبانی یه سری چالش هم داره. همونطور که توسعهدهنده زبان Leaf اشاره کرد، «دردسر بوتاسترپینگ» یه مشکل واقعیه و میتونه تمرکز تیم رو از اهداف اصلی زبان، مثل اضافه کردن ویژگیهای جدید، منحرف کنه.
یه سوال جالب دیگه هم در این مورد مطرح میشه: آیا زبانی که برای کامپایل شدن به یه کتابخونه بزرگ مثل LLVM وابسته است، واقعا خودمیزبانه؟
بعضیها معتقدن که اگه کامپایلر شما نتونه سورس کد LLVM رو هم کامپایل کنه، پس کاملا خودمیزبان نیست. شاید خودمیزبانی واقعی زمانی اتفاق بیفته که زبان شما بکاند (Backend) خودش رو هم داشته باشه، حتی اگه خیلی ضعیف باشه. در این صورت، LLVM فقط یه هدف (Target) دیگه در کنار بقیه هدفها محسوب میشه. این بحث نشون میده که مفهوم خودمیزبانی میتونه لایههای مختلفی داشته باشه.
از کجا شروع کنیم؟ راهنمای عملی برای کنجکاوها
اگه به این موضوع علاقهمند شدید و دوست دارید بدونید چطور میشه یه کامپایلر رو خودمیزبان کرد، اینجا یه نقشه راه کلی بر اساس تجربیات دیگران وجود داره. فرآیند برای زبانهای مفسری و کامپایلری یه کم متفاوته.
برای زبانهای مفسری/دینامیک:
- یه رانتایم (Runtime) اولیه با یه زبان موجود مثل C، راست یا گو بنویسید.
- این رانتایم رو گسترش بدید تا بتونه با فایل سیستم کار کنه و فرمتهای اجرایی نیتیو رو بفهمه.
- حالا دوباره همون رانتایم رو این بار با استفاده از زبان خودتون بازنویسی کنید.
- رانتایمی که تازه نوشتید رو با استفاده از رانتایم اولیه اجرا کنید و اون رو به یه فایل اجرایی خروجی بگیرید.
- تبریک میگم! مفسر یا REPL شما خودمیزبان شد!
یه مثال خوب در این زمینه، زبان اینکو (Inko) هست. کامپایلر فعلیش با روبی نوشته شده و سازندهش داره تلاش میکنه اون رو خودمیزبان کنه. در اینکو، عملیاتهای سطح پایین (مثل باز کردن فایل) به صورت دستورات ماشین مجازی (VM instructions) پیادهسازی شدن. برای مثال، کدی که پروسس در حال اجرا رو برمیگردونه این شکلیه:
def current -> Process { _INKOC.process_current }
اینجا _INKOC.process_current
یه دستور اولیهست که کامپایلر اون رو به یه دستور ماشین مجازی تبدیل میکنه. این روش به سازنده اینکو اجازه میده بدون تغییر سینتکس زبان، این قابلیتهای سطح پایین رو اضافه کنه.
برای زبانهای کامپایلری (با استفاده از LLVM):
- یه فرانتاند (Frontend) بنویسید که زبان شما رو بگیره و اون رو به یه نمایش میانی (Intermediate Representation یا IR) برای یه بکاند مثل LLVM یا GCC تبدیل کنه.
- یه بکاند با زبان خودتون بنویسید که این نمایش میانی رو بگیره و به اسمبلی یا بایتکد تبدیل کنه. این بکاند جدید رو با استفاده از فرانتاند اولیه کامپایل کنید.
- اگه بکاند جدید شما از نمایش میانی متفاوتی استفاده میکنه، فرانتاند رو طوری تغییر بدید که باهاش سازگار باشه.
- حالا فرانتاند رو هم با زبان خودتون بازنویسی کنید و اون رو با استفاده از فرانتاند قدیمی و بکاند جدید کامپایل کنید.
- الان شما یه تولچین (toolchain) کاملا نیتیو دارید که با زبان خودتون نوشته شده!
یک پروژه واقعی: استارفورث (Starforth)
برای اینکه ببینیم این ایدهها در عمل چطور پیاده میشن، بیاید نگاهی به پروژه استارفورث بندازیم. این پروژه توسط یه برنامهنویس شروع شده که همیشه به زبان فورث (Forth) علاقه داشته. فورث یه زبان خیلی خاصه که فقط با یه استک کار میکنه و از نوشتار لهستانی معکوس استفاده میکنه. یعنی به جای 2 + 2
مینویسید 2 2 +
.
نقطه عطف برای این برنامهنویس زمانی بود که فهمید در زبان فورث، وقتی اسم یه متغیر (مثلا foo
) رو مینویسید، آدرس اون متغیر روی استک قرار میگیره. بعد با یه عملگر به اسم @
(fetch) میشه اون آدرس رو از روی استک خوند و به مقدار داخلش دسترسی پیدا کرد. اونجا بود که متوجه شد: فورث اشارهگر (pointer) داره!
این کشف باعث شد که تصمیم بگیره یه کامپایلر جدید به اسم استارفورث بنویسه و این اهداف رو برای خودش مشخص کرد:
- تولید کد ماشین واقعی: برخلاف پروژههای قبلیش که برای ماشینهای مجازی بودن، این بار میخواست کد ماشین واقعی تولید کنه.
- خودمیزبانی: کامپایلر باید بتونه سورس کد خودش رو کامپایل کنه.
- کامپایلر پیش از اجرا (Ahead-of-Time): برخلاف اکثر پیادهسازیهای فورث که مفسری هستن، این کامپایلر باید کارش رو انجام بده و از مسیر خارج بشه.
- بوتاسترپ فقط با یک اسمبلر: فایل باینری اولیه باید تا حد ممکن کوچیک باشه و فقط با یه اسمبلر ساده قابل ساخت باشه.
تصمیمات فنی پروژه هم اینها بودن:
- محیط هدف: لینوکس.
- معماری: x86 ۳۲ بیتی (چون کدش کوچیکتره و حس و حال روزهای قدیم رو داره!).
- خروجی: فایلهای باینری ELF کاملا مستقل، بدون هیچ وابستگی به کتابخانههای اشتراکی مثل libc.
این پروژه نشون میده که چطور یه علاقه شخصی به یه زبان خاص میتونه به یه پروژه چالشبرانگیز و آموزنده برای ساخت یه کامپایلر خودمیزبان تبدیل بشه.
پرسش و پاسخ
سوال ۱: پس خلاصه، یه کامپایلر خودمیزبان یعنی با همون زبانی نوشته شده که خودش کامپایل میکنه. این چطور ممکنه؟
جواب: این کار از طریق یه فرآیندی به اسم «بوتاسترپینگ» انجام میشه. شما نسخه اولیه کامپایلر (نسخه صفر) رو با یه زبان دیگه که از قبل وجود داره (مثلا پایتون یا C++) مینویسید. بعد با استفاده از این کامپایلر اولیه، نسخه جدید کامپایلر (نسخه یک) رو که با زبان خودش نوشته شده، کامپایل میکنید. وقتی نسخه یک ساخته شد، دیگه نیازی به نسخه صفر ندارید و از اون به بعد کامپایلر میتونه خودش رو کامپایل و بهروزرسانی کنه.
سوال ۲: اصلیترین فایده این همه زحمت چیه؟
جواب: مهمترین فایدهش «Dogfooding» هست. یعنی شما از محصول خودتون استفاده میکنید. این کار بهترین تست ممکن برای زبان شماست. چون کامپایلر یه برنامه خیلی پیچیدهست، اگه زبان شما بتونه از پس نوشتن خودش بربیاد، یعنی به بلوغ و قدرت کافی رسیده. این فرآیند به پیدا کردن سریع باگها، مشکلات عملکردی و حتی بهبود طراحی خود زبان کمک میکنه.
سوال ۳: آیا همه زبانهای بزرگ مثل پایتون خودمیزبان هستن؟
جواب: نه لزوما. مثلا مفسر اصلی پایتون (CPython) با زبان C نوشته شده. کامپایلر جاوا اسکریپت هم همینطور. خودمیزبان نبودن یه زبان به هیچ وجه نشونه ضعف اون نیست و چیزی از ارزشهای اون زبان یا زبانی که باهاش نوشته شده کم نمیکنه. این فقط یه انتخاب در مسیر توسعهست.
سوال ۴: اولین کامپایلر خودمیزبان برای چه زبانی ساخته شد؟
جواب: اولین کامپایلر خودمیزبان (به جز اسمبلرها که از ابتدا خودمیزبان بودن) برای زبان Lisp در سال ۱۹۶۲ در دانشگاه MIT توسط هارت و لوین ساخته شد. اونها کامپایلر لیسپ رو داخل یه مفسر لیسپ توسعه دادن تا به نقطهای رسید که تونست خودش رو کامپایل کنه.
منابع
- [2] What are the benefits to self-hosting compilers? – Programming Language Design and Implementation Stack Exchange
- [4] Why would we want a self-hosting compiler? – Computer Science Stack Exchange
- [6] Why are self-hosting compilers considered a rite of passage for new languages? – Software Engineering Stack Exchange
- [8] elektito | Starforth: A Minimal Self-Hosting Compiler