برای هر یک از ما که هر روز در جایی از این دنیا بیدار می شویم و کد می نویسیم، اغلب خود را به زبان برنامه نویسی که بیشتر دوست داریم وابسته می بینیم. همه ما وقتی سعی میکنیم مرزهای خود را بشکنیم و سعی میکنیم چیز جدیدی یاد بگیریم، آن نقطه خاص را احساس میکنیم، زیرا این زمینه همیشه مانند یک جهان روی استروئیدها در حال گسترش و تکامل است.
برای کسانی که هنوز مبتدی هستند و سعی می کنند راه هایی برای درک نحوه عملکرد یک زبان برنامه نویسی بیابند، چه چیزی بهتر از تلاش برای ساختن زبان برنامه نویسی خود است؟ 🙂 این پست اساساً اثبات کوچکی از مفهوم است که در آن ما به سراغ یک زبان برنامه نویسی باطنی سرگرم کننده کوچک خواهیم رفت.
قبل از اینکه به نکات بسیار مهم داخل بپردازیم، اجازه دهید توضیح دهم که در اینجا چه خواهیم کرد. ما یک زبان برنامه نویسی کوچک می نویسیم که توسط ANTLR تجزیه و لکس می شود، به C# تبدیل می شود و کد ترجمه شده به Roslyn C# Script API وارد می شود.
اگر کلمات فوق الذکر برای شما بیش از حد لفظی به نظر می رسند، اجازه دهید آن را برای شما روشن کنم. مانند هر زبانی که هر روز برای صحبت از آن استفاده می کنیم، زبان برنامه نویسی نیز خود یک گرامر دارد. اگر یک خط کد در زندگی خود نوشته اید، باید مانند روز روشن باشد. هر زبان برنامه نویسی از یک ساختار بسیار تعریف شده پیروی می کند و یک رژه رقصنده از کلمات به دنبال آن است. بنابراین، از آنجایی که ما در حال ایجاد یکی هستیم، اولین چیزی که به آن گرامر برای زبان خود نیاز داریم. به جای اینکه این کار را از ابتدا انجام دهیم، ابزار انتخابی ما ANTLR است که مخفف "ابزار دیگری برای تشخیص زبان" است. ANTLR به ما کمک می کند تا گرامر را تعریف کنیم، lexer و تجزیه کننده آن را تولید کنیم و در نهایت می توانیم از آن مؤلفه ها برای انتقال کد خود به C# استفاده مجدد کنیم. به یاد داشته باشید که ANTLR از جاوا اسکریپت، جاوا و پایتون نیز پشتیبانی می کند. بنابراین،
اکنون، حتی قبل از شروع صحبت در مورد زبان ها و هر چیز دیگری، باید کمی در مورد نظریه کامپایلر بدانیم. اکنون، من می توانم وارد جزئیات ناخوشایند lexer و parser شوم، اما از آنجایی که این عصر گوگل است، اجازه می دهم این مسئولیت به دست شما بیفتد. ما تا حدودی از یک روش اکتشافی برای درک نحوه عملکرد همه چیز استفاده می کنیم و آن را از آنجا می گیریم.
بیایید فرض کنیم که زبانی مانند این داریم،
- derp a = 20 🙂 # Initialization
- # basic if-else
- a > 2 ???
- yep ->
- a = 5 🙂
- kbye
- dump a 🙂
اولین چیزی که نیاز داریم یک دستور زبان است. برای درک زبان برنامه نویسی به چیزی نیاز داریم که همه کلمات اینجا را بفهمد. و آن مرد لکسر است. یک lexer کل بلوک کدی را که ما به کامپایلر ارسال می کنیم می خورد، آن را به واژگان کوچک خرد می کند (رشته ها/کلمات را در اینجا بخوانید). بنابراین ما می توانیم بفهمیم که چه زمانی یک کلمه کلیدی خاص را زده ایم. مانند زبان ساختگی خود در اینجا، ما از زبان اینترنتی محبوب derp
برای مقداردهی اولیه یک متغیر استفاده کردیم.
کار بعدی که باید انجام دهید این است که گزیده ای معنادار از کلمات تهیه کنید. مانند زمانی که یک بلوک if را می نویسیم ، کامپایلر باید بداند چه رشته هایی از کاراکترها یا کلمات باید کنار هم قرار گیرند تا یک عبارت یا عبارت معنادار را بسازند. این کار تجزیه کننده است. اگر از نزدیک ببینید، خواهید دید که هر بلوک کد را می توان به صورت درختی بیان کرد. به عنوان مثال، بلوک مقداردهی اولیه در اینجا به شکل زیر است.
![](http://pezhvak24.ir/dl/10kcor/cscd/article/creating-a-nano-scripting-language-using-antlr-and-roslyn/Images/assignment.png)
این قطعاً یک درخت تک شاخه است، می گوید برای شروع عبارت باید کلمه derp را بنویسید و برای اتمام عبارت باید آن را با یک شکلک ( 🙂 ) پایان دهید. در وسط از یک شناسه (نام برای متغیر خود) و یک عملگر ASSIGN (=) برای تعریف جریان انتساب از راست به چپ استفاده می کنید. آن درخت در واقع مستقیماً از دستور زبانی که امروز می خواهیم استفاده کنیم ایجاد می شود.
بنابراین، چرا صبر کنید؟ بیایید نگاهی به دستور زبانی که قرار است امروز استفاده کنیم بیندازیم،
- grammar Profane;
- compilationUnit: statement* EOF;
- statement:
- printstmt
- | assignstmt
- | ifstmt
- | setstmt;
- printstmt : 'dump' expr? SMILEY;
- assignstmt : 'derp' ID ASSIGN expr SMILEY;
- setstmt : ID ASSIGN expr SMILEY;
- ifstmt :
- conditionExpr '???'
- 'yep ->'
- statement*
- 'kbye';
- conditionExpr: expr relop expr;
- expr: term | opExpression;
- opExpression: term op term;
- op: PLUS | ASSIGN | MINUS;
- relop: EQUAL | NOTEQUAL | GT | LT | GTEQ | LTEQ;
- term: ID | number | STRING;
- number: NUMBER;
- // Keywords
- ID: [a-zA-Z_] [a-zA-Z0-9_]*;
- SMILEY: ':)';
- WS: [ \n\t\r]+ -> skip;
- PLUS :'+';
- EQUAL : '====';
- ASSIGN : '=';
- NOTEQUAL: '!!==';
- MINUS : '-';
- GT : '>';
- LT : '<';
- GTEQ : '>=';
- LTEQ : '>=';
- fragment INT: [0-9]+;
- NUMBER: INT ('.'(INT)?)?;
- STRING: '"' (~('\n' | '"'))* '"';
اکنون، اولین توصیه من در اینجا این است که با اندازه گرامر گیج نشوید و شروع به برداشتن بیت هایی کنید که در ابتدا درک می کنیم. اگر به تصویر اول در این تصویر مراجعه کنید و به دستور زبان نگاهی بیندازید، خواهید دید که قانون assignstmt دقیقاً به همان شکلی که در تصویر نشان داده شده است تعریف شده است. اگر کلمات ID، ASSIGN و SMILEY را پیدا کنید، همچنین خواهید دید که همه آنها در دستور زبان زیر تعریف شده اند، جایی که شکل تحت اللفظی خود را دارند. این نشانه ها به تجزیه کننده کمک می کند تا آنچه را نوشته اید بفهمد. و assignstmt یک قانون نامیده می شود که روابط بین توکن ها یا قوانین مختلف را تعریف می کند. دیدن؟ اینگونه است که ما قوانینی را برای دستور زبان خود می سازیم. زیرا گرامر چیزی نیست جز مجموعه ای از قوانین به خوبی تعریف شده.