Vai al contenuto

Architettura

Phasis è un tree walker a quattro fasi con una VM bytecode opportunistica. Il flusso dei dati è:

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

Ogni fase è una classe PHP con un'interfaccia ridotta, utilizzata dalla fase successiva. Non esiste una fase di collegamento: il parsing produce un AST che l'interprete percorre direttamente.

Lexer

src/Lexer/Lexer.php converte il testo sorgente in una sequenza di istanze Token. Ogni token è una struttura di sola lettura: tipo (enumerazione TokenType, circa 80 casi), valore, riga e colonna.

Il lexer gestisce:

  • Letterali numerici: decimali, esadecimali (0x), ottali (0o), binari (0b), esponenziali e separatori numerici.
  • Letterali stringa: apici singoli e doppi, tutte le sequenze di escape e template literal con espressioni annidate.
  • Letterali di espressioni regolari: tokenizzazione completa /pattern/flags, distinguendo quando un / deve essere interpretato come espressione regolare o divisione.
  • Identificatori, parole chiave e punteggiatura.
  • Commenti su riga e a blocco, conservati come trivia.
  • Sorgente Unicode: byte UTF-8 interpretati come sorgente UTF-16 JS.

L'output è un generatore che produce token in modo differito. Il parser li consuma con un token di lookahead.

Parser

src/Parser/Parser.php è un parser Pratt scritto a mano. La precedenza delle espressioni è definita in src/Parser/Precedence.php. Le istruzioni usano una propria discesa ricorsiva.

Produce nodi AST immutabili da src/Ast/. Ogni nodo include la posizione nel sorgente per le stack trace e la segnalazione degli errori.

Strutture degne di nota:

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

L'inserimento automatico del punto e virgola è implementato nel parser, non nel lexer, come previsto dalle specifiche.

Interprete

src/Runtime/Interpreter.php percorre l'AST. Visita ogni nodo e restituisce un JsValue (oppure un Completion per le istruzioni che modificano il flusso di controllo).

Classi di supporto principali:

  • Environment (src/Runtime/Environment.php) — catena degli scope lessicali. Ogni chiamata di funzione crea un nuovo ambiente che punta al genitore lessicale. var viene sollevato allo scope della funzione; let / const hanno scope di blocco e una TDZ.
  • CallStack (src/Runtime/CallStack.php) — stack esplicito con limite di profondità. Le chiamate a funzioni e metodi aggiungono frame; le eccezioni li rimuovono.
  • Completion (src/Runtime/Completion.php) — unione discriminata di normal, return, throw, break, continue, più un valore e un'etichetta facoltativi.
  • Reference (src/Runtime/Reference.php) — tipo Reference della specifica per la valutazione del lato sinistro nelle assegnazioni e in delete.

Valori

I valori JavaScript sono oggetti PHP sotto src/Value/. Ognuno implementa l'interfaccia JsValue e dispone dei propri metodi di conversione del tipo (toNumber(), toString(), toBoolean()).

Classe tipo JS
JsUndefined undefined (singleton)
JsNull null (singleton)
JsBoolean true / false
JsNumber double IEEE 754; conserva NaN, ±Infinity, ±0
JsString semantica delle stringhe UTF-16
JsObject oggetto base con mappa delle proprietà e catena dei prototipi
JsArray estende JsObject con il tracciamento della lunghezza
JsFunction closure con Environment acquisito
JsSymbol primitivo Symbol
JsBigInt precisione arbitraria tramite bcmath
JsProxy tutti i 13 trap, revocabile
JsPromise esecutore sincrono + coda di microtask
JsGenerator doppia modalità: snapshot del frame (corpi compilati in bytecode; vedi Generatori con snapshot del frame in Bytecode VM) con fallback a PHP Fiber per yield*, try-with-yield e generatori asincroni
JsMap, JsSet, JsWeakMap, JsWeakSet, JsWeakRef, JsFinalizationRegistry raccolte
JsArrayBuffer, JsSharedArrayBuffer, JsDataView, JsTypedArray dati binari

La gestione dei numeri è la principale fonte storica di bug: NaN !== NaN deve essere true, -0 === 0 deve essere true e i casi limite di ToNumber richiedono un'attenzione precisa a livello di bit. La classe Spec/TypeConversion implementa alla lettera gli algoritmi della specifica ed è sottoposta a test esaustivi.

Libreria integrata

src/BuiltIn/ fornisce tutti gli oggetti della libreria standard: Array, String, Object, Math, JSON, Date, RegExp, Map, Set, Promise, Proxy, Reflect, Symbol, BigInt, TypedArray, Temporal, Intl, console, oltre ai pacchetti Web Platform / Fetch (URL, Headers, Request, Response, Blob, FormData, Streams completo, EventTarget, AbortController/AbortSignal, DOMException, structuredClone, performance, TextEncoder/TextDecoder, atob/btoa), crypto + SubtleCrypto, WebSocket, XMLHttpRequest e il ciclo di eventi (setTimeout, setInterval, queueMicrotask, AsyncContext).

Ogni elemento integrato è un metodo statico install() di una classe. Engine li chiama in ordine di dipendenza durante la costruzione. Ognuno definisce la propria catena di prototipi, installa i metodi come istanze JsFunction racchiuse in closure PHP ed espone il costruttore come globale.

Bridge PHP↔JS

La conversione dei valori attraverso il confine avviene in due punti:

  • Phasis\Interop\PhpToJs::convert() — valore PHP in JsValue, usato per gli argomenti di setGlobal(), call() e i valori restituiti dalle funzioni host. Un callable PHP diventa un JsFunction il cui corpo lo chiama.
  • PhpToJs::toPhp() — JsValue in PHP per gli argomenti di una funzione host.
  • Engine::toPhp() (privato) — JsValue in PHP per i risultati di eval() e call().

Non esiste un bridge per gli oggetti: gli oggetti PHP che non sono callable vengono convertiti in undefined (problema #18). La classe JsToPhp in src/Bytecode/ non è correlata all'interoperabilità: è il transpiler descritto di seguito. Le regole di conversione sono documentate in Conversione dei valori.

Gestione degli errori

Un JS throw non intercettato diventa un PHP Phasis\Exceptions\JsThrowable; gli errori generati direttamente dal motore vengono segnalati come Phasis\Exceptions\TypeError, RangeError o ReferenceError, tutte sottoclassi di Phasis\Exceptions\RuntimeError. All'interno del motore, un JS try / catch intercetta JsThrowable, qualsiasi RuntimeError e Phasis\Exceptions\SyntaxError. Le altre eccezioni PHP, incluse quelle generate dalle funzioni host, non vengono convertite e si propagano direttamente al chiamante PHP (problema #20).

Esistono due limiti: il tetto dello stack delle chiamate (CallStack::maxDepth, 4,096 frame, più una protezione di 8,192 frame per le closure transpiled) e maxLoopIterations, impostato tramite Engine::setLimit() e applicato solo dal tree-walker (problema #19). Entrambi generano Phasis\Exceptions\InternalError. Non ci sono limiti alla lunghezza delle stringhe, alle dimensioni dell'output o al tempo di esecuzione.

VM bytecode

Le funzioni più usate vengono trasformate in due modi. JsToPhp (in src/Bytecode/) genera una closure PHP per i corpi con molte operazioni numeriche o variabili locali, che il JIT di tracciamento di PHP può compilare in codice nativo; la pipeline trasforma le letture di new Ctor(args), obj.method(args) e this.member, quanto basta per consentire al modello canonico di un metodo di classe in un ciclo di bypassare completamente la VM. Tutto il resto viene compilato in bytecode lineare, eseguito da VM::execute con uno stack di chiamate personalizzato che mantiene ogni chiamata JS all'interno di un unico frame PHP e una cache inline per PC per le letture di metodi dei prototipi. Anche i corpi semplici dei generatori vengono compilati in bytecode: Op::YIELD acquisisce uno snapshot del frame heap invece di passare attraverso PHP Fiber.

Qualsiasi struttura che il compilatore non trasforma (e.g. with, try con un catch che ridefinisce la variabile, yield*, generatori asincroni) ricade nel tree-walker. Per il meccanismo completo, vedi VM bytecode.