آموزش ساخت یک زبان برنامه نویسی - بخش دوم
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 8 دقیقه

آموزش ساخت یک زبان برنامه نویسی - بخش دوم

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

چرا این کار به صورت سفارشی سازی شده بهتر است؟

در مورد lexer، واضح بود که من می‌خواهم از کد مختص خود استفاده کنم. یک lexer چنان برنامه ناچیزی است که عدم نوشتن lexer مختص خود، به مانند عدم نوشتن left-pad به دست خود است.

این مسئله در مورد parse کننده کمی متفاوت است. parse کننده Pinecore من در حال حاضر ۷۵۰ خط طول دارد، و من سه بار آن را نوشتم؛ زیرا دو مورد اول به درد نمی‌خوردند.

در اصل من تصمیم خود را به چند علت گرفتم، و با این که روند کار خیلی نرم پیش نرفته است، اکثر آن‌ها حقیقت دارند. علل اصلی، این موارد هستند:

  • کاهش جا به جایی زمینه در جریان کاری: جا به جایی زمینه بین C++ و Pinecore بدون وارد کردن قوائد Bison، به خودی خود به اندازه کافی بد است.
  • ساده نگه داشتن روند ساخت: هر زمان که قوائد تغییر می‌کنند، Bison باید قبل از ساخت اجرا شود. این کار می‌تواند خودکارسازی شود، اما وقتی که به جا به جایی میان سیستم‌های ساخت می‌رسیم، سخت می‌شود.
  • من ساختن چیزهای جالب را دوست دارم: من Pinecore را به علت این که فکر می‌کردم ساده است، نساختم؛ پس چرا وقتی که می‌توانم یک نقش مرکزی را خودم انجام دهم، آن را به دیگری واگذار کنم؟ یک parse کننده سفارشی ممکن است ناچیز نباشد، اما کاملا قابل انجام است.

در ابتدا کاملا مطمئن نبودم که یک مسیر قابل اعتماد را پیش می‌رفتم یا نه، اما توسط جمله‌ای از Walter Bright (توسعه دهنده‌ای در نسخه‌های اولیه C++ و خالق زبان D) روحیه گرفتم:

«من وقت خود را بر روی lexer یا مولدهای parse کننده هدر نمی‌دهم. نوشتن یک lexer و parse کننده  درصد کوچکی از کار نوشتن یک کمپایلر است. استفاده از یک مولد تقریبا به اندازه نوشتن با یک دست زمان خواهد بود، و مولدها هم شهرت بزرگی در نمایش پیغام‌های خطای ناخوش‌آیند دارند.»

ساختار درختی actionها (Action Tree)

حال ما ناحیه اصطلاحات رایج و جهانی را رد کرده‌ایم. چیزی که من طبق درک خود به آن «ساختار درختی action» می‌گویم، بسیار وابسته به IR (Intermediate Representation = ارائه حد واسط) در LLVM است.

یک تفاوت نامحسوس، ولی بسیار قابل ملاحظه میان ساختار درختی action و ساختار درختی سینتکس چکیده (AST) وجود دارد. برای من مدتی زمان برد که حتی درک کنم اصلا تفاوتی میان آن‌ها وجود دارد.

Action Tree در مقابل AST

به زبان ساده، action tree همان AST‌ به همراه متن است. آن متن، اطلاعاتی مانند این است که یک تابع چه typeای را بر می‌گرداند، یا مثلا این که دو مکانی که یک متغیر در آن‌ها استفاده شده است، در واقع از متغیر مشابهی استفاده می‌کنند. از آنجایی که کدی که action tree را تولید می‌کند باید تمام این متن را در یابد و به یاد داشته باشد، نیاز به مقدار زیادی گردش در جداول و برخی موارد دیگر دارد.

اجرای Action Tree

پس از این که action tree را به دست آوردیم، اجرای کد ساده می‌شود. هر action node یک تابع اجرایی دارد که مقداری ورودی را می‌گیرد، هر کاری که باید را انجام می‌دهد (مثلا فراخوانی actionهای زیر مجموعه) و خروجی آن action را بر می‌گرداند. تفسیر کننده در عمل به این صورت است.

گزینه‌های کمپایل کردن

صبر کنید، من گفتم که: «Pinecore قرار است کمپایل شود.» بله، اما کمپایل کردن سخت‌تر از تفسیر کردن است. تعداد کمی رویکرد ممکن برای آن وجود دارند.

ساخت کمپایلر مختص خودم

در ابتدا این به نظر من یک ایده خوب می‌آمد. من از این که خودم همه چیز را بسازم خوشم می‌‌آید، و به دنبال بهانه‌ای بوده‌ام که اسمبلی را هم خوب یاد بگیرم.

متاسفانه، نوشتن یک کمپایلر قابل حمل به راحتی نوشتن مقداری کد ماشین برای هر عنصر زبان نیست. با توجه به تعداد بالای معماری‌ها و سیستم عامل‌ها، هر کسی نمی‌تواند یک backend کمپایلر میان پلتفرمی بنویسد.

حتی تیم‌های پشت Swift، Rust و Clang هم نمی‌خواهند به تنهایی به آن رسیدگی کنند، پس در عوض آن‌ها از این موارد استفاده می‌کنند:

LLVM

LLVM مجموعه‌ای از ابزار کمپایلر می‌باشد. LLVM اساسا یک کتابخانه است که زبان شما را تبدیل به یک عد دو دویی قابل اجرای کمپایل شده می‌کند. این مورد به نظر انتخابی بی نقص می‌آمد، پس من به سراغ آن رفتم. متاسفانه من عمق آب را بررسی نکردم و سریعا در آن غرق شدم.

LLVM با این یک زبان اسمبلی سخت نیست، اما از نظر پیچیدگی کتابخانه، سخت است. استفاده از آن غیر ممکن نیست و سازندگان آن آموزش‌های خوبی فراهم کرده‌اند، اما من پی بردم قبل از این که بتوانم یک کمپایلر Pinecore را به کلی با استفاده از آن پیاده‌سازی کنم، باید کمی تمرین کنم.

Transpile کردن

من نوعی Pinecore کمپایل شده را با سرعت بالا می‌خواستم، پس به سراغ متدی رفتم که می‌دانستم کار خواهد کرد: transpile کردن.

من یک transpiler برای Pinecore‌ به C++ نوشتم، و قابلیت کمپایل خودکار خروجی با استفاده از GCC را به آن اضافه کردم. در حال حاضر این برای تمام برنامه‌های Pinecore کار می‌کند. (گرچه در برخی موارد کم شکست می‌خورد) این یک راه حل قابل حمل یا مقیاس‌پذیر نیست، اما فعلا کار می‌کند.

آینده

با فرض این که من به توسعه دهی Pinecore ادامه دهم، بالاخره این زبان پشتیبانی کمپایل کردن LLVM را دریافت خواهد کرد. به نظر می‌رسد که اهمیت ندارد چقدر بر رویش کار کنم، این transpiler هیچ وقت کاملا با ثبات نخواهد بود و منفعت‌های LLVM بی شمار هستند. مسئله فقط این است که من کی برای ساخت برخی پروژه‌های نمونه در LLVM زمان خواهم داشت، تا بتوانم پیچ و خم آن را یاد بگیرم.

تا آن موقع، تفسیر کننده برای برنامه‌های ناچیز عالی است و transpiler‌ کردن C++ برای اکثر موارد که نیاز به کارایی بالاتری دارند، کار می‌کند.

نتیجه گیری

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

در اینجا نصیحت سطح بالای من برای شروع کار را مشاهده می‌‌کنید (به یاد داشته باشید که من این کار را کاملا درک نمی‌کنم، پس این موارد را با کمی مبالغه در نظر بگیرید):

  • اگر در شک قرار دارید، با ساخت یک زبان تفسیر شده پیش بروید. زبان‌های تفسیر شده عموما راحت‌تر طراحی، ساخته و یاد گرفته می‌شوند. اگر مطمئنید که می‌خواهید یک زبان کمپایل شده بسازید، من نمی‌خواهم شما را از آن دلسرد کنم، اما اگر در لبه گیر کرده‌اید، به سراغ زبان تفسیر شده بروید.
  • وقتی که به lexerها و parse‌ کننده‌ها می‌رسید، هر کاری که دوست دارید انجام دهید. استدلال‌های معتبری موافق و مخالف نوشتن یک مورد مختص خود وجود دارند. در نهایت، اگر درباره طراحی خود به خوبی فکر کنید و همه چیز را به روشی عاقلانه پیاده‌سازی کنید، واقعا خیلی مهم نیست که چه کار کنید.
  • از لوله‌کشی‌ای که من در نهایت با آن مواجه شدم یاد بگیرید. مقدار زیادی آزمایش و خطا به لوله‌کشی‌ای که من در حال حاضر دارم ختم شدند. من به از بین بردن ASTها، ASTهایی که در جای مناسب تبدیل به action treeها می‌شوند، و برخی ایده‌‌های بد دیگر فکر کرده‌ام. این لوله‌کشی به درستی کار می‌کند، پس اگر ایده بهتری ندارید آن را تغییر ندهید.

وقتی که به توسعه دهی Pinecore می‌رسم، خیلی پشیمانی‌های کمی دارم. من در طی مسیر چند تصمیم بد گرفتم، اما اکثر کد خود را تحت تاثیر اشتباهات این چنینی نوشته‌ام.

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

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
4.67 از 3 رای

/@er79ka

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید