در حال بارگذاری 0%
// منو.exe
خانه رزومه بلاگ تماس سفارش
English فارسی
~/blog / programming / build-a-small-programming-language

چطور یک زبان برنامه‌نویسی کوچک بسازیم؟ از چند کاراکتر تا کدی که واقعاً اجرا می‌شود

فرض کنید فایلی با پسوند .mini ساخته‌ایم و داخل آن نوشته‌ایم: «عدد ۷ را بگیر، آن را تا ۱ کاهش بده و در هر مرحله حاصل را ضرب کن». بعد دستور اجرا را می‌زنیم و کامپیوتر عدد 5040 را چاپ می‌کند. نه Python این دستورها را می‌شناسد، نه CPU چیزی درباره let و while می‌داند. پس چه چیزی باعث می‌شود چند کاراکتر ساده ناگهان به یک برنامه تبدیل شوند؟ در این مقاله پاسخ را فقط توضیح نمی‌دهیم؛ یک زبان برنامه‌نویسی کوچک اما واقعی را از صفر می‌سازیم.

چطور یک زبان برنامه‌نویسی کوچک بسازیم؟ از چند کاراکتر تا کدی که واقعاً اجرا می‌شود

# یک فایل متنی چطور به «برنامه» تبدیل می‌شود؟

این برنامه را ببینید:
factorial.mini — MiniLang
// محاسبه 7!
let n = 7;
let result = 1;
 
while n > 1 {
    result = result * n;
    n = n - 1;
}
 
if result == 5040 {
    print result;
} else {
    print 0;
}
$ python minilang.py factorial.mini
5040
فایل بالا فقط متن است. درون آن هیچ دستور Python یا کد ماشین وجود ندارد. معنای let، اولویت * نسبت به + و رفتار حلقهٔ while را ما تعریف می‌کنیم. اگر برنامه‌ای بتواند این قواعد را بخواند و اجرا کند، صاحب یک زبان برنامه‌نویسی شده‌ایم؛ هرچند زبانمان هنوز کوچک باشد.
نکته کلیدی: زبان برنامه‌نویسی فقط مجموعه‌ای از کلمات کلیدی نیست. هر زبان دو قرارداد اصلی دارد: Syntax تعیین می‌کند چه برنامه‌هایی از نظر شکل معتبرند و Semantics تعیین می‌کند آن برنامه‌های معتبر چه معنایی دارند.

# مفسر می‌سازیم یا کامپایلر؟

این دو واژه اغلب به‌جای هم استفاده می‌شوند، اما یکسان نیستند. کامپایلر معمولاً برنامه را به شکل دیگری—برای مثال کد ماشین، Bytecode یا یک زبان میانی—تبدیل می‌کند. مفسر برنامه را می‌خواند و رفتار آن را اجرا می‌کند. مرز میان این دو همیشه کاملاً سخت نیست؛ برای مثال بسیاری از زبان‌های امروزی ابتدا Bytecode تولید می‌کنند و بعد آن را با ماشین مجازی یا JIT اجرا می‌کنند.
Interpreter

ساختار برنامه را می‌خواند و معنای آن را اجرا می‌کند.

Source → AST → Result
Compiler

برنامه را به نمایش دیگری مانند کد ماشین یا IR تبدیل می‌کند.

Source → Target Code
پروژهٔ ما یک Tree-Walk Interpreter است: کد منبع را به یک درخت معنایی تبدیل می‌کنیم و مفسر روی آن درخت حرکت می‌کند. بنابراین در این مقاله کد ماشین، LLVM IR، Bytecode، بهینه‌ساز یا JIT نمی‌سازیم. با این حال Lexer، Parser، AST و قواعد اجرایی ما واقعی‌اند و همان ایده‌هایی را نشان می‌دهند که در پیاده‌سازی زبان‌های بزرگ‌تر هم وجود دارند.
هشدار ساده‌سازی: «زبان کوچک» به معنی «اسباب‌بازی تقلبی» نیست؛ یعنی عمداً ویژگی‌هایی مانند Function، String، Scope، Module، Type Checker، Garbage Collector و کد ماشین را فعلاً کنار گذاشته‌ایم تا هستهٔ کار قابل مشاهده بماند.

# نقشهٔ راه زبان ما

مسیر اجرای هر فایل MiniLang از شش ایستگاه عبور می‌کند:
1
Source — متن فایل .mini
2
Lexer — ساخت Tokenها
3
Parser — بررسی Grammar
4
AST — ساخت درخت معنایی
5
Interpreter — اجرای درخت
Output — نتیجه یا خطا
اگر یک کاراکتر ناشناخته وارد شود، Lexer متوقف می‌شود. اگر ترتیب Tokenها با Grammar سازگار نباشد، Parser خطا می‌دهد. اگر برنامه از متغیری استفاده کند که وجود ندارد یا بر صفر تقسیم کند، خطا هنگام اجرا رخ می‌دهد. این تفکیک مهم است: همهٔ خطاها از یک مرحله نمی‌آیند.

# اول قرارداد زبان را مشخص کنیم

قبل از نوشتن Lexer باید تصمیم بگیریم چه چیزی در زبان معتبر است. MiniLang ما این قابلیت‌ها را خواهد داشت:
01
Value
عدد صحیح، عدد اعشاری و Booleanهای true و false
02
Expression
محاسبات، مقایسه، پرانتز و عملگرهای یک‌عملوندی
03
State
تعریف متغیر با let و تغییر مقدار آن
04
Control Flow
شرط if/else و حلقهٔ while
05
I/O
نمایش مقدار با دستور print
هر دستور ساده با ; تمام می‌شود. بدنهٔ شرط و حلقه داخل { } قرار می‌گیرد. شرط‌ها به پرانتز اجباری نیاز ندارند. متغیرها در این نسخه همگی در یک محیط سراسری نگهداری می‌شوند؛ یعنی Block Scope نداریم.

# مرحله اول — Lexer: متن را به Token تبدیل کنیم

Lexer یا Tokenizer هنوز نمی‌داند برنامه چه معنایی دارد. وظیفه‌اش فقط این است که رشتهٔ خام را به قطعات معنی‌دار تقسیم کند. برای مثال:
tokens.txt
Source:
let x = 10 + 2;
 
Tokens:
LET  IDENT(x)  =  NUMBER(10)  +  NUMBER(2)  ;  EOF
فاصله‌ها و کامنت‌ها برای Parser اهمیتی ندارند، پس Lexer آن‌ها را کنار می‌گذارد. اما عدد 10 فقط متن نیست؛ Lexer مقدار عددی آن را هم ذخیره می‌کند. همچنین let ابتدا شبیه یک Identifier خوانده می‌شود و بعد جدول Keywordها مشخص می‌کند که یک کلمهٔ رزروشده است.
این بخش اول فایل minilang.py است. برای کوتاه و قابل‌فهم ماندن پروژه، شناسه‌ها را با حرف انگلیسی یا _ آغاز می‌کنیم، کامنت‌ها با // شروع می‌شوند و شمارهٔ خط هر Token را برای پیام‌های خطا نگه می‌داریم:
minilang.py — بخش ۱: Token و Lexer
from __future__ import annotations
 
from dataclasses import dataclass, field
import re
import sys
from typing import Any
 
 
@dataclass(frozen=True)
class Token:
    type: str
    text: str
    value: Any
    line: int
 
 
TOKEN_PATTERN = re.compile(
    r"""
    (?P   \d+(?:\.\d+)? )
  | (?P    [A-Za-z_]\w*   )
  | (?P  //[^\n]*        )
  | (?P       ==|!=|<=|>=|[+\-*/=!<>] )
  | (?P    [(){};]         )
  | (?P  \n             )
  | (?P    [ \t\r]+       )
  | (?P    .            )
    """,
    re.VERBOSE,
)
 
KEYWORDS = {
    "let": "LET",
    "print": "PRINT",
    "if": "IF",
    "else": "ELSE",
    "while": "WHILE",
    "true": "TRUE",
    "false": "FALSE",
}
 
 
def tokenize(source: str) -> list[Token]:
    tokens: list[Token] = []
    line = 1
 
    for match in TOKEN_PATTERN.finditer(source):
        kind = match.lastgroup
        text = match.group()
 
        if kind == "NEWLINE":
            line += 1
        elif kind in {"SPACE", "COMMENT"}:
            continue
        elif kind == "NUMBER":
            value = float(text) if "." in text else int(text)
            tokens.append(Token("NUMBER", text, value, line))
        elif kind == "IDENT":
            tokens.append(Token(KEYWORDS.get(text, "IDENT"), text, None, line))
        elif kind in {"OP", "PUNCT"}:
            tokens.append(Token(text, text, None, line))
        else:
            raise SyntaxError(f"line {line}: unexpected character {text!r}")
 
    tokens.append(Token("EOF", "", None, line))
    return tokens
Lexer فرق میان = و == را با انتخاب الگوی بلندتر می‌فهمد. ترتیب الگوها مهم است: اگر = زودتر تطبیق داده می‌شد، عبارت == ممکن بود اشتباه به دو Token جدا تقسیم شود.

# مرحله دوم — Grammar: چه ترتیبی معتبر است؟

داشتن Token کافی نیست. این دو رشته تقریباً Tokenهای یکسانی دارند، اما فقط اولی یک تعریف متغیر معتبر است:
معتبر
let answer = 42;
نامعتبر
let = answer 42;
Grammar ترتیب مجاز Tokenها را تعریف می‌کند. Grammar کوچک ما به‌شکل زیر است:
grammar.ebnf
program     → statement* EOF ;
 
statement   → "let" IDENT "=" expression ";"
            | IDENT "=" expression ";"
            | "print" expression ";"
            | "if" expression block ("else" block)?
            | "while" expression block ;
 
block       → "{" statement* "}" ;
 
expression  → equality ;
equality    → comparison (("==" | "!=") comparison)* ;
comparison  → term ((">" | ">=" | "<" | "<=") term)* ;
term       → factor (("+" | "-") factor)* ;
factor     → unary (("*" | "/") unary)* ;
unary      → ("!" | "-") unary | primary ;
primary     → NUMBER | "true" | "false" | IDENT
            | "(" expression ")" ;
در این نشانه‌گذاری، | یعنی «یکی از این حالت‌ها»، علامت ? یعنی بخش اختیاری و * یعنی صفر یا چند بار تکرار. این * علامت ضرب زبان ما نیست؛ فقط بخشی از نشانه‌گذاری Grammar است.
نکتهٔ مهم‌تر، ترتیب لایه‌های Expression است. factor عملگرهای ضرب و تقسیم را می‌خواند، در حالی که term جمع و تفریق را از Factorها می‌سازد. بنابراین در عبارت 2 + 3 * 4 ابتدا 3 * 4 یک زیرساختار می‌شود و بعد با ۲ جمع می‌شود. اولویت عملگرها نتیجهٔ یک if مخفی نیست؛ مستقیماً داخل Grammar نوشته شده است.

# مرحله سوم — AST: شکل برنامه را نگه داریم، نه شکل متن را

Parser می‌تواند Tokenها را فقط تأیید کند، اما برای اجرا به ساختاری نیاز داریم که رابطهٔ اجزای برنامه را نگه دارد. این ساختار Abstract Syntax Tree یا AST است. «Abstract» یعنی جزئیاتی مانند فاصله، خط جدید، کامنت و حتی پرانتز اضافی کنار گذاشته می‌شوند و فقط ساختار مؤثر باقی می‌ماند.
برای عبارت let result = 2 + 3 * 4; درخت مفهومی چنین سلسله‌مراتبی دارد:
ROOT
Let: result
تعریف متغیر و نگهداری Expression مقدار اولیه
+
Binary: 2 + (...)
فرزند چپ: ۲ — فرزند راست: عبارت ضرب
*
Binary: 3 * 4
ضرب پایین‌تر از جمع قرار گرفته و زودتر ارزیابی می‌شود
برای ساده ماندن کد، همهٔ انواع گره را با یک کلاس عمومی Node نمایش می‌دهیم. در یک کامپایلر بزرگ معمولاً کلاس‌های جداگانه‌ای مانند BinaryExpression، VariableDeclaration و WhileStatement دیده می‌شود؛ اما مدل عمومی برای پروژهٔ ما کافی است:
minilang.py — بخش ۲: AST Node
@dataclass
class Node:
    kind: str
    value: Any = None
    children: list[Node] = field(default_factory=list)
    line: int = 0

# مرحله چهارم — Parser: از Tokenها درخت بسازیم

Parser ما از روش Recursive Descent استفاده می‌کند؛ یعنی تقریباً برای هر Rule در Grammar یک تابع داریم. تابع statement نوع دستور را تشخیص می‌دهد و تابع block تا رسیدن به } دستورها را جمع می‌کند:
minilang.py — بخش ۳: Statement Parser
class Parser:
    def __init__(self, tokens: list[Token]):
        self.tokens = tokens
        self.current = 0
 
    def parse(self) -> list[Node]:
        program: list[Node] = []
        while not self.check("EOF"):
            program.append(self.statement())
        return program
 
    def statement(self) -> Node:
        if self.match("LET"):
            name = self.consume("IDENT", "expected variable name")
            self.consume("=", "expected '=' after variable name")
            value = self.expression()
            self.consume(";", "expected ';' after declaration")
            return Node("let", name.text, [value], name.line)
 
        if self.match("PRINT"):
            value = self.expression()
            self.consume(";", "expected ';' after value")
            return Node("print", children=[value], line=self.previous().line)
 
        if self.match("IF"):
            line = self.previous().line
            condition = self.expression()
            then_branch = Node("block", children=self.block(), line=line)
            else_branch = (
                Node("block", children=self.block(), line=line)
                if self.match("ELSE")
                else Node("block", children=[], line=line)
            )
            return Node("if", children=[condition, then_branch, else_branch], line=line)
 
        if self.match("WHILE"):
            line = self.previous().line
            condition = self.expression()
            body = Node("block", children=self.block(), line=line)
            return Node("while", children=[condition, body], line=line)
 
        if self.check("IDENT") and self.peek_next().type == "=":
            name = self.advance()
            self.advance()  # consume '='
            value = self.expression()
            self.consume(";", "expected ';' after assignment")
            return Node("assign", name.text, [value], name.line)
 
        token = self.peek()
        raise SyntaxError(f"line {token.line}: expected a statement")
 
    def block(self) -> list[Node]:
        self.consume("{", "expected '{' before block")
        statements: list[Node] = []
        while not self.check("}") and not self.check("EOF"):
            statements.append(self.statement())
        self.consume("}", "expected '}' after block")
        return statements
بخش Expression دقیقاً از روی لایه‌های Grammar ساخته می‌شود. هر تابع فقط عملگرهای سطح خودش را مصرف می‌کند و برای Operand به سطح قوی‌تر بعدی می‌رود:
minilang.py — بخش ۴: Expression Parser
    def expression(self) -> Node:
        return self.equality()
 
    def equality(self) -> Node:
        node = self.comparison()
        while self.match("==", "!="):
            operator = self.previous()
            node = Node("binary", operator.text, [node, self.comparison()], operator.line)
        return node
 
    def comparison(self) -> Node:
        node = self.term()
        while self.match(">", ">=", "<", "<="):
            operator = self.previous()
            node = Node("binary", operator.text, [node, self.term()], operator.line)
        return node
 
    def term(self) -> Node:
        node = self.factor()
        while self.match("+", "-"):
            operator = self.previous()
            node = Node("binary", operator.text, [node, self.factor()], operator.line)
        return node
 
    def factor(self) -> Node:
        node = self.unary()
        while self.match("*", "/"):
            operator = self.previous()
            node = Node("binary", operator.text, [node, self.unary()], operator.line)
        return node
 
    def unary(self) -> Node:
        if self.match("!", "-"):
            operator = self.previous()
            return Node("unary", operator.text, [self.unary()], operator.line)
        return self.primary()
 
    def primary(self) -> Node:
        if self.match("NUMBER"):
            token = self.previous()
            return Node("literal", token.value, line=token.line)
        if self.match("TRUE", "FALSE"):
            token = self.previous()
            return Node("literal", token.type == "TRUE", line=token.line)
        if self.match("IDENT"):
            token = self.previous()
            return Node("variable", token.text, line=token.line)
        if self.match("("):
            node = self.expression()
            self.consume(")", "expected ')' after expression")
            return node
        token = self.peek()
        raise SyntaxError(f"line {token.line}: expected expression")
 
    def match(self, *types: str) -> bool:
        if self.peek().type in types:
            self.advance()
            return True
        return False
 
    def consume(self, token_type: str, message: str) -> Token:
        if self.check(token_type):
            return self.advance()
        token = self.peek()
        raise SyntaxError(f"line {token.line}: {message}; got {token.text!r}")
 
    def check(self, token_type: str) -> bool:
        return self.peek().type == token_type
 
    def advance(self) -> Token:
        token = self.peek()
        if token.type != "EOF":
            self.current += 1
        return token
 
    def peek(self) -> Token:
        return self.tokens[self.current]
 
    def peek_next(self) -> Token:
        return self.tokens[min(self.current + 1, len(self.tokens) - 1)]
 
    def previous(self) -> Token:
        return self.tokens[self.current - 1]
چرا حلقه داخل Parser؟ عبارت 10 - 3 - 2 باید به‌صورت (10 - 3) - 2 ساخته شود. حلقهٔ تابع term گره قبلی را هر بار فرزند چپ گره جدید می‌کند و همین موضوع چپ‌همبستگی عملگرها را می‌سازد.

# مرحله پنجم — Interpreter: به درخت معنا بدهیم

تا اینجا برنامه را خوانده‌ایم، اما هنوز هیچ کاری انجام نشده است. AST فقط می‌گوید برنامه چه ساختاری دارد. مفسر باید برای هر نوع Node یک رفتار تعریف کند:
let
Define — مقدار را در Environment ذخیره کن
+
Evaluate — دو فرزند را حساب و جمع کن
if
Branch — فقط شاخهٔ مناسب را اجرا کن
while
Repeat — شرط را دوباره ارزیابی کن
Environment در نسخهٔ ما یک Dictionary ساده است. دستور let x = 10; مقدار ۱۰ را با کلید x ذخیره می‌کند. هر بار که Node متغیر x ارزیابی شود، مفسر مقدار فعلی را از همان Dictionary می‌خواند. اگر نام وجود نداشته باشد، به‌جای حدس زدن مقدار، خطای Runtime می‌دهیم.
minilang.py — بخش ۵: Tree-Walk Interpreter
class Interpreter:
    def __init__(self):
        self.environment: dict[str, Any] = {}
 
    def run(self, program: list[Node]) -> None:
        for statement in program:
            self.execute(statement)
 
    def execute(self, node: Node) -> None:
        if node.kind == "let":
            if node.value in self.environment:
                self.fail(node, f"variable {node.value!r} is already defined")
            self.environment[node.value] = self.evaluate(node.children[0])
        elif node.kind == "assign":
            if node.value not in self.environment:
                self.fail(node, f"undefined variable {node.value!r}")
            self.environment[node.value] = self.evaluate(node.children[0])
        elif node.kind == "print":
            print(self.stringify(self.evaluate(node.children[0])))
        elif node.kind == "block":
            for statement in node.children:
                self.execute(statement)
        elif node.kind == "if":
            branch = node.children[1] if self.truthy(self.evaluate(node.children[0])) else node.children[2]
            self.execute(branch)
        elif node.kind == "while":
            while self.truthy(self.evaluate(node.children[0])):
                self.execute(node.children[1])
        else:
            self.fail(node, f"unknown statement {node.kind!r}")
 
    def evaluate(self, node: Node) -> Any:
        if node.kind == "literal":
            return node.value
        if node.kind == "variable":
            if node.value not in self.environment:
                self.fail(node, f"undefined variable {node.value!r}")
            return self.environment[node.value]
        if node.kind == "unary":
            right = self.evaluate(node.children[0])
            if node.value == "!":
                return not self.truthy(right)
            self.require_numbers(node, right)
            return -right
        if node.kind == "binary":
            left = self.evaluate(node.children[0])
            right = self.evaluate(node.children[1])
 
            if node.value == "==":
                return left == right
            if node.value == "!=":
                return left != right
 
            self.require_numbers(node, left, right)
            operations = {
                "+": lambda: left + right,
                "-": lambda: left - right,
                "*": lambda: left * right,
                "/": lambda: left / right,
                ">": lambda: left > right,
                ">=": lambda: left >= right,
                "<": lambda: left < right,
                "<=": lambda: left <= right,
            }
            if node.value == "/" and right == 0:
                self.fail(node, "division by zero")
            return operations[node.value]()
        self.fail(node, f"unknown expression {node.kind!r}")
 
    @staticmethod
    def truthy(value: Any) -> bool:
        return value if isinstance(value, bool) else value != 0
 
    @staticmethod
    def stringify(value: Any) -> str:
        if isinstance(value, bool):
            return "true" if value else "false"
        if isinstance(value, float) and value.is_integer():
            return str(int(value))
        return str(value)
 
    @staticmethod
    def require_numbers(node: Node, *values: Any) -> None:
        if any(isinstance(v, bool) or not isinstance(v, (int, float)) for v in values):
            Interpreter.fail(node, "operand must be a number")
 
    @staticmethod
    def fail(node: Node, message: str) -> None:
        raise RuntimeError(f"line {node.line}: {message}")
در شرط‌ها، false و عدد صفر نادرست هستند و true و عدد غیرصفر درست محسوب می‌شوند. عملگرهای حسابی فقط عدد می‌پذیرند؛ چون در Python مقدار Boolean زیرکلاس int است، عمداً Boolean را جداگانه رد می‌کنیم تا عبارتی مانند true + 1 در MiniLang به‌شکل تصادفی معتبر نشود.
در حلقهٔ while شرط باید در هر تکرار دوباره ارزیابی شود. اگر مقدار شرط را فقط یک بار قبل از حلقه حساب می‌کردیم، تغییر متغیر n داخل بدنه هیچ اثری نداشت و حلقهٔ فاکتوریل هرگز تمام نمی‌شد.

# مرحله ششم — قطعات را به هم وصل کنیم

تابع run همان Pipeline مقاله را در سه خط اجرا می‌کند: Tokenize، Parse و Interpret. بخش Command Line نیز فایل ورودی را با UTF-8 می‌خواند و خطاهای قابل انتظار را بدون نمایش یک Traceback طولانی گزارش می‌کند:
minilang.py — بخش ۶: Runner
def run(source: str) -> None:
    tokens = tokenize(source)
    program = Parser(tokens).parse()
    Interpreter().run(program)
 
 
if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("usage: python minilang.py program.mini")
    try:
        with open(sys.argv[1], encoding="utf-8") as source_file:
            run(source_file.read())
    except (OSError, SyntaxError, RuntimeError) as error:
        print(f"MiniLang error: {error}", file=sys.stderr)
        raise SystemExit(1) from error
حالا شش بخش بالا را به‌ترتیب در یک فایل به نام minilang.py قرار دهید و برنامهٔ ابتدای مقاله را در factorial.mini ذخیره کنید:
terminal — اجرای واقعی
$ python minilang.py factorial.mini
5040
خروجی ۵۰۴۰ از هیچ تابع آمادهٔ فاکتوریلی نیامده است. Lexer کاراکترها را Token کرده، Parser از آن‌ها AST ساخته، Interpreter متغیرهای n و result را ایجاد کرده، شرط حلقه را چند بار سنجیده، ضرب و انتساب را انجام داده و در پایان شاخهٔ درست if را اجرا کرده است.

# اگر برنامه اشتباه باشد چه می‌شود؟

یک زبان فقط با مسیر موفق تعریف نمی‌شود؛ محل و نوع شکست هم بخشی از طراحی آن است. MiniLang سه خانوادهٔ اصلی خطا دارد:
LEX
کاراکتر ناشناخته
print @; — Lexer نمی‌تواند @ را Token کند
PARSE
ساختار نامعتبر
let x = 10 — علامت ; جا افتاده است
RUN
خطای زمان اجرا
print 1 / 0; — ساختار معتبر است، اما اجرا ممکن نیست
broken.mini — output
$ python minilang.py broken.mini
MiniLang error: line 1: division by zero
توجه کنید که تقسیم بر صفر را Parser رد نمی‌کند. عبارت 1 / 0 از نظر Grammar کاملاً معتبر است؛ مشکل فقط زمانی آشکار می‌شود که Interpreter بخواهد آن را اجرا کند. در مقابل، جا افتادن ; قبل از اجرا و در مرحله Parse تشخیص داده می‌شود.

# این زبان هنوز چه چیزهایی ندارد؟

MiniLang اکنون یک زبان قابل اجراست، اما هنوز برای ساخت نرم‌افزار واقعی مناسب نیست. محدودیت‌هایش را باید صریح بدانیم:
آنچه واقعاً ساخته‌ایم

Lexer، Grammar، Recursive-Descent Parser، AST، متغیر، محاسبه، شرط، حلقه، خروجی و خطا.

آنچه عمداً نساخته‌ایم

String، Function، Scope، Array، Module، Type Checker، Bytecode، Optimizer، Garbage Collector و Native Code.

حتی Environment ما Block Scope ندارد؛ بنابراین متغیر تعریف‌شده داخل if در بیرون آن هم باقی می‌ماند. همچنین مرحلهٔ Semantic Analysis جداگانه‌ای نداریم و خطاهایی مانند متغیر تعریف‌نشده هنگام اجرا شناسایی می‌شوند. این‌ها باگ پنهان نیستند؛ تصمیم‌های ساده‌ساز پروژه‌اند.
برای رشد زبان می‌توانیم هر ویژگی را در چهار نقطه اضافه کنیم: Tokenهای لازم در Lexer، Rule جدید در Grammar، Node مناسب در AST و رفتار آن در Interpreter. برای مثال افزودن Function نیازمند تعریف Syntax پارامترها و فراخوانی، ساخت Nodeهای Function/Call، ایجاد Environment محلی و مدیریت return است.

# چرا ساختن یک زبان کوچک ارزش دارد؟

وقتی فقط از یک زبان استفاده می‌کنیم، کلمات کلیدی و عملگرها بدیهی به نظر می‌رسند. تصور می‌کنیم کامپیوتر «طبیعتاً» می‌داند ضرب باید قبل از جمع انجام شود، while یعنی تکرار و نام متغیر به کجا اشاره می‌کند. اما هیچ‌کدام طبیعی نیستند؛ همه قراردادهایی‌اند که سازندهٔ زبان تعریف و پیاده‌سازی کرده است.
ساخت MiniLang نشان می‌دهد یک زبان از جادو ساخته نشده است. Lexer متن را نام‌گذاری می‌کند. Parser رابطهٔ Tokenها را می‌فهمد. AST جزئیات ظاهری را کنار می‌گذارد. Interpreter روی ساختار حرکت می‌کند و به هر Node معنا می‌دهد. اگر بخواهیم قدم بعدی را برداریم، می‌توانیم همین AST را به‌جای اجرای مستقیم، به Bytecode یا LLVM IR تبدیل کنیم؛ آنجا مسیر ما از مفسر درختی به سمت کامپایلر تغییر می‌کند.
جذاب‌ترین بخش ماجرا شاید این باشد: کامپیوتر هیچ‌وقت کلمهٔ while را نفهمید. ما برنامه‌ای ساختیم که توافق کرد هنگام دیدن آن چه رفتاری انجام دهد. زبان برنامه‌نویسی در اصل قراردادی دقیق میان انسان و یک پیاده‌سازی است—قراردادی که اگر کوچک‌ترین ابهامی داشته باشد، برنامه دیگر قابل پیش‌بینی نخواهد بود.
takeaway.txt
کامپیوتر هیچ‌وقت کلمهٔ while را نفهمید؛
فقط برنامه‌ای که ما نوشتیم، هنگام دیدنش توافق کرد چه کاری انجام دهد.
زبان برنامه‌نویسی یعنی همین:
قراردادی دقیق که به متن ساده، معنا می‌دهد.