
# VM s bajtkódom

Predvolenou cestou vykonávania je stromový interpreter — prejde každý uzol AST a vráti hodnotu. Pri horúcich funkciách, ktoré sa volajú miliónkrát, je úzkym hrdlom réžia dispečingu každého uzla. Phasis na to pridáva vrstvu špekulatívneho kompilátora z bajtkódu do uzáverov PHP, ktorý túto réžiu znižuje.

## Kedy sa aktivuje VM

Každá funkcia `JsFunction` má počítadlo horúcich volaní. Keď prekročí prah (predvolene 100), engine sa pokúsi skompilovať telo funkcie do uzáveru PHP pomocou `src/Bytecode/JsToPhp.php`.

Kompilácia uspeje, ak telo funkcie zodpovedá známej množine tvarov:

- Priradenia lokálnym premenným s predvídateľnými typmi (číslo, objekt, funkcia, pole, reťazec).
- Aritmetické operácie s číslami.
- Čítanie členov v lokálnych premenných objektového typu A AJ v `this` (`this.v`, `obj.x`).
- Volania metód nad lokálnymi premennými objektového typu (`obj.method(args)`), pri ktorých sa prijímajúci objekt odovzdáva ako `$thisValue`.
- Volania konštruktora `new Ctor(args)` — povýšia typ lokálnej premennej na ľavej strane na `object` a kontrolou ústupu ošetria návratovú hodnotu, ktorá nie je objektom.
- Cykly `for` / `while` s jednoduchými podmienkami.
- `return` s vypočítateľným výrazom.
- `if` / `else` s podmienkami s typovanými hodnotami.

Kompilácia zlyhá (a funkcia zostane v stromovom interpreteri), ak telo obsahuje:

- Príkazy `with`.
- `try / catch / finally` (stromový interpreter ich spracúva, aby zachoval správnu sémantiku dokončenia).
- Volania `eval()`.
- Priamu mutáciu `arguments`.
- Všetko, čo kompilátor zatiaľ nevie preložiť.

Po úspešnej kompilácii engine presmeruje cestu volania funkcie na skompilovaný uzáver. Nasledujúce volania vykonávajú natívny kód PHP s réžiou jedného volania funkcie, nie s réžiou každého uzla AST.

## Ústup

Skompilovaná rýchla cesta predpokladá určité vlastnosti (čísla zostávajú číslami, vlastnosti zostávajú na rovnakom tvare objektu). Ak sa niektorý predpoklad za behu poruší, uzáver vyhodí `Bailout` a dané volanie sa znova spustí v stromovom interpreteri.

Práve tu je najdôležitejšia správnosť podľa špecifikácie. Stromový interpreter musí vedieť znova vykonať volanie, pri ktorom nastal ústup, od začiatku a bez toho, aby zaznamenal vedľajšie účinky neúspešnej rýchlej cesty. Phasis to rieši dvoma spôsobmi:

1. **Čisté prefixy** — kompilátor kontroluje vedľajšie účinky pred akoukoľvek špekuláciou. Ak funkcia vykonáva I/O (console.log, hostiteľské funkcie) pred bodom špekulácie, rýchla cesta ho nevynechá; potvrdí ho a pokračuje.
2. **Odmietnutie pri kompilácii** — pri tvaroch, ktoré by vyžadovali vrátenie pozorovateľných vedľajších účinkov (nenumerický príkaz `let r = fn()`, pri ktorom `fn()` mení stav), kompilátor odmietne kompiláciu. Funkcia zostane v stromovom interpreteri.

Motivačný príklad: nenumerické priradenie vykoná `fn()` na rýchlej ceste, vyhodí `Bailout` a potom sa znova vykoná v stromovom interpreteri — takže `fn()` sa spustí dvakrát a spôsobí dve sady vedľajších účinkov. Odmietnutie kompilácie zabráni spusteniu `fn()` na rýchlej ceste, takže stromový interpreter pozoruje jediné volanie.

## Dispečing VM (druhá vrstva)

Niektoré funkcie sa kompilujú do uzáverov PHP; ostatné prechádzajú do ešte jednoduchšieho interpretera bajtkódu v `src/Bytecode/VM.php`. Bajtkód tvorí ploché pole opkódov (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), ktoré spracúva `switch` dispečing.

VM slúži funkciám, ktoré sa neoplatí kompilovať do uzáveru PHP, no sú dosť horúce na to, aby im prospelo vynechanie prechodu cez AST. Volania metód vstavaných objektov (Array.prototype.map, String.prototype.split, …) využívajú rýchle cesty vo VM, ktoré obchádzajú všeobecný mostík `callFunction`.

## Vlastný zásobník volaní

`Op::CALL`, `Op::CALL_METHOD` a `Op::NEW_CALL` používajú cestu inline volaní, ktorá udržiava celý zásobník volaní JS v `VM::execute` — pri každom volaní JS sa nevytvára nový rámec PHP. Pri vstupe sa stav volajúceho (pc, prúd kódu, zásobník operandov, lokálne premenné, env, `this`, príznak prísneho režimu) uloží do fondu inštancií `SavedFrame` v rámci každého VM `savedFrames` a cyklus dispečingu nastaví svoje lokálne premenné na hodnoty volanej funkcie. `Op::RET` vyberie uložený rámec a obnoví stav. Výnimky sa postupne šíria cez uložené rámce a potom sa spracujú v stromovom interpreteri.

Oprávnenosť sa zapamätá na `JsFunction`: `vmInlineCallCache` pre bežné volania (telo skompilované do BC, nie je viazané pomocou `bind()` ani natívne), `vmInlineConstructCache` pre výrazy `new` (navyše `isConstructable`, nie je odvodený, bez inicializátorov polí inštancie a súkromných metód). Všetko ostatné naďalej prechádza cez kanonický mostík `callFunction` / `vmNewExpression`.

Vedľajší architektonický prínos: limit zásobníka PHP už neobmedzuje hĺbku rekurzie JS. Jediným limitom je teraz `CallStack::maxDepth` — nastavený na **4 096**, čo je s pohodlnou rezervou nad každým bežným prípadom test262 (väčšina potrebuje menej než 2 k rámcov) a zároveň dosť nízko na to, aby sa patologické testy nekonečnej rekurzie rýchlo ukončili v časovom limite spúšťača. Cesta transpilovaného uzáveru (`JsToPhp`) má vlastný limit 8 rámcov 192 pre náhradnú cestu bez inline volania.

## Generátory so snímkou rámca

Jednoduché telá generátorov — bez `yield*`, bez `yield` vnútri try/catch/finally a bez `await` — sa kompilujú do bajtkódu. Kompilátor pri pozastavení tela vytvorí nový opkód `Op::YIELD`; `Op::YIELD` zachytí stav dispečingu (cf, pc+1, zásobník, sp, lokálne premenné, env, thisValue, strict) do objektu na halde typu `GeneratorSnapshot` a vráti sentinel `YieldResult` z `VM::execute`. Riadiaci `JsGenerator` uloží snímku a pri ďalšom `next()` / `throw()` ju odovzdá späť do `VM::execute`, pričom nahradí návratovú hodnotu na vrchu zásobníka operandov.

Pre podporovanú podmnožinu to nahrádza cestu PHP-Fiber. Ovládač založený na Fiber (`executeGeneratorBody`) sa naďalej používa pre tvary, ktoré kompilátor odmietne — delegovanie `yield*`, generátory s yield vo vnútri bloku try, asynchrónne generátory a asynchrónne funkcie s `await`. `JsGenerator` podporuje oba režimy: pri každom `next()` / `throw()` / `return()` sa kontroluje, či má inštancia snímku alebo Fiber, a podľa toho sa zvolí spôsob dispečingu.

Nastavenie sa nedá ladiť pomocou benchmarkov; oprávnenosť sa určuje pri kompilácii a zapamätá sa na `JsFunction`. Zlepšenie spočíva v jednom menšom prepnutí kontextu PHP na každý yield v ceste so snímkou; cesta Fiber zostáva pre všetko ostatné nezmenená.

## Inline cache

`Op::LOAD_MEMBER` obsahuje inline cache pre každé PC, ktorej kľúčom je identita prototypu; používa sa pri vzore metódy prototypu na inštancii (`c.sum(...)`, `arr.push(...)`, každé volanie metódy triedy). Podmienka zásahu: prijímajúci objekt nemá vlastnú položku s daným názvom, referencia na objekt prototypu uložená v cache je totožná s objektom `getPrototype()` prijímajúceho objektu a hodnota `PropertyMap::$version` prototypu sa stále zhoduje so zachytenou verziou.

Invalidácia cache využíva `PropertyMap::$version`. Štrukturálne mutácie (nastavenie / odstránenie) ho vždy zvýšia. Hodnoty dátových položiek sa zvýšia iba vtedy, keď `PropertyMap::$isUsedAsProto` platí — príznak bariéry zápisu sa nastaví pri prvom zistení, že objekt slúži ako `[[Prototype]]` iného objektu. Inštančné objekty, ktoré nikdy neslúžia ako prototypy, neplatia za infraštruktúru IC pri každom `obj.x = v`.

Prístupové metódy getter sa nikdy neukladajú do cache (pri každom čítaní sa musia spustiť znova).

`PropertyMap` obsahuje druhé počítadlo `$mutationVersion`, ktoré sa zvýši *vždy* pri každom zápise (bez bariéry zápisu). Cache `JSON.stringify` si zaznamenáva toto počítadlo pri každom navštívenom vnorenom objekte alebo poli, takže mutácia obmedzená na inštanciu, ako napríklad `o.a = 99`, správne zneplatní uložený reťazec aj vtedy, keď by sa pri IC kvôli `$version` počítadlo nezvýšilo.

## Cache JSON.stringify

`JsObject` obsahuje `$jsonCacheString` + `$jsonCacheVersions` (zoznam dvojíc `[obj, mutationVersion]` pre každý vnorený objekt navštívený pri priechode na vytvorenie jednoduchého reťazca). Pri ďalšom volaní `JSON.stringify(o)` bez náhrady a medzier rýchla cesta pre jednoduchý prípad overí, či sa nezmenila žiadna zaznamenaná verzia, a vráti uložený reťazec bez opätovného prechádzania alebo kódovania. Vzorové operácie na hlboké klonovanie, ako napríklad `JSON.parse(JSON.stringify(o))`, zrýchľujú časť stringify približne 2×.

Pri neúspešnom overení (keď sa líši niektorá zaznamenaná verzia) sa cache potichu zahodí a naplní pri ďalšom priechode. Objekty s `toJSON`, getrami, prístupovými metódami, symbolmi alebo položkami `[[PrimitiveValue]]` prejdú do serializátora podľa špecifikácie a nikdy nenaplnia cache — uložená cesta preto spracúva iba štrukturálne jednotné údaje.

## Rýchle cesty vstavaných funkcií

Najväčšie zrýchlenia prišli po doplnení podpory konkrétnych vstavaných funkcií do VM:

- **`Atomics.load` / `store` / `compareExchange`** s kooperatívnym plánovaním `loadSpinHook` / `storeNotifyHook` — obnovili sa 4 testy spin-loop Atomics.
- **`Date.prototype.getTimezoneOffset`** s DST cache založenou priamo na hashmap — zrýchlenie o 45 % pri záťažových testoch DST SpiderMonkey.
- **`String.fromCharCode`** s rýchlou cestou pre ASCII — pri typickom prechode sa ušetrí približne 1 milión dispečingov.
- **`decodeURI` / `decodeURIComponent`** s mimoriadne rýchlou cestou `%XX%XX%XX%XX` pre 12-znakové hodnoty, ktorá využíva hromadné dekódovanie `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** s priamym prístupom k mape vlastností.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** s natívnou iteráciou nad podkladovým poľom PHP.

Každá rýchla cesta je podmienená „kontrolou tvaru“ — ak prijímajúci objekt nemá očakávaný tvar (e.g. pohľad TypedArray, ktorý nie je podložený obyčajným PHP `string`), VM prejde na pomalú cestu, ktorá správne dodržiava špecifikáciu.

## Čo táto funkcia nerobí

VM s bajtkódom **nie je JIT** v zmysle V8. Negeneruje natívny strojový kód pre každú funkciu a nešpekuluje na základe pozorovaných typov nad rámec monomorfnej kontroly identity prototypu v IC. Vkladanie na hranici funkcií približuje vlastný zásobník volaní (bez ďalšieho rámca PHP pri každom volaní JS), nie preklad tela volanej funkcie do bajtkódu volajúcej funkcie.

Transpilátor JsToPhp sa najviac približuje JIT kompilátoru: generuje zdrojový kód PHP pre telá horúcich funkcií, ktoré trasovací JIT nástroja PHP 8.5 následne skompiluje do natívneho kódu. JsToPhp používa `eval()`, preto sa platí jednorazová cena za kompiláciu a trasovací JIT musí funkciu objaviť, akoby išlo o používateľský kód. Vynechaním kroku `eval()` a priamym generovaním opkódov opcache by sa rozdiel ešte zmenšil — pozrite si [Benchmarky § Plán vývoja](https://phasis.inline0.dev/sk/docs/advanced/benchmarks#roadmap).

## Benchmark

`bench/microbench.js` spúšťa vyše tucta drobných benchmarkov (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). Medián času behu:

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

Aktuálne výsledky sa po každom spustení workflow `bench` ukladajú do `BENCH.md`. Matice workflow `bench` a test262 sa obe spúšťajú manuálne.
