
# VM bytecode

Il percorso di esecuzione predefinito è un tree-walker: visita ogni nodo AST e restituisce un valore. Per le funzioni più usate, chiamate milioni di volte, il collo di bottiglia è il sovraccarico del dispatch per nodo. Phasis aggiunge un compilatore speculativo da bytecode a closure PHP per ridurre questo sovraccarico.

## Quando entra in funzione la VM

Ogni `JsFunction` ha un contatore delle chiamate frequenti. Quando supera una soglia (predefinita: 100), il motore tenta di compilare il corpo della funzione in una closure PHP tramite `src/Bytecode/JsToPhp.php`.

La compilazione riesce quando il corpo della funzione corrisponde a un insieme noto di strutture:

- Assegnazioni a variabili locali con tipi prevedibili (numerico, oggetto, funzione, array, stringa).
- Operazioni aritmetiche sui numeri.
- Letture di membri su variabili locali di tipo oggetto E su `this` (`this.v`, `obj.x`).
- Chiamate a metodi su variabili locali di tipo oggetto (`obj.method(args)`), con il ricevitore passato come `$thisValue`.
- Chiamate al costruttore `new Ctor(args)` — promuove il tipo della variabile locale LHS a `object` e protegge con Bailout un valore restituito non oggetto.
- Cicli `for` / `while` con condizioni semplici.
- `return` con un'espressione calcolabile.
- `if` / `else` con condizioni tipizzate.

La compilazione non riesce (e la funzione resta nel tree-walker) quando il corpo contiene:

- Istruzioni `with`.
- `try / catch / finally` (gestito dal tree-walker per garantire la corretta semantica di completamento).
- Chiamate `eval()`.
- Modifiche dirette a `arguments`.
- Qualsiasi costrutto che il compilatore non sappia ancora trasformare.

Quando la compilazione riesce, il motore sostituisce il percorso di chiamata della funzione con la closure compilata. Le chiamate successive eseguono codice nativo PHP con il sovraccarico di una sola chiamata di funzione, invece che con un sovraccarico per ogni nodo AST.

## Fallback

Il percorso veloce compilato si basa su alcune ipotesi (i numeri restano numeri, le proprietà mantengono la stessa struttura). Se un'ipotesi viene violata a runtime, la closure genera un `Bailout` e l'esecuzione riparte nel tree-walker per quella chiamata.

È qui che la correttezza rispetto alla specifica conta di più. Il tree-walker deve poter rieseguire da zero la chiamata per cui è scattato il fallback, senza rilevare effetti collaterali del percorso veloce non riuscito. Phasis gestisce questo caso in due modi:

1. **Prefissi puri** — il compilatore cerca gli effetti collaterali prima di qualsiasi speculazione. Se la funzione esegue I/O (console.log, funzioni host) prima del punto di speculazione, il percorso veloce non lo salta: lo conferma e prosegue.
2. **Rifiuto in fase di compilazione** — per le strutture che richiederebbero di annullare effetti collaterali osservabili (un `let r = fn()` non numerico quando `fn()` modifica lo stato), il compilatore rifiuta la compilazione. La funzione resta nel tree-walker.

Il caso che ha motivato questa soluzione: un'assegnazione non numerica esegue `fn()` nel percorso veloce, genera `Bailout` e poi viene rieseguita nel tree-walker, eseguendo `fn()` due volte con due serie di effetti collaterali. Il rifiuto in fase di compilazione impedisce del tutto a `fn()` di essere eseguito nel percorso veloce, così il tree-walker osserva l'unica chiamata.

## Dispatch della VM (il secondo livello)

Alcune funzioni vengono compilate in closure PHP; altre passano a un interprete bytecode ancora più semplice in `src/Bytecode/VM.php`. Il bytecode è un array lineare di opcode (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …) eseguiti da un dispatch `switch`.

La VM è destinata alle funzioni che non vale la pena compilare in una closure PHP, ma che sono comunque abbastanza usate da trarre vantaggio dall'evitare l'attraversamento dell'AST. Le chiamate ai metodi sugli oggetti integrati (Array.prototype.map, String.prototype.split, …) passano per percorsi veloci nella VM che aggirano il trampoline generico `callFunction`.

## Stack di chiamate personalizzato

`Op::CALL`, `Op::CALL_METHOD` e `Op::NEW_CALL` usano un percorso di chiamata inline che mantiene l'intero stack delle chiamate JS all'interno di `VM::execute`, senza un nuovo frame PHP per ogni chiamata JS. All'ingresso, lo stato del chiamante (pc, flusso di codice, stack degli operandi, variabili locali, env, `this`, flag strict-mode) viene inserito in un pool `savedFrames` per VM di istanze `SavedFrame`, e il ciclo di dispatch riassegna le variabili locali al chiamato. `Op::RET` estrae il frame salvato e ripristina lo stato. Le eccezioni risalgono i frame salvati prima di tornare al tree-walker.

L'idoneità viene memorizzata in cache su `JsFunction`: `vmInlineCallCache` per le chiamate normali (corpo compilato in BC, non associato a `bind()`, non nativo), `vmInlineConstructCache` per le espressioni `new` (inoltre `isConstructable`, non derivato, senza inizializzatori di campi di istanza o metodi privati). Tutto il resto continua a passare per il trampoline canonico `callFunction` / `vmNewExpression`.

Il vantaggio architetturale secondario: il limite dello stack PHP non limita più la profondità della ricorsione JS. Ora l'unico limite è `CallStack::maxDepth`, impostato su **4 096**: ben al di sopra di ogni caso legittimo test262 (la maggior parte richiede meno di 2 k frame) e abbastanza rigoroso da far fallire rapidamente i test di ricorsione infinita patologica entro il limite di timeout del runner. Il percorso delle closure transpiled (`JsToPhp`) ha una protezione separata di 8 frame 192 per il fallback non inline.

## Generatori con snapshot del frame

I corpi dei generatori semplici — senza `yield*`, senza `yield` all’interno di try/catch/finally, senza `await` — vengono compilati in bytecode. Il compilatore emette un nuovo opcode `Op::YIELD` nel punto in cui il corpo si sospende; `Op::YIELD` acquisisce lo stato di dispatch (cf, pc+1, stack, sp, locals, env, thisValue, strict) in un oggetto heap `GeneratorSnapshot` e restituisce da `VM::execute` un valore sentinella `YieldResult`. Il driver `JsGenerator` memorizza lo snapshot e lo passa a `VM::execute` al successivo `next()` / `throw()`, sostituendo il valore di ripresa in cima allo stack degli operandi.

Questo sostituisce il percorso PHP-Fiber per il sottoinsieme supportato. Il driver basato su Fiber (`executeGeneratorBody`) è ancora disponibile per le strutture rifiutate dal compilatore: delega `yield*`, generatori che eseguono yield all’interno di un blocco try, generatori asincroni, funzioni asincrone con `await`. `JsGenerator` è in modalità doppia: ogni `next()` / `throw()` / `return()` verifica se l’istanza dispone di uno snapshot o di un Fiber e instrada l’esecuzione di conseguenza.

Nessuna opzione regolabile tramite benchmark: l’idoneità viene decisa in fase di compilazione e memorizzata in cache su `JsFunction`. Nel percorso snapshot, il vantaggio è un cambio di contesto PHP in meno per ogni yield; il percorso Fiber resta invariato per tutto il resto.

## Cache inline

`Op::LOAD_MEMBER` mantiene una cache inline per PC, indicizzata per identità del prototipo e usata per il modello metodo-sul-prototipo sull’istanza (`c.sum(...)`, `arr.push(...)`, ogni dispatch di metodo di classe). La condizione di cache hit è che il ricevitore non abbia uno slot proprio per il nome, che il riferimento al prototipo memorizzato in cache sia identico a `getPrototype()` del ricevitore e che `PropertyMap::$version` del prototipo corrisponda ancora alla versione acquisita.

L’invalidazione della cache si basa su `PropertyMap::$version`. Le modifiche strutturali (impostazione / eliminazione) lo incrementano sempre. Le sovrascritture dei valori degli slot dati lo incrementano solo quando `PropertyMap::$isUsedAsProto` è true: un flag di write barrier che viene attivato la prima volta che l’oggetto viene rilevato come `[[Prototype]]` di un altro oggetto. Gli oggetti istanza che non fungono mai da prototipi non sostengono alcun costo per l’infrastruttura IC a ogni `obj.x = v`.

Gli accessor getter non vengono mai memorizzati in cache (devono essere eseguiti di nuovo a ogni lettura).

`PropertyMap` mantiene un secondo contatore, `$mutationVersion`, che viene incrementato *sempre* a ogni scrittura (senza write barrier). La cache `JSON.stringify` memorizza questo contatore per ogni oggetto/array annidato visitato, così una modifica limitata all’istanza come `o.a = 99` invalida correttamente la stringa memorizzata in cache, anche quando `$version` dell’IC avrebbe evitato l’incremento.

## Cache JSON.stringify

`JsObject` mantiene `$jsonCacheString` + `$jsonCacheVersions` (un elenco di coppie `[obj, mutationVersion]` per ogni oggetto annidato visitato durante la serializzazione triviale in stringa). Alla successiva chiamata `JSON.stringify(o)` senza replacer né spazio, il percorso rapido triviale verifica che nessuna delle versioni memorizzate sia cambiata e restituisce la stringa in cache senza ripetere la visita né la codifica. I pattern frequenti di deep clone come `JSON.parse(JSON.stringify(o))` ottengono un’accelerazione di circa 2× nella fase di stringify.

In caso di mancata corrispondenza (una versione memorizzata è diversa), la cache viene eliminata senza avvisi e ripopolata dalla visita successiva. Gli oggetti con `toJSON`, getter, accessor, simboli o slot `[[PrimitiveValue]]` ricorrono al serializzatore previsto dalle specifiche e non popolano mai la cache: il percorso in cache vede quindi solo dati con struttura uniforme.

## Percorsi rapidi integrati

I maggiori miglioramenti delle prestazioni sono arrivati insegnando alla VM a gestire specifiche funzioni integrate:

- **`Atomics.load` / `store` / `compareExchange`** con pianificazione cooperativa `loadSpinHook` / `storeNotifyHook` — recuperati 4 test di spin loop di Atomics.
- **`Date.prototype.getTimezoneOffset`** con una cache DST basata su hashmap diretta: velocità superiore del 45 % nei test di stress DST di SpiderMonkey.
- **`String.fromCharCode`** con un percorso rapido ASCII inline: risparmia circa 1 M dispatch per scansione tipica.
- **`decodeURI` / `decodeURIComponent`** con un percorso ultra rapido `%XX%XX%XX%XX` a 12 caratteri che usa la decodifica massiva `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** con accesso diretto alla mappa delle proprietà.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** con iterazione nativa sull’array sottostante PHP.

Ogni percorso rapido è subordinato a un «controllo della forma»: se il ricevitore non corrisponde alla forma prevista (e.g, ad esempio una vista TypedArray non basata su un PHP `string` semplice), la VM ricorre al percorso lento conforme alle specifiche.

## Cosa non fa

La VM bytecode **non è un JIT** nel senso di V8. Non genera codice macchina nativo per ogni funzione e non fa speculazioni sui tipi osservati oltre al controllo monomorfico dell’identità del prototipo usato dall’IC. L’inlining ai confini delle funzioni è approssimato dallo stack di chiamate personalizzato (senza un frame PHP aggiuntivo per chiamata JS), anziché compilando il corpo della funzione chiamata nel bytecode del chiamante.

Il transpiler JsToPhp è la cosa più simile a un JIT nell’engine: genera codice sorgente PHP per i corpi delle funzioni più usate, che il JIT con tracing di 8.5 di PHP compila poi in codice nativo. JsToPhp passa per `eval()`, quindi paga un costo di compilazione una tantum e il JIT con tracing deve individuare la funzione come se fosse codice utente. Evitare il passaggio `eval()` emettendo direttamente gli opcode opcache ridurrebbe ulteriormente il divario: vedi [Roadmap dei benchmark](https://phasis.inline0.dev/it/docs/advanced/benchmarks#roadmap).

## Benchmark

`bench/microbench.js` esegue una dozzina abbondante di piccoli benchmark (loop-arith, loop-fib, fn-recurse, fn-deep-recurse, obj-create, obj-prop, proto-method, gen-iterate, arr-push, arr-map, str-concat, str-split-join, json-roundtrip, closure, destructure). Tempo mediano trascorso:

```bash
php bench/run.php
```

I numeri attuali vengono salvati in `BENCH.md` dopo ogni esecuzione del workflow `bench`. I workflow della matrice `bench` e test262 vengono avviati entrambi manualmente.
