خیلی از زبونها رو میشنویم که از llvm استفاده میکنن؟
این چی بود اصلا که همه کامپایلرها رو گرفت؟
ماجرا از اینجا شروع میشه که کامپایلرها باید برای کامپایل کردن یک فایل از زبون مبدا (مثلا راسط) به زبان ماشین، این عملیات رو انجام بدن:
۱- خوندن کیلدواژه به کلیدواژهی سورسکد مبدا
۲- پارس کردن ساختارهای مختلف با کمک کلیدواژهها (parsing خیلی مفصله ماجراش و الگوریتمهای متنوعی داره که باشه برای بعد)
۳- بررسی اینکه این کدی که نوشته شده چیه، مثلا تایپهاش درست هستن؟ معنی میده اصلا؟ بهینهسازیای نیاز نداره و اینا.
۴- تبدیل کد ماشین
حالا این تبدیل کد ماشین رو ما خیلی ساده میگیم ماشین اما کدوم ماشین؟ x86؟ arm؟ پس فردا risc-v و mips چی؟ بعد هر کدوم از اینا نسخه دارن و instruction مختلفی بهشون اضافه میشه. اینکه فقط بخوایم یه کدی تولید کنیم شاید خیلی سخت نباشه ولی بهینه کردنش و اطمینان از اینکه درست کار میکنه و پشتیبانی از معماری پردازندههای مختلف واقعا کار سختیه. این کار سخت رو مثلا کامپایلر GCC خودش انجام میده.
اما همهی کامپایلرها اینطور کار نمیکنن. مثلا جاوا یک رویکرد تقریبا منحصر به فرد پیش گرفت. گفت من کامپایلرم نمیتونه کد به هر پلتفرم مقصدی تولید کنه، بلکه میام یه «کد میانی» تولید میکنم به اسم byte code. یعنی چی؟ یعنی این کد میانی یه جور کد ماشینیه که واقعا هیچ ماشینی بلد نیست اجراش کنه، یه اسمبلیه برای یک پردازندهی فرضی. مزیتش برای کامپایلر اینه که فقط لازمه دغدغهش این باشه که همین رو تولید کنه و همین رو بهینه کنه.
اما در مرحلهی بعد چه کسی اینو اجرا کنه؟ مجبوریم یه VM (به اسم JVM یا java virtual machine) دااشته باشیم که اون بایتکد رو میگیره و ترجمه میکنه به زبان ماشین و همونجا هم اجراش میکنه. مثلا JVM برای پردازندهی x86 و الی آخر.
جای تعجب نداره که جاوا اونقدر نمیتونه سریع باشه چون این JVM به هر حال سربار داره، هرچقدر هم بهینهش کنن چون به هر حال داره ترجمهای رو انجام میده.
اما حالا فرض کنید که کامپایلر ما همون کار راحت تولید یه bytecode رو انجام میداد، اما به جای اینکه خود byte code رو بدیم به ملت و اونجا مجبور باشن VM نصب کنن، این bytecode رو یکی تبدیل میکرد به کد ماشین و بهینهش هم میکرد. خب llvm همینجا وارد میشه!
یعنی چی؟ یعنی به کامپایلر راسط میگه شما لازم نیست خودت کد اسمبلی/زبان ماشین تولید کنی، بلکه بیا LLVM IR یا LLVM intermediate representation تولید کن. من اینو تبدیل میکنم به کد ماشین بهینه!
و اینجا یه مفهوم front-end و back-endی هم شکل میگیره. فرانتاند به قسمتی که سورسکد رو میخونه و پارس میکنه و ... تا IR تولید بشه میگن و بک اند میشه قسمتی که IR رو میخونه و پردازش میکنه و کد ماشین تولید میکنه. LLVM برای ما نقش یک بکاند رو داره.
حالا سینتکس LLVM IR چه شکلیه؟ خیلی برامون مهم نیست ولی میتونیم ببینیمش. مثلا به clang بگیم که به جای تولید کد ماشین نهایی، برامون LLVM IR رو خروجی بده. میتونه این کارو بکنه. حتما حدس میزنید که clang یه کامپایلر بر پایهی llvmئه که این قابلیت رو داره.
clang -S -emit-llvm foo.c
با کمک این دستور میتونید IR رو مشاهده کنید و حتی میتونید با کمک llvm اجراش کنید. با دستور lli یا llvm interpreter میتونید به شکل تفسیری اجراش کنید یا با llc کامپایلش کنید.
اما llvm باز هم کار ما رو سادهتر میکنه، به جای اینکه از فرانتاند بخواد که یکجا بهش IR رو تحویل بده، میاد از visitor design pattern استفاده میکنه. خلاصهش اینطوریه که به جای اینکه نهایتا خروجی IR رو ازمون بگیره، در روال ساختش هم مشارکت میکنه. مثلا میگه تو میدونی که یه تابع داری ولی محتواش رو هنوز نخوندی؟ عیب نداره تا همینجا به من بگو من برات میسازم. خب یه if توش داری؟ من داخل تابع اضافهش میکنم. و الی آخر. برای اینکه از این قابلیت استفاده کنیم لازم داریم که توابعی که llvm داره مثلا برای ساخت تابع، داخل فرانتاندمون صدا کنیم. اینجا یه مشکلی که داریم اینه که به شکل پیشفرض کتابخونهش فقط برای cpp وجود داره، که البته چون خیلی از کامپایلرها با cpp هستن برامون کافیه. ولی همچنین یکسری binding (غیررسمی) برای باقی زبونها هم وجود داره مثلا کامپایلر haskell و rust از همین binding ها استفاده میکنن چون کامپایلرشون به ترتیب با haskell و rust نوشته شده.