
# Virtuální stroj s bajtkódem

Výchozí cesta provádění používá stromový interpret — prochází každý uzel AST a vrací hodnotu. U často volaných funkcí v řádu milionů volání je úzkým hrdlem režie zpracování jednotlivých uzlů. Phasis nad ni přidává spekulativní kompilátor z bajtkódu do uzávěrů PHP, který tuto režii snižuje.

## Kdy se aktivuje VM

Každá funkce `JsFunction` má počitadlo častých volání. Jakmile překročí prahovou hodnotu (výchozí 100), engine se pokusí zkompilovat tělo funkce do uzávěru PHP prostřednictvím `src/Bytecode/JsToPhp.php`.

Kompilace uspěje, pokud tělo funkce odpovídá některému ze známých tvarů:

- Přiřazení lokálním proměnným s předvídatelnými typy (číslo, objekt, funkce, pole, řetězec).
- Aritmetické operace s čísly.
- Čtení vlastností objektově typovaných lokálních proměnných A TAKÉ objektů `this` (`this.v`, `obj.x`).
- Volání metod nad objektově typovanými lokálními proměnnými (`obj.method(args)`), při kterém se příjemce předává jako `$thisValue`.
- Volání konstruktoru `new Ctor(args)` — změní typ levé lokální proměnné na `object` a pomocí Bailout ověří, že návratová hodnota není objekt.
- Smyčky `for` / `while` s jednoduchými tvary podmínek.
- `return` s výrazem, jehož hodnotu lze vypočítat.
- `if` / `else` nad typovanými podmínkami.

Kompilace selže (a funkce zůstane ve stromovém interpretu), pokud tělo obsahuje:

- Příkazy `with`.
- `try / catch / finally` (zpracovává je stromový interpret, aby správně dodržel sémantiku dokončení).
- Volání `eval()`.
- Přímou změnu `arguments`.
- Cokoli, co kompilátor zatím neumí převést.

Pokud kompilace uspěje, engine přepne cestu volání funkce na zkompilovaný uzávěr. Další volání spouštějí nativní PHP s režií jediného volání funkce, nikoli jednoho volání na každý uzel AST.

## Ústupová řešení

Zrychlená cesta zkompilovaného kódu vychází z předpokladů (čísla zůstávají čísly, vlastnosti zůstávají ve stejné struktuře). Pokud se některý předpoklad za běhu poruší, uzávěr vyvolá `Bailout` a provádění tohoto volání se znovu spustí ve stromovém interpretu.

Právě zde je správnost podle specifikace nejdůležitější. Stromový interpret musí umět znovu provést volání, u kterého došlo k ústupu, od začátku a bez pozorovatelných vedlejších účinků z neúspěšné zrychlené cesty. Phasis to zajišťuje dvěma způsoby:

1. **Čisté úvodní části** — kompilátor před spekulací hledá vedlejší účinky. Pokud funkce před místem spekulace provádí I/O (console.log, hostitelské funkce), zrychlená cesta je nevynechá; potvrdí je a pokračuje.
2. **Odmítnutí při kompilaci** — u tvarů, které by vyžadovaly vrácení pozorovatelných vedlejších účinků zpět (nečíselné `let r = fn()`, kde `fn()` mění stav), kompilátor kompilaci zcela odmítne. Funkce zůstane ve stromovém interpretu.

Příklad: nečíselné přiřazení spustí `fn()` ve zrychlené cestě, vyvolá `Bailout` a poté se znovu provede ve stromovém interpretu — tím se `fn()` spustí dvakrát se dvěma sadami vedlejších účinků. Odmítnutí při kompilaci zabrání spuštění `fn()` ve zrychlené cestě, takže stromový interpret zaznamená jediné volání.

## Zpracování ve VM (druhá vrstva)

Některé funkce se kompilují do uzávěrů PHP; ostatní přecházejí k ještě jednoduššímu interpretu bajtkódu v `src/Bytecode/VM.php`. Bajtkód je ploché pole operačních kódů (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), které zpracovává `switch`.

VM slouží funkcím, které se nevyplatí kompilovat do uzávěru PHP, ale volají se dost často na to, aby se vyplatilo přeskočit procházení AST. Volání metod vestavěných objektů (Array.prototype.map, String.prototype.split, …) procházejí rychlými cestami ve VM, které obcházejí obecný předávací mechanismus `callFunction`.

## Vlastní zásobník volání

`Op::CALL`, `Op::CALL_METHOD` a `Op::NEW_CALL` používají cestu pro přímá volání, která udržuje celý zásobník volání JS uvnitř `VM::execute` — při každém volání JS nevzniká nový rámec PHP. Při vstupu se stav volajícího (pc, proud kódu, zásobník operandů, lokální proměnné, env, `this`, příznak strict mode) uloží do fondu `SavedFrame` instancí `savedFrames` pro daný VM a smyčka zpracování přenastaví lokální proměnné na volanou funkci. `Op::RET` vyjme uložený rámec a obnoví stav. Výjimky se postupně vracejí přes uložené rámce a poté se předají stromovému interpretu.

Způsobilost se ukládá do mezipaměti pro `JsFunction`: `vmInlineCallCache` pro běžná volání (tělo zkompilované do bajtkódu, bez vazby na `bind()`, nikoli nativní), `vmInlineConstructCache` pro výrazy `new` (navíc `isConstructable`, nikoli odvozené, bez inicializátorů polí instance nebo privátních metod). Vše ostatní nadále prochází kanonickým předávacím mechanismem `callFunction` / `vmNewExpression`.

Vedlejší přínos této architektury: limit zásobníku PHP už neomezuje hloubku rekurze JS. Nyní je jediným limitem `CallStack::maxDepth` — nastavený na **4 096**, tedy výrazně nad potřebami všech legitimních případů test262 (většině stačí méně než 2 tisíce rámců) a zároveň dost nízký na to, aby patologické testy s nekonečnou rekurzí rychle selhaly v časovém limitu běhu. Cesta přes transpillovaný uzávěr (`JsToPhp`) má vlastní ochranný limit 8 rámců 192 pro záložní variantu bez přímého volání.

## Generátory se snímkem rámce

Jednoduchá těla generátorů — bez `yield*`, bez `yield` uvnitř try/catch/finally a bez `await` — se kompilují do bajtkódu. Kompilátor vloží nový operační kód `Op::YIELD` do místa, kde se tělo pozastaví; `Op::YIELD` zachytí stav zpracování (cf, pc+1, zásobník, sp, lokální proměnné, env, thisValue, strict) do objektu na haldě typu `GeneratorSnapshot` a vrátí z `VM::execute` zástupnou hodnotu `YieldResult`. Řídicí `JsGenerator` uloží snímek a při dalším `next()` / `throw()` ho předá zpět `VM::execute` a nahradí hodnotu pro obnovení na vrcholu zásobníku operandů.

U podporované podmnožiny to nahrazuje cestu přes PHP Fiber. Ovladač založený na Fiber (`executeGeneratorBody`) zůstává pro tvary, které kompilátor odmítne — `yield*` delegování, generátory s příkazem yield uvnitř bloku try, asynchronní generátory a asynchronní funkce s `await`. `JsGenerator` pracuje ve dvou režimech: každý `next()` / `throw()` / `return()` zkontroluje, zda má instance snímek, nebo Fiber, a podle toho zvolí způsob zpracování.

Žádný parametr k ladění testů výkonu; způsobilost se určuje při kompilaci a ukládá do mezipaměti pro `JsFunction`. Zrychlení spočívá v jednom kontextovém přepnutí PHP méně při každém pozastavení ve variantě se snímkem; cesta přes Fiber zůstává pro vše ostatní beze změny.

## Mezipaměti inline

`Op::LOAD_MEMBER` má inline cache pro každý PC, klíčovanou identitou prototypu, která se používá pro vzor metoda prototypu na instanci (`c.sum(...)`, `arr.push(...)`, každé volání metody třídy). Podmínka zásahu: příjemce nemá vlastní slot s daným názvem, odkaz na prototyp v cache je totožný s `getPrototype()` příjemce a `PropertyMap::$version` prototypu stále odpovídá zachycené verzi.

Zneplatnění cache probíhá přes `PropertyMap::$version`. Strukturální změny (nastavení / smazání) jej vždy zvýší. Přepsání hodnoty datového slotu jej zvýší jen tehdy, když je `PropertyMap::$isUsedAsProto` pravdivý — příznak bariéry zápisu, který se přepne při prvním zjištění, že objekt slouží jako `[[Prototype]]` jiného objektu. Instance, které nikdy neslouží jako prototypy, neplatí za infrastrukturu IC při každém `obj.x = v`.

Přístupory getterů se nikdy neukládají do cache (při každém čtení se musí spustit znovu).

`PropertyMap` má druhé počítadlo, `$mutationVersion`, které se zvýší *vždy* při každém zápisu (bez bariéry zápisu). Cache `JSON.stringify` připne toto počítadlo u každého navštíveného vnořeného objektu či pole, aby změna pouze instance, například `o.a = 99`, správně zneplatnila řetězec v cache, i když by `$version` IC zvýšení přeskočilo.

## Cache JSON.stringify

`JsObject` uchovává `$jsonCacheString` + `$jsonCacheVersions` (seznam dvojic `[obj, mutationVersion]` pro každý navštívený vnořený objekt při průchodu pro triviální serializaci do řetězce). Při dalším volání `JSON.stringify(o)` bez replaceru a bez mezery triviální rychlá cesta ověří, že se žádná připnutá verze nezměnila, a vrátí řetězec z cache bez opakovaného průchodu či kódování. Časté vzory s hlubokým klonováním, jako je `JSON.parse(JSON.stringify(o))`, zrychlí serializaci do řetězce zhruba 2×.

Při neúspěšném zásahu (liší se některá připnutá verze) se cache tiše zahodí a znovu naplní při dalším průchodu. Objekty s `toJSON`, gettery, přístupory, symboly nebo sloty `[[PrimitiveValue]]` přejdou na serializátor podle specifikace a cache nikdy nenaplní — cesta přes cache tak pracuje pouze se strukturálně jednotnými daty.

## Vestavěné rychlé cesty

Největší výkonnostní zisky přineslo přizpůsobení VM konkrétním vestavěným funkcím:

- **`Atomics.load` / `store` / `compareExchange`** s kooperativním plánováním `loadSpinHook` / `storeNotifyHook` — obnovilo 4 testy spin-loopů Atomics.
- **`Date.prototype.getTimezoneOffset`** s přímou cache DST založenou na hashmapě — o 45 % rychlejší při zátěžovém testu DST SpiderMonkey.
- **`String.fromCharCode`** s inline rychlou cestou pro ASCII — ušetří přibližně 1 M volání při typickém průchodu.
- **`decodeURI` / `decodeURIComponent`** s ultra rychlou cestou `%XX%XX%XX%XX` o délce 12 znaků, která používá hromadné dekódování `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** s přímým přístupem k mapě vlastností.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** s nativní iterací nad podkladovým polem PHP.

Každá rychlá cesta je podmíněna „kontrolou tvaru“ — pokud příjemce neodpovídá očekávanému tvaru (e.g, například pohled TypedArray, který není podložen běžným PHP `string`), VM přejde na pomalejší cestu odpovídající specifikaci.

## Co tato implementace nedělá

Bajtový kód VM **není JIT** ve smyslu V8. Negeneruje nativní strojový kód pro každou funkci a nespekuluje na základě pozorovaných typů nad rámec monomorfní kontroly identity prototypu v IC. Vkládání kódu přes hranice funkcí se napodobuje vlastním zásobníkem volání (bez dalšího rámce PHP při každém volání JS), nikoli převedením těla volané funkce do bajtového kódu volajícího.

Transpilátor JsToPhp je nejblíže JITu, co tento engine nabízí: generuje zdrojový kód PHP pro těla často volaných funkcí, který trasovací JIT v PHP 8.5 následně zkompiluje do nativního kódu. JsToPhp používá `eval()`, takže platí jednorázovou cenu za kompilaci a trasovací JIT musí funkci objevit, jako by šlo o uživatelský kód. Přeskočení kroku `eval()` přímým generováním opkódů opcache by rozdíl dále zmenšilo — viz [Srovnávací testy § Plán](https://phasis.inline0.dev/cs/docs/advanced/benchmarks#roadmap).

## Srovnávací testy

`bench/microbench.js` spouští něco přes tucet drobných srovnávacích testů (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ánová doba běhu:

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

Aktuální výsledky se po každém spuštění pracovního postupu `bench` ukládají do souboru `BENCH.md`. Pracovní postupy `bench` a matice test262 se spouštějí ručně.
