Zum Inhalt springen

Architektur

Phasis ist ein vierstufiger Tree-Walker mit einer opportunistischen Bytecode-VM. Der Datenfluss sieht so aus:

Code
JS text
  │
  ▼
┌──────────┐
│  Lexer   │  src/Lexer/
└────┬─────┘
     ▼ tokens
┌──────────┐
│  Parser  │  src/Parser/
└────┬─────┘
     ▼ AST nodes
┌──────────────┐
│ Interpreter  │  src/Runtime/
│  + Bytecode  │  src/Bytecode/
│      VM      │
└────┬─────────┘
     ▼ JsValue
PHP host

Jede Stufe ist eine PHP-Klasse mit einer kleinen Schnittstelle, die von der nächsten Stufe verwendet wird. Einen Link-Schritt gibt es nicht: Das Parsing erzeugt einen AST, den der Interpreter direkt durchläuft.

Lexer

src/Lexer/Lexer.php wandelt Quelltext in einen Strom von Token-Instanzen um. Jedes Token ist ein schreibgeschützter Struct mit Typ (TokenType-Enum mit rund 80 Fällen), Wert, Zeile und Spalte.

Der Lexer verarbeitet:

  • Zahlenliterale – dezimal, hexadezimal (0x), oktal (0o), binär (0b), exponentiell und mit numerischen Trennzeichen.
  • String-Literale – einfache und doppelte Anführungszeichen, alle Escape-Sequenzen sowie Template-Literale mit verschachtelten Ausdrücken.
  • Reguläre Ausdrucksliterale – vollständige /pattern/flags-Tokenisierung. Dabei wird berücksichtigt, ob ein / als regulärer Ausdruck oder als Division zu lesen ist.
  • Bezeichner, Schlüsselwörter und Satzzeichen.
  • Zeilen- und Blockkommentare, die als Trivia erhalten bleiben.
  • Unicode-Quelltext – UTF-8-Bytes, die als JS-Quelltext in UTF-16 interpretiert werden.

Die Ausgabe ist ein Generator, der Tokens bei Bedarf liefert. Der Parser liest sie mit einem Token Vorausblick.

Parser

src/Parser/Parser.php ist ein handgeschriebener Pratt-Parser. Die Operatorrangfolge von Ausdrücken ist in src/Parser/Precedence.php festgelegt. Anweisungen werden mit rekursivem Abstieg geparst.

Er erzeugt unveränderliche AST-Knoten aus src/Ast/. Jeder Knoten speichert seine Quellposition für Stack-Traces und Fehlermeldungen.

Beispiele für wichtige Strukturen:

  • Ausdruck: Literal, Identifier, BinaryExpression, UnaryExpression, AssignmentExpression, CallExpression, MemberExpression, ArrowFunction, ObjectExpression, ArrayExpression, ConditionalExpression, TemplateLiteral, SpreadElement, NewExpression, ThisExpression, SequenceExpression, TaggedTemplate, ClassExpression, YieldExpression, AwaitExpression.
  • Anweisung: BlockStatement, IfStatement, ForStatement, ForInStatement, ForOfStatement, WhileStatement, DoWhileStatement, SwitchStatement, ReturnStatement, ThrowStatement, TryStatement, BreakStatement, ContinueStatement, ExpressionStatement.
  • Deklaration: VariableDeclaration, FunctionDeclaration, ClassDeclaration, ImportDeclaration.
  • Muster (Destrukturierung): ArrayPattern, ObjectPattern, RestElement, AssignmentPattern.

Die automatische Semikolon-Einfügung wird gemäß Spezifikation im Parser (nicht im Lexer) umgesetzt.

Interpreter

src/Runtime/Interpreter.php ist der AST-Walker. Er besucht jeden Knoten und gibt einen JsValue zurück (oder ein Completion bei Anweisungen, die den Kontrollfluss ändern).

Wichtige unterstützende Klassen:

  • Environment (src/Runtime/Environment.php) — lexikalische Gültigkeitsbereichskette. Jeder Funktionsaufruf erzeugt eine neue Umgebung, die auf ihren lexikalischen Elternbereich verweist. var wird in den Funktionsbereich gehoben; let / const sind blockbezogen und unterliegen einer TDZ.
  • CallStack (src/Runtime/CallStack.php) — expliziter Stack mit Tiefenlimit. Funktions- und Methodenaufrufe legen Frames ab; Ausnahmen bauen sie wieder ab.
  • Completion (src/Runtime/Completion.php) — diskriminierte Union aus normal, return, throw, break und continue, ergänzt um einen optionalen Wert und ein Label.
  • Reference (src/Runtime/Reference.php) — Referenztyp gemäß Spezifikation für die Auswertung der linken Seite bei Zuweisungen und delete.

Werte

JavaScript-Werte sind PHP-Objekte unter src/Value/. Jedes implementiert die Schnittstelle JsValue und verfügt über eigene Typkonvertierungsmethoden (toNumber(), toString(), toBoolean()).

Klasse JS-Typ
JsUndefined undefined (Singleton)
JsNull null (Singleton)
JsBoolean true / false
JsNumber IEEE-754-Double; erhält NaN, ±Infinity, ±0
JsString UTF-16-Zeichenfolgensemantik
JsObject Basisobjekt mit Eigenschaftszuordnung und Prototypenkette
JsArray erweitert JsObject um Längenverwaltung
JsFunction Closure mit erfasstem Environment
JsSymbol Symbol-Primitive
JsBigInt beliebige Genauigkeit über bcmath
JsProxy alle 13 Traps, widerrufbar
JsPromise synchroner Executor + Microtask-Warteschlange
JsGenerator Dual-Modus: Frame-Snapshot (Bytecode-kompilierte Funktionskörper; siehe Bytecode-VM § Frame-Snapshot-Generatoren) mit PHP-Fiber-Fallback für yield*, try-with-yield und asynchrone Generatoren
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry Sammlungen
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray Binärdaten

Die Zahlenverarbeitung ist die häufigste Ursache für historische Fehler: NaN !== NaN muss wahr sein, -0 === 0 muss wahr sein, und ToNumber-Grenzfälle erfordern genaue Sorgfalt auf Bitebene. Die Klasse Spec/TypeConversion implementiert die Spezifikationsalgorithmen wortgetreu und wurde umfassend getestet.

Integrierte Bibliothek

src/BuiltIn/ stellt alle Standardbibliotheksobjekte bereit: Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console sowie die Web-Platform- und Fetch-Pakete (URL, Headers, Request, Response, Blob, FormData, vollständige Streams, EventTarget, AbortController/AbortSignal, DOMException, structuredClone, performance, TextEncoder/TextDecoder, atob/btoa), crypto + SubtleCrypto, WebSocket, XMLHttpRequest und die Ereignisschleife (setTimeout, setInterval, queueMicrotask, AsyncContext).

Jedes integrierte Objekt ist eine statische Methode install() einer Klasse. Der Engine ruft sie beim Erstellen in Abhängigkeitsreihenfolge auf. Jedes definiert seine Prototypenkette, installiert Methoden als JsFunction-Instanzen, die in PHP-Closures verpackt sind, und stellt den Konstruktor global bereit.

PHP↔JS-Brücke

Die Wertkonvertierung über die Grenze hinweg findet an zwei Stellen statt:

  • Phasis\Interop\PhpToJs::convert() — Wert von PHP zu JsValue, verwendet für setGlobal(), call()-Argumente und Rückgabewerte von Host-Funktionen. Ein aufrufbares PHP wird zu einem JsFunction, dessen Rumpf es aufruft.
  • PhpToJs::toPhp() — von JsValue zu PHP für die Argumente einer Host-Funktion.
  • Engine::toPhp() (privat) — von JsValue zu PHP für die Ergebnisse von eval() und call().

Es gibt keine Objektbrücke: PHP-Objekte, die nicht aufrufbar sind, werden zu undefined (Issue #18). Die Klasse JsToPhp in src/Bytecode/ hat nichts mit Interoperabilität zu tun; sie ist der weiter unten beschriebene Transpiler. Die Konvertierungsregeln sind unter Wertkonvertierung dokumentiert.

Fehlerbehandlung

Ein nicht abgefangener JS-throw wird zu einer PHP Phasis\Exceptions\JsThrowable; vom Engine selbst ausgelöste Fehler werden als Phasis\Exceptions\TypeError, RangeError oder ReferenceError ausgegeben, allesamt Unterklassen von Phasis\Exceptions\RuntimeError. Innerhalb der Engine fängt ein JS-try / catch JsThrowable, jedes RuntimeError und Phasis\Exceptions\SyntaxError ab. Andere PHP-Ausnahmen, auch solche aus Host-Funktionen, werden nicht konvertiert und direkt an den PHP-Aufrufer weitergegeben (Issue #20).

Es gibt zwei Limits: die Obergrenze des Aufrufstacks (CallStack::maxDepth, 4,096 Frames plus eine 8,192-Frame-Schranke für transpilierte Closures) und maxLoopIterations, festgelegt über Engine::setLimit(), die nur der Tree-Walker durchsetzt (Issue #19). Beide lösen Phasis\Exceptions\InternalError aus. Es gibt keine Limits für Zeichenfolgenlänge, Ausgabegröße oder Ausführungszeit.

Bytecode-VM

Häufig aufgerufene Funktionen werden auf zwei Arten umgewandelt. JsToPhp (in src/Bytecode/) erzeugt eine PHP-Closure für Funktionskörper mit vielen Zahlen oder lokalen Variablen, die der Tracing-JIT von PHP nativ kompilieren kann. Die Pipeline wandelt new Ctor(args)-, obj.method(args)- und this.member-Zugriffe um, sodass das typische Muster aus Klassenmethode in Schleife die VM vollständig umgehen kann. Alles andere wird zu flachem Bytecode kompiliert, den VM::execute verarbeitet. Dabei hält ein benutzerdefinierter Aufrufstack jeden JS-Aufruf in einem einzigen PHP-Frame, und ein Inline-Cache pro PC beschleunigt Zugriffe auf Prototypmethoden. Einfache Generator-Funktionskörper werden ebenfalls zu Bytecode kompiliert; dabei erfasst Op::YIELD einen Frame-Snapshot im Heap, statt eine PHP-Fiber-Grenze zu überqueren.

Alle Formen, die der Compiler nicht umwandelt (e.g, with, try mit neu gebundenem Catch, yield*, asynchrone Generatoren), fallen auf den Tree-Walker zurück. Den vollständigen Ablauf findest du unter Bytecode-VM.