Архитектура
Phasis е четиристепенен обход на дървото с VM за байткод, използвана при възможност. Потокът от данни е:
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 double; запазва NaN, ±Infinity, ±0 |
JsString |
семантика на низовете UTF-16 |
JsObject |
базов обект с карта на свойствата и верига на прототипите |
JsArray |
разширява JsObject с проследяване на дължината |
JsFunction |
затваряне със захванат Environment |
JsSymbol |
примитив Symbol |
JsBigInt |
произволна точност чрез bcmath |
JsProxy |
всичките 13 капана, с възможност за отмяна |
JsPromise |
синхронен изпълнител + опашка за микрозадачи |
JsGenerator |
двоен режим: генератори със снимка на рамката (компилирани до байткод тела; вижте Bytecode VM § Генератори със снимка на рамката) с резервен вариант чрез PHP Fiber за yield*, try-with-yield и асинхронни генератори |
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry |
колекции |
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray |
двоични данни |
Обработката на числа е най-големият източник на исторически грешки: NaN !== NaN трябва да е true, -0 === 0 трябва да е true, а граничните случаи на 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, пълни потоци, 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 при типичния шаблон с метод на клас в цикъл. Всичко останало се компилира до плосък байткод, изпълняван от VM::execute, с персонализиран стек за извиквания, който държи всяко извикване на JS в една рамка PHP и кеш за вграждане на четенията на методи от прототипа за всяка PC позиция. Телата на простите генератори също се компилират до байткод, като Op::YIELD улавя снимка на рамка в купа памет вместо да преминава през PHP Fiber.
Всеки шаблон, който компилаторът не може да преобразува (e.g. with, try с повторно обвързване на catch, yield*, асинхронни генератори), преминава към обхождащото дърво. Вижте Виртуална машина за байткод за пълното описание на механизма.