Architektura
Phasis to czteroetapowy walker drzewa z doraźną maszyną wirtualną kodu bajtowego. Przepływ danych wygląda tak:
JS text
│
▼
┌──────────┐
│ Lexer │ src/Lexer/
└────┬─────┘
▼ tokens
┌──────────┐
│ Parser │ src/Parser/
└────┬─────┘
▼ AST nodes
┌──────────────┐
│ Interpreter │ src/Runtime/
│ + Bytecode │ src/Bytecode/
│ VM │
└────┬─────────┘
▼ JsValue
PHP host
Każdy etap to klasa PHP o niewielkim interfejsie, z którego korzysta kolejny etap. Nie ma etapu linkowania — parsowanie tworzy AST, który interpreter przechodzi bezpośrednio.
Lekser
src/Lexer/Lexer.php przekształca tekst źródłowy w strumień instancji Token. Każdy token jest strukturą tylko do odczytu: typ (enum TokenType, ok. 80 wariantów), wartość, wiersz i kolumna.
Lekser obsługuje:
- Literały liczbowe — dziesiętne, szesnastkowe (
0x), ósemkowe (0o), binarne (0b), wykładnicze oraz separatory liczbowe. - Literały tekstowe — pojedyncze i podwójne cudzysłowy, wszystkie sekwencje ucieczki, literały szablonów z zagnieżdżonymi wyrażeniami.
- Literały wyrażeń regularnych — pełna tokenizacja
/pattern/flagsz uwzględnieniem tego, czy/oznacza wyrażenie regularne, czy dzielenie. - Identyfikatory, słowa kluczowe i znaki interpunkcyjne.
- Komentarze (wierszowe i blokowe), zachowywane jako elementy pomocnicze.
- Źródło Unicode — bajty UTF-8 interpretowane jako źródło UTF-16 JS.
Wynikiem jest generator, który leniwie zwraca tokeny. Parser odczytuje je z wyprzedzeniem o jeden token.
Parser
src/Parser/Parser.php to napisany ręcznie parser Pratt. Priorytety wyrażeń są zdefiniowane w src/Parser/Precedence.php. Instrukcje obsługuje własny parser rekurencyjny typu descent.
Tworzy niezmienne węzły AST z src/Ast/. Każdy węzeł zawiera lokalizację w źródle, używaną w śladach stosu i raportach błędów.
Charakterystyczne struktury:
- Wyrażenie:
Literal,Identifier,BinaryExpression,UnaryExpression,AssignmentExpression,CallExpression,MemberExpression,ArrowFunction,ObjectExpression,ArrayExpression,ConditionalExpression,TemplateLiteral,SpreadElement,NewExpression,ThisExpression,SequenceExpression,TaggedTemplate,ClassExpression,YieldExpression,AwaitExpression. - Instrukcja:
BlockStatement,IfStatement,ForStatement,ForInStatement,ForOfStatement,WhileStatement,DoWhileStatement,SwitchStatement,ReturnStatement,ThrowStatement,TryStatement,BreakStatement,ContinueStatement,ExpressionStatement. - Deklaracja:
VariableDeclaration,FunctionDeclaration,ClassDeclaration,ImportDeclaration. - Wzorzec (destrukturyzacja):
ArrayPattern,ObjectPattern,RestElement,AssignmentPattern.
Automatyczne wstawianie średników jest zaimplementowane w parserze (nie w lekserze), zgodnie ze specyfikacją.
Interpreter
src/Runtime/Interpreter.php przechodzi AST. Odwiedza każdy węzeł i zwraca JsValue (lub Completion dla instrukcji zmieniających przepływ sterowania).
Najważniejsze klasy pomocnicze:
Environment(src/Runtime/Environment.php) — łańcuch zakresów leksykalnych. Każde wywołanie funkcji tworzy nowe środowisko wskazujące na nadrzędny zakres leksykalny.varsą wynoszone do zakresu funkcji;let/constmają zakres blokowy i podlegają TDZ.CallStack(src/Runtime/CallStack.php) — jawny stos z limitem głębokości. Wywołania funkcji i metod odkładają ramki; wyjątki je zdejmują.Completion(src/Runtime/Completion.php) — unia rozłączna obejmującanormal,return,throw,break,continueoraz opcjonalną wartość i etykietę.Reference(src/Runtime/Reference.php) — typ Reference ze specyfikacji, służący do ewaluacji lewej strony przypisań idelete.
Wartości
Wartości JavaScript to obiekty PHP w ramach src/Value/. Każdy z nich implementuje interfejs JsValue i ma własne metody konwersji typów (toNumber(), toString(), toBoolean()).
| Klasa | Typ JS |
|---|---|
JsUndefined |
undefined (singleton) |
JsNull |
null (singleton) |
JsBoolean |
true / false |
JsNumber |
liczba zmiennoprzecinkowa IEEE 754; zachowuje NaN, ±Infinity, ±0 |
JsString |
semantyka ciągów UTF-16 |
JsObject |
obiekt bazowy z mapą właściwości i łańcuchem prototypów |
JsArray |
rozszerza JsObject o śledzenie długości |
JsFunction |
domknięcie z przechwyconym Environment |
JsSymbol |
typ prymitywny Symbol |
JsBigInt |
dowolna precyzja dzięki bcmath |
JsProxy |
wszystkie 13 pułapek, z możliwością odwołania |
JsPromise |
synchroniczny executor + kolejka zadań mikroprocesowych |
JsGenerator |
dwa tryby: migawka ramki (ciała skompilowane do kodu bajtowego; zob. Generatory z migawką ramki maszyny wirtualnej kodu bajtowego) z awaryjnym użyciem PHP Fiber dla yield*, try-with-yield i asynchronicznych generatorów |
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry |
kolekcje |
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray |
dane binarne |
Obsługa liczb jest głównym historycznym źródłem błędów: NaN !== NaN musi być prawdziwe, -0 === 0 musi być prawdziwe, a przypadki brzegowe ToNumber wymagają dokładności na poziomie bitów. Klasa Spec/TypeConversion implementuje algorytmy ze specyfikacji dosłownie i została gruntownie przetestowana.
Wbudowana biblioteka
src/BuiltIn/ udostępnia wszystkie obiekty biblioteki standardowej: Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console, a także pakiety Web Platform / Fetch (URL, Headers, Request, Response, Blob, FormData, pełny Streams, EventTarget, AbortController/AbortSignal, DOMException, structuredClone, performance, TextEncoder/TextDecoder, atob/btoa), crypto + SubtleCrypto, WebSocket, XMLHttpRequest oraz pętlę zdarzeń (setTimeout, setInterval, queueMicrotask, AsyncContext).
Każda funkcja wbudowana jest statyczną metodą install() klasy. Podczas tworzenia Engine wywołuje je w kolejności zależności. Każda definiuje łańcuch prototypów, instaluje metody jako instancje JsFunction opakowane wokół domknięć PHP i udostępnia konstruktor jako zmienną globalną.
Most PHP↔JS
Konwersja wartości na granicy odbywa się w dwóch miejscach:
Phasis\Interop\PhpToJs::convert()— wartość PHP naJsValue; używane dla argumentówsetGlobal(),call()i wartości zwracanych przez funkcje hosta. Wywoływalny obiekt PHP staje sięJsFunction, którego ciało go wywołuje.PhpToJs::toPhp()—JsValuena PHP dla argumentów funkcji hosta.Engine::toPhp()(prywatne) —JsValuena PHP dla wynikóweval()icall().
Nie ma mostu dla obiektów: obiekty PHP, które nie są wywoływalne, są konwertowane na undefined (zgłoszenie #18). Klasa JsToPhp w src/Bytecode/ nie ma związku z interoperacyjnością; to transpiler opisany poniżej. Zasady konwersji opisano w sekcji Konwersja wartości.
Obsługa błędów
Nieprzechwycony JS throw staje się PHP Phasis\Exceptions\JsThrowable; błędy zgłaszane przez sam silnik są prezentowane jako Phasis\Exceptions\TypeError, RangeError lub ReferenceError, wszystkie będące podklasami Phasis\Exceptions\RuntimeError. Wewnątrz silnika JS try / catch przechwytuje JsThrowable, dowolne RuntimeError i Phasis\Exceptions\SyntaxError. Inne wyjątki PHP, w tym rzucane przez funkcje hosta, nie są konwertowane i propagują się bezpośrednio do wywołującego PHP (zgłoszenie #20).
Istnieją dwa limity: maksymalna głębokość stosu wywołań (CallStack::maxDepth, 4,096 ramek oraz ograniczenie do 8,192 ramek dla transpile'owanych domknięć) i maxLoopIterations, ustawiany przez Engine::setLimit() i egzekwowany tylko przez walker drzewa (zgłoszenie #19). Przekroczenie obu limitów powoduje rzucenie Phasis\Exceptions\InternalError. Nie ma limitów długości ciągu, rozmiaru danych wyjściowych ani czasu wykonania.
Maszyna wirtualna kodu bajtowego
Często wykonywane funkcje są obniżane na dwa sposoby. JsToPhp (w src/Bytecode/) emituje domknięcie PHP dla ciał intensywnie korzystających z liczb lub zmiennych lokalnych, które śledzący JIT PHP może skompilować do kodu natywnego; potok obniża odczyty new Ctor(args), obj.method(args) i this.member, co wystarcza, by typowy wzorzec metody klasy w pętli całkowicie ominął maszynę wirtualną. Pozostałe przypadki są kompilowane do płaskiego kodu bajtowego, przetwarzanego przez VM::execute, z własnym stosem wywołań, który utrzymuje każde wywołanie JS w pojedynczej ramce PHP, oraz pamięcią podręczną inline dla odczytów metod prototypu, indeksowaną według PC. Proste ciała generatorów również są kompilowane do kodu bajtowego; Op::YIELD przechwytuje migawkę ramki na stercie zamiast przechodzić przez Fiber PHP.
Każda postać, której kompilator nie obniża (e.g. with, try z ponownym przypisaniem w catch, yield*, generatory asynchroniczne), wraca do walkera drzewa. Pełny opis mechanizmu znajdziesz w sekcji Maszyna wirtualna kodu bajtowego.