
# VM de bytecode

Calea implicită de execuție este un parcurgător arborescent — vizitează fiecare nod AST și returnează o valoare. Pentru funcțiile intens apelate, de milioane de ori, costul dispatch-ului per nod devine un blocaj. Phasis adaugă deasupra un compilator speculativ de bytecode în closure-uri PHP, pentru a reduce acest cost.

## Când intră în funcțiune VM-ul

Fiecare `JsFunction` are un contor de apeluri frecvente. Când depășește un prag (implicit 100), motorul încearcă să compileze corpul funcției într-un closure PHP prin `src/Bytecode/JsToPhp.php`.

Compilarea reușește când corpul funcției se încadrează într-un set cunoscut de forme:

- Atribuiri de variabile locale cu tipuri previzibile (numeric, obiect, funcție, array, șir de caractere).
- Operații aritmetice asupra numerelor.
- Citiri de membri din localuri de tip obiect ȘI din `this` (`this.v`, `obj.x`).
- Apeluri de metode asupra localurilor de tip obiect (`obj.method(args)`), cu receptorul transmis mai departe ca `$thisValue`.
- Apeluri de constructor `new Ctor(args)` — promovează localul LHS la tipul `object` și verifică prin Bailout că rezultatul nu este un obiect.
- Bucle `for` / `while` cu forme simple de condiție.
- `return` cu o expresie calculabilă.
- `if` / `else` cu condiții tipizate.

Compilarea eșuează (iar funcția rămâne pe parcurgătorul arborescent) când corpul conține:

- Instrucțiuni `with`.
- `try / catch / finally` (gestionat de parcurgătorul arborescent pentru semantica adecvată de finalizare).
- Apeluri `eval()`.
- Modificare directă a `arguments`.
- Orice nu știe încă compilatorul să transforme în cod de nivel inferior.

Când compilarea reușește, motorul schimbă calea de apel a funcției cu closure-ul compilat. Apelurile ulterioare execută cod nativ PHP cu un singur cost de apel de funcție, nu unul pentru fiecare nod AST.

## Abandonări

Calea rapidă compilată se bazează pe presupuneri (numerele rămân numere, proprietățile rămân pe aceeași formă). Când o presupunere este încălcată în timpul execuției, closure-ul aruncă un `Bailout`, iar execuția repornește în parcurgătorul arborescent pentru acel apel.

Aici contează cel mai mult corectitudinea față de specificație. Parcurgătorul arborescent trebuie să poată reexecuta de la zero apelul abandonat, fără să observe efecte secundare de pe calea rapidă eșuată. Phasis tratează situația în două moduri:

1. **Prefixe pure** — compilatorul caută efecte secundare înainte de orice speculație. Dacă funcția efectuează I/O (console.log, funcții gazdă) înainte de punctul de speculație, calea rapidă nu îl elimină; îl finalizează și continuă.
2. **Refuz la compilare** — pentru forme care ar necesita anularea unor efecte secundare observabile (un `let r = fn()` non-numeric unde `fn()` modifică starea), compilatorul refuză complet compilarea. Funcția rămâne pe parcurgătorul arborescent.

Cazul care a motivat această abordare: o atribuire non-numerică execută `fn()` pe calea rapidă, aruncă `Bailout`, apoi este reexecutată pe parcurgătorul arborescent — rulând `fn()` de două ori, cu două seturi de efecte secundare. Refuzul la compilare împiedică `fn()` să ruleze deloc pe calea rapidă, astfel încât parcurgătorul arborescent observă singurul apel.

## Dispatch-ul VM-ului (al doilea nivel)

Unele funcții sunt compilate în closure-uri PHP; altele trec la un interpretor de bytecode și mai simplu în `src/Bytecode/VM.php`. Bytecode-ul este o matrice liniară de opcoduri (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), procesată prin dispatch `switch`.

VM-ul există pentru funcțiile care nu merită compilate într-un closure PHP, dar sunt suficient de frecvent apelate pentru a beneficia de omiterea parcurgerii AST-ului. Apelurile de metode asupra obiectelor încorporate (Array.prototype.map, String.prototype.split, …) trec prin căi rapide din VM, care ocolesc trampoline-ul generic `callFunction`.

## Callstack personalizat

`Op::CALL`, `Op::CALL_METHOD` și `Op::NEW_CALL` folosesc o cale de apel inline care păstrează întregul callstack JS în `VM::execute` — fără un cadru PHP nou pentru fiecare apel JS. La intrare, starea apelantului (pc, flux de cod, stivă de operanzi, localuri, env, `this`, indicatorul de mod strict) este pusă într-un pool `savedFrames` per VM de instanțe `SavedFrame`, iar bucla de dispatch își reconectează localurile la apelat. `Op::RET` scoate cadrul salvat din pool și îl restaurează. Excepțiile parcurg cadrele salvate înapoi înainte de a reveni la parcurgătorul arborescent.

Eligibilitatea este memorată în cache pe `JsFunction`: `vmInlineCallCache` pentru apeluri obișnuite (corp compilat BC, nu legat de `bind()`, nu nativ), `vmInlineConstructCache` pentru expresii `new` (în plus `isConstructable`, nu derivat, fără inițializatoare de câmpuri de instanță sau metode private). Orice altceva trece în continuare prin trampoline-ul canonic `callFunction` / `vmNewExpression`.

Beneficiul arhitectural secundar: limita stivei PHP nu mai limitează adâncimea recursiei JS. Acum singura limită este `CallStack::maxDepth` — setată la **4 096**, suficient de mare pentru orice caz legitim test262 (majoritatea necesită sub 2 k cadre) și suficient de strictă pentru ca testele de recursie infinită patologică să eșueze rapid în bugetul de timeout al runner-ului. Calea cu closure transpilat (`JsToPhp`) are propria protecție de 8 cadre 192 pentru fallback-ul neinliniat.

## Generatoare cu instantanee de cadre

Corpurile simple ale generatoarelor — fără `yield*`, fără `yield` în interiorul try/catch/finally, fără `await` — sunt compilate în bytecode. Compilatorul emite un nou opcode `Op::YIELD` în locul unde corpul se suspendă; `Op::YIELD` capturează starea de dispatch (cf, pc+1, stivă, sp, localuri, env, thisValue, strict) într-un obiect heap `GeneratorSnapshot` și returnează un sentinel `YieldResult` din `VM::execute`. Mecanismul `JsGenerator` păstrează instantaneul și îl transmite înapoi către `VM::execute` la următorul `next()` / `throw()`, înlocuind valoarea de reluare din vârful stivei de operanzi.

Aceasta înlocuiește calea PHP-Fiber pentru subsetul acceptat. Driverul bazat pe Fiber (`executeGeneratorBody`) rămâne disponibil pentru formele respinse de compilator — delegarea `yield*`, generatoare care cedează execuția în interiorul unui bloc try, generatoare asincrone, funcții asincrone cu `await`. `JsGenerator` funcționează în mod dual: fiecare `next()` / `throw()` / `return()` verifică dacă instanța are un instantaneu sau un Fiber și face dispatch-ul corespunzător.

Nu există un parametru reglabil pentru benchmark; eligibilitatea este decisă la compilare și memorată în cache pe `JsFunction`. Câștigul este eliminarea unui PHP comutări de context la fiecare yield pe calea cu instantanee; calea Fiber rămâne neschimbată pentru toate celelalte cazuri.

## Cache-uri inline

`Op::LOAD_MEMBER` păstrează un cache inline per PC, indexat după identitatea prototipului, folosit pentru tiparul metodei prototipului pe instanță (`c.sum(...)`, `arr.push(...)`, fiecare dispatch de metodă de clasă). Condiție de acces: receptorul nu are un slot propriu pentru nume, referința prototipului din cache este identică cu `getPrototype()` receptorului, iar `PropertyMap::$version` prototipului încă se potrivește cu versiunea capturată.

Invalidarea cache-ului se bazează pe `PropertyMap::$version`. Modificările structurale (setare/ștergere) îl incrementează necondiționat. Suprascrierile valorilor sloturilor de date îl incrementează numai când `PropertyMap::$isUsedAsProto` este activ — un indicator de write-barrier activat prima dată când obiectul este observat ca `[[Prototype]]` al altui obiect. Obiectele instanță care nu servesc niciodată drept prototipuri nu plătesc costul infrastructurii IC la fiecare `obj.x = v`.

Accesorii de tip getter nu sunt niciodată memorizați în cache (trebuie rulați la fiecare citire).

`PropertyMap` păstrează un al doilea contor, `$mutationVersion`, care crește *necondiționat* la fiecare scriere (fără write-barrier). Cache-ul `JSON.stringify` fixează acest contor pe fiecare obiect/matrice imbricată vizitată, astfel încât o modificare doar a instanței, precum `o.a = 99`, să invalideze corect șirul memorat în cache, chiar și atunci când `$version` al IC-ului ar fi omis incrementarea.

## Cache JSON.stringify

`JsObject` păstrează `$jsonCacheString` + `$jsonCacheVersions` (o listă de perechi `[obj, mutationVersion]` pentru fiecare obiect imbricat vizitat în parcurgerea trivial-stringify). La următorul apel `JSON.stringify(o)` fără replacer/fără spațiere, calea rapidă trivială verifică dacă toate versiunile fixate au rămas neschimbate și returnează șirul memorat în cache fără să parcurgă din nou sau să recodifice. Tipare frecvente de clonare profundă, precum `JSON.parse(JSON.stringify(o))`, obțin o accelerare de aproximativ 2× la partea de stringify.

Cache-ul este eliminat fără notificare la o ratare (dacă orice versiune fixată diferă) și este reumplut la următoarea parcurgere. Obiectele cu `toJSON`, getters, accesorii, simboluri sau sloturi `[[PrimitiveValue]]` revin la serializatorul conform specificației și nu populează niciodată cache-ul — astfel, calea memorată în cache vede doar date uniforme structural.

## Căi rapide pentru funcțiile încorporate

Cele mai mari câștiguri de performanță au venit din instruirea VM-ului să gestioneze anumite funcții încorporate:

- **`Atomics.load` / `store` / `compareExchange`** cu planificare cooperativă `loadSpinHook` / `storeNotifyHook` — au recuperat 4 teste de buclă de așteptare Atomics.
- **`Date.prototype.getTimezoneOffset`** cu un cache DST direct bazat pe hashmap — accelerare de 45 % la testul de stres DST SpiderMonkey.
- **`String.fromCharCode`** cu o cale rapidă ASCII inline — economisește aproximativ 1 M de operații dispatch la o parcurgere tipică.
- **`decodeURI` / `decodeURIComponent`** cu o cale ultra-rapidă `%XX%XX%XX%XX` de 12 caractere, folosind decodarea bulk `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** cu acces direct la harta proprietăților.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** cu iterare nativă peste matricea PHP subiacentă.

Fiecare cale rapidă este condiționată de o „verificare a formei” — dacă receptorul nu corespunde formei așteptate (e.g. o vizualizare TypedArray care nu este susținută de o PHP `string` simplă), VM-ul trece la calea lentă conformă cu specificația.

## Ce nu face

VM-ul de bytecode **nu este un JIT** în sensul V8. Nu generează cod mașină nativ per funcție și nu face speculații pe baza tipurilor observate, în afara verificării monomorfe a identității prototipului din IC. Inserarea inline la limita funcției este aproximată prin callstack-ul personalizat (fără cadru PHP suplimentar pentru fiecare apel JS), nu prin coborârea corpului apelat în bytecode-ul apelantului.

Transpilerul JsToPhp este lucrul cel mai apropiat de un JIT pe care îl are motorul: generează cod sursă PHP pentru corpurile funcțiilor frecvent apelate, pe care JIT-ul de trasare al versiunii PHP 8.5 îl compilează apoi în cod nativ. JsToPhp trece prin `eval()`, deci suportă costul compilării o singură dată, iar JIT-ul de trasare trebuie să descopere funcția ca și cum ar fi cod scris de utilizator. Eliminarea etapei `eval()` prin emiterea directă a opcodurilor opcache ar reduce mai mult diferența — consultați [Benchmarks § Plan de dezvoltare](https://phasis.inline0.dev/ro/docs/advanced/benchmarks#roadmap).

## Benchmark

`bench/microbench.js` rulează peste o duzină de benchmark-uri mici (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). Timp median de execuție:

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

Valorile curente sunt salvate în `BENCH.md` după fiecare rulare a fluxului de lucru `bench`. Fluxurile de lucru cu matricea `bench` și test262 sunt pornite manual.
