Przejdź do treści

Architektura

Phasis to czteroetapowy walker drzewa z doraźną maszyną wirtualną kodu bajtowego. Przepływ danych wygląda tak:

Kod
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/flags z 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. var są wynoszone do zakresu funkcji; let / const mają 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ąca normal, return, throw, break, continue oraz opcjonalną wartość i etykietę.
  • Reference (src/Runtime/Reference.php) — typ Reference ze specyfikacji, służący do ewaluacji lewej strony przypisań i delete.

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 na JsValue; używane dla argumentów setGlobal(), call() i wartości zwracanych przez funkcje hosta. Wywoływalny obiekt PHP staje się JsFunction, którego ciało go wywołuje.
  • PhpToJs::toPhp() — JsValue na PHP dla argumentów funkcji hosta.
  • Engine::toPhp() (prywatne) — JsValue na PHP dla wyników eval() i call().

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.