Перейти к содержимому

Архитектура

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*, асинхронные генераторы), передается обходчику дерева. Полный механизм описан в разделе Виртуальная машина байткода.