Архитектура
Phasis — это четырехэтапный обход дерева с виртуальной машиной байт-кода, включаемой при подходящих условиях. Поток данных:
JS text
│
▼
┌──────────┐
│ Lexer │ src/Lexer/
└────┬─────┘
▼ tokens
┌──────────┐
│ Parser │ src/Parser/
└────┬─────┘
▼ AST nodes
┌──────────────┐
│ Interpreter │ src/Runtime/
│ + Bytecode │ src/Bytecode/
│ VM │
└────┬─────────┘
▼ JsValue
PHP host
Каждый этап — класс PHP с небольшим интерфейсом, который использует следующий этап. Этапа связывания нет: парсер создает AST, а интерпретатор обходит его напрямую.
Лексер
src/Lexer/Lexer.php преобразует исходный текст в поток экземпляров Token. Каждый токен — неизменяемая структура: тип (перечисление TokenType, около 80 вариантов), значение, строка, столбец.
Лексер обрабатывает:
- Числовые литералы: десятичные, шестнадцатеричные (
0x), восьмеричные (0o), двоичные (0b), экспоненциальные, с разделителями разрядов. - Строковые литералы: одинарные и двойные кавычки, все escape-последовательности, шаблонные литералы с вложенными выражениями.
- Литералы регулярных выражений: полная токенизация
/pattern/flagsс учетом того, когда/должно трактоваться как регулярное выражение, а когда — как деление. - Идентификаторы, ключевые слова, знаки пунктуации.
- Комментарии в строке и блоке, сохраняемые как вспомогательные элементы.
- Исходный код Unicode: байты UTF-8 интерпретируются как исходный код JS UTF-16.
Результат — генератор, который лениво выдает токены. Парсер считывает их с предварительным просмотром одного токена.
Парсер
src/Parser/Parser.php — написанный вручную парсер Pratt. Приоритет операторов выражений задается в src/Parser/Precedence.php. Для операторов используется отдельный рекурсивный нисходящий разбор.
Он создает неизменяемые узлы AST из src/Ast/. Каждый узел содержит расположение в исходном коде для трассировки стека и сообщений об ошибках.
Примечательные структуры:
- Выражения:
Literal,Identifier,BinaryExpression,UnaryExpression,AssignmentExpression,CallExpression,MemberExpression,ArrowFunction,ObjectExpression,ArrayExpression,ConditionalExpression,TemplateLiteral,SpreadElement,NewExpression,ThisExpression,SequenceExpression,TaggedTemplate,ClassExpression,YieldExpression,AwaitExpression. - Операторы:
BlockStatement,IfStatement,ForStatement,ForInStatement,ForOfStatement,WhileStatement,DoWhileStatement,SwitchStatement,ReturnStatement,ThrowStatement,TryStatement,BreakStatement,ContinueStatement,ExpressionStatement. - Объявления:
VariableDeclaration,FunctionDeclaration,ClassDeclaration,ImportDeclaration. - Шаблоны (деструктуризация):
ArrayPattern,ObjectPattern,RestElement,AssignmentPattern.
Автоматическая вставка точки с запятой реализована в парсере, а не в лексере, согласно спецификации.
Интерпретатор
src/Runtime/Interpreter.php — обходчик AST. Он посещает каждый узел и возвращает JsValue (или Completion для инструкций, меняющих поток управления).
Основные вспомогательные классы:
Environment(src/Runtime/Environment.php) — цепочка лексических областей видимости. Каждый вызов функции создает новое окружение, связанное с родительским лексическим окружением.varподнимается в область видимости функции;let/constограничены блоком и имеют TDZ.CallStack(src/Runtime/CallStack.php) — явный стек с ограничением глубины. Вызовы функций и методов добавляют кадры; исключения раскручивают стек.Completion(src/Runtime/Completion.php) — размеченное объединение изnormal,return,throw,break,continue, а также необязательного значения и метки.Reference(src/Runtime/Reference.php) — тип Reference из спецификации для вычисления левой части присваиваний иdelete.
Значения
Значения JavaScript — это объекты PHP под управлением src/Value/. Каждый реализует интерфейс JsValue и содержит собственные методы преобразования типов (toNumber(), toString(), toBoolean()).
| Класс | тип JS |
|---|---|
JsUndefined |
undefined (синглтон) |
JsNull |
null (синглтон) |
JsBoolean |
true / false |
JsNumber |
число с плавающей точкой IEEE 754; сохраняет NaN, ±Infinity, ±0 |
JsString |
семантика строк UTF-16 |
JsObject |
базовый объект с картой свойств и цепочкой прототипов |
JsArray |
наследует JsObject и отслеживает длину |
JsFunction |
замыкание с захваченным Environment |
JsSymbol |
примитив Symbol |
JsBigInt |
произвольная точность через bcmath |
JsProxy |
все 13 ловушек, допускает отзыв |
JsPromise |
синхронный исполнитель + очередь микрозадач |
JsGenerator |
два режима: снимок кадра (скомпилированные в байткод тела; см. Глава «Виртуальная машина байткода § Генераторы со снимком кадра») с резервным переходом на PHP Fiber для yield*, try-with-yield и асинхронных генераторов |
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry |
коллекции |
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray |
двоичные данные |
Обработка чисел — главный источник ошибок, оставшихся с прежних версий: условие NaN !== NaN должно быть истинным, условие -0 === 0 должно быть истинным, а пограничные случаи ToNumber требуют точности до бита. Класс Spec/TypeConversion дословно реализует алгоритмы спецификации и прошел исчерпывающие тесты.
Встроенная библиотека
src/BuiltIn/ предоставляет все объекты стандартной библиотеки: Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console, а также пакеты Web Platform / Fetch (URL, Headers, Request, Response, Blob, FormData, полноценные Streams, EventTarget, AbortController/AbortSignal, DOMException, structuredClone, performance, TextEncoder/TextDecoder, atob/btoa), crypto + SubtleCrypto, WebSocket, XMLHttpRequest и цикл событий (setTimeout, setInterval, queueMicrotask, AsyncContext).
Каждый встроенный объект — это статический метод install() класса. При создании Engine вызывает их в порядке зависимостей. Каждый определяет свою цепочку прототипов, устанавливает методы как экземпляры JsFunction, обернутые в замыкания PHP, и предоставляет конструктор как глобальную сущность.
Мост PHP↔JS
Преобразование значений при пересечении границы выполняется в двух местах:
Phasis\Interop\PhpToJs::convert()— значение PHP преобразуется вJsValue; используется для аргументовsetGlobal(),call()и возвращаемых значений хост-функций. Функция PHP становитсяJsFunction, тело которого вызывает ее.PhpToJs::toPhp()—JsValueпреобразуется в PHP для аргументов хост-функции.Engine::toPhp()(приватный) —JsValueпреобразуется в PHP для результатовeval()иcall().
Моста для объектов нет: объекты PHP, которые не являются вызываемыми, преобразуются в undefined (проблема № 18). Класс JsToPhp в src/Bytecode/ не связан с взаимодействием систем; это транспилятор, описанный ниже. Правила преобразования описаны в разделе Преобразование значений.
Обработка ошибок
Неперехваченное исключение JS throw становится PHP Phasis\Exceptions\JsThrowable; ошибки, возникающие в самом движке, представлены как Phasis\Exceptions\TypeError, RangeError или ReferenceError — все они являются подклассами Phasis\Exceptions\RuntimeError. Внутри движка JS try / catch перехватывает JsThrowable, любые RuntimeError и Phasis\Exceptions\SyntaxError. Остальные исключения PHP, включая выброшенные хост-функциями, не преобразуются и напрямую передаются вызывающему коду на PHP (проблема № 20).
Есть два ограничения: предел глубины стека вызовов (CallStack::maxDepth, 4,096 кадров, плюс ограничение в 8,192 кадров для транспилированных замыканий) и maxLoopIterations, задаваемое через Engine::setLimit() и проверяемое только обходчиком дерева (проблема № 19). Оба вызывают Phasis\Exceptions\InternalError. Ограничений на длину строк, размер вывода или время выполнения нет.
Виртуальная машина байткода
Горячие функции преобразуются двумя способами. JsToPhp (в src/Bytecode/) создает замыкание PHP для тел с большим количеством операций над числами и локальными переменными, которое трассирующий JIT-компилятор PHP может скомпилировать в машинный код. Конвейер преобразует обращения к new Ctor(args), obj.method(args) и this.member, чего достаточно, чтобы типичный шаблон с методом класса в цикле полностью обходил виртуальную машину. Все остальное компилируется в плоский байткод, который выполняет VM::execute с пользовательским стеком вызовов: каждый вызов JS сохраняется в одном кадре PHP, а обращения к методам прототипа используют встроенный кэш для каждой точки выполнения. Простые тела генераторов тоже компилируются в байткод, а Op::YIELD сохраняет снимок кадра в куче вместо перехода через PHP Fiber.
Любая форма кода, которую компилятор не преобразует (e.g. with, try с повторным связыванием catch, yield*, асинхронные генераторы), передается обходчику дерева. Полный механизм описан в разделе Виртуальная машина байткода.