Компиляторы
Написание интерпретатора
Написать интерпретатор нового языка с нуля - это несколько сотен строк кода. Thorsten Ball написал книгу об этом за один уикенд. Ruby был интерпретатором на C в 1996 и был медленным - но это не помешало ему изменить веб-разработку через Rails. Интерпретатор - это кратчайший путь от идеи языка до работающего прототипа, и понять как он устроен значит понять как работают Python, Ruby, Lua изнутри.
- **Lua в Nginx (OpenResty)**: tree-walking интерпретатор Lua 5.1 обрабатывает 100k+ req/s для конфигурационной логики - скорость интерпретатора не bottleneck при правильной архитектуре
- **Rhai (Rust скриптовый язык)**: встраиваемый интерпретатор для игровых движков и конфигурации; tree-walking с ~80 наносекунд на простую операцию - достаточно для game scripting
- **Python CPython eval loop**: `ceval.c` - центральный dispatch loop CPython; каждый Python байткод - case в switch. Понимание этого кода открывает все тайны CPython performance
Tree-Walking Interpreter
Tree-walking interpreter рекурсивно обходит AST и вычисляет значения узлов на ходу. Простейший дизайн: функция `eval(node, env)` принимает AST-узел и окружение (переменные), возвращает значение. Это подход Ruby MRI до 1.9, Python CPython (частично), Lua 5.1 reference interpreter. Преимущество - простота; недостаток - медленнее байткод-интерпретатора в 3-10x.
Книга 'Writing An Interpreter In Go' (Thorsten Ball, 2016) - популярный туториал по tree-walking interpreters. Monkey language из книги реализован в Go за ~2000 строк. Ruby MRI до версии 1.8 использовал tree-walking подход; в 1.9 YARV (Yet Another Ruby VM) заменил его байткод-интерпретатором с 3x ускорением.
Почему tree-walking interpreter медленнее байткод-интерпретатора?
Environment Chains
Environment (окружение) - структура данных хранящая привязки имён к значениям. Chain environments: каждый вложенный scope создаёт новый environment с указателем на родительский. Поиск переменной: проверить текущий -> parent -> parent -> global. Это реализация lexical scoping.
Scheme, Python, JavaScript, Lua - все используют lexical scoping через environment chains. Dynamic scoping (Emacs Lisp до lexical-binding, bash) - переменная ищется в call stack, не в lexical scope. Первый - предсказуемее для программиста; второй - проще реализовать без closures. Python оптимизирует это: локальные переменные функций хранятся в массиве, а не в словаре.
Что такое 'lexical scoping' в контексте environment chains?
Closures
Closure - функция вместе с captured environment: ссылка на окружение в котором функция была определена. При вызове closure исполняется в своём captured environment, а не в текущем. Это позволяет функциям 'запоминать' переменные из enclosing scope даже после того как enclosing функция завершилась.
V8 closure optimization: если переменная из enclosing scope не изменяется - она копируется в closure (flat closure), иначе - shared heap cell. Python closures используют 'cell objects' для mutable captured variables. Rust closures выбирают capture mode: `Fn` (shared ref), `FnMut` (exclusive ref), `FnOnce` (move) - компилятор выводит автоматически.
Почему closure содержит ссылку на environment, а не его копию?
Error Handling в интерпретаторе
Runtime errors в интерпретаторе: undeclared variable, type mismatch, stack overflow (бесконечная рекурсия), division by zero. Два подхода: exceptions (бросать ошибку и раскручивать стек) или Result type (возвращать Either<Value, Error>). Хорошая диагностика требует location info: строка и столбец источника каждого AST-узла.
Dart использует 'zones' для structured error handling в async интерпретаторе. Lua implementации (LuaJIT, PUC-Lua) используют setjmp/longjmp для error propagation без C++ exceptions. Python Traceback объект строится инкрементально при раскрутке стека. Rust-based интерпретаторы (например, Rhai скриптовый язык) используют Result<Value, EvalAltResult> без исключений.
Tree-walking interpreter достаточно медленный чтобы быть бесполезным для production
Ruby MRI, Python CPython (частично), PHP (ранние версии) и многие production языки использовали tree-walking; для I/O bound задач скорость интерпретатора часто не bottleneck
Если язык управляет базой данных или HTTP запросами, 90% времени - в I/O. Интерпретатор быстрее или медленнее на 3x не заметен. Tree-walking interpreter в production оправдан если скорость разработки языка важнее его производительности
Почему location info (строка/столбец) в AST узлах важна для runtime errors?
Итоги
- Tree-walking interpreter: `eval(node, env)` рекурсивно обходит AST; проще байткод-VM, медленнее в 3-10x из-за pointer chasing
- Environment chain реализует lexical scoping: каждый scope = новый env с parent; lookup идёт вверх по цепочке
- Closure = function + capturedEnv (ссылка, не копия); location info в каждом AST узле критична для понятных runtime errors
Связанные темы
Интерпретатор - практическое применение всех компиляторных концепций:
- Написание компилятора — Компилятор добавляет к интерпретатору этап кодогенерации вместо прямого вычисления AST
- JIT-компиляция основы — JIT - это интерпретатор который компилирует горячий код: сначала tree-walking, потом нативный
- DSL компиляторы — Многие DSL реализуются как простые интерпретаторы поверх AST основного языка
Вопросы для размышления
- Python CPython использует байткод-VM вместо tree-walking. Какие конкретно операции ускоряются больше всего при переходе к байткод-интерпретатору?
- Closure захватывает ссылку на environment. Что происходит с памятью когда closure живёт дольше создавшей её функции? Как это влияет на GC?
- Tail call optimization (TCO) позволяет бесконечную рекурсию без переполнения стека. Как реализовать TCO в tree-walking интерпретаторе?