Architektur
Phasis ist ein vierstufiger Tree-Walker mit einer opportunistischen Bytecode-VM. Der Datenfluss sieht so aus:
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.varwird in den Funktionsbereich gehoben;let/constsind 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 ausnormal,return,throw,breakundcontinue, 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 unddelete.
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 zuJsValue, verwendet fürsetGlobal(),call()-Argumente und Rückgabewerte von Host-Funktionen. Ein aufrufbares PHP wird zu einemJsFunction, dessen Rumpf es aufruft.PhpToJs::toPhp()— vonJsValuezu PHP für die Argumente einer Host-Funktion.Engine::toPhp()(privat) — vonJsValuezu PHP für die Ergebnisse voneval()undcall().
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.