
# Navidezni stroj z bajtno kodo

Privzeta pot izvajanja je sprehajalnik dreves: obišče vsako vozlišče AST-ja in vrne vrednost. Pri vročih funkcijah, ki se pokličejo milijonkrat, je ozko grlo režija dispečiranja za posamezno vozlišče. Phasis temu doda špekulativni prevajalnik iz bajtne kode v zaprtje PHP, da zmanjša to režijo.

## Kdaj se aktivira navidezni stroj

Vsaka funkcija `JsFunction` ima števec vročih klicev. Ko preseže prag (privzeto 100), pogon poskusi telo funkcije prek `src/Bytecode/JsToPhp.php` prevesti v zaprtje PHP.

Prevajanje uspe, kadar se telo funkcije ujema z znanimi oblikami:

- Dodelitve lokalnim spremenljivkam s predvidljivimi vrstami (številska, objektna, funkcijska, polje, niz).
- Aritmetične operacije nad števili.
- Branje članov iz lokalnih spremenljivk objektnega tipa IN iz `this` (`this.v`, `obj.x`).
- Klici metod na lokalnih spremenljivkah objektnega tipa (`obj.method(args)`), pri čemer se prejemnik posreduje kot `$thisValue`.
- Klici konstruktorja `new Ctor(args)` — nadgradi tip lokalne spremenljivke LHS v `object` in z varovalom Bailout preveri, ali je vrnjena vrednost objekt.
- Zanke `for` / `while` s preprostimi oblikami pogojev.
- `return` z izrazom, ki ga je mogoče izračunati.
- `if` / `else` nad pogoji z določenim tipom.

Prevajanje ne uspe (funkcija ostane v sprehajalniku dreves), če telo vsebuje:

- Stavke `with`.
- `try / catch / finally` (obravnava jih sprehajalnik dreves, da zagotovi pravilno semantiko dokončanja).
- Klice `eval()`.
- Neposredno spreminjanje `arguments`.
- Karkoli, česar prevajalnik še ne zna prevesti v nižjenivojsko obliko.

Ko prevajanje uspe, pogon preusmeri pot klicev funkcije na prevedeno zaprtje. Nadaljnji klici izvedejo izvorno PHP z režijo enega klica funkcije, namesto režije za vsako vozlišče AST-ja.

## Izhodi iz optimizirane poti

Prevedena hitra pot temelji na predpostavkah (števila ostanejo števila, lastnosti ostanejo na isti obliki). Ko je predpostavka med izvajanjem kršena, zaprtje sproži `Bailout`, izvajanje pa se za ta klic znova začne v sprehajalniku dreves.

Tu je pravilnost glede na specifikacijo najpomembnejša. Spet mora biti mogoče klic, ki je sprožil izhod, od začetka izvesti v sprehajalniku dreves, ne da bi ta zaznal stranske učinke neuspešne hitre poti. Phasis to zagotavlja na dva načina:

1. **Čiste predpone** — prevajalnik pred kakršnim koli špekuliranjem poišče stranske učinke. Če funkcija izvede V/I (console.log, funkcije gostitelja) pred točko špekuliranja, hitra pot tega ne izpusti, temveč se učinek zapiše in izvajanje nadaljuje.
2. **Zavrnitev med prevajanjem** — pri oblikah, ki bi zahtevale povrnitev opaznih stranskih učinkov (neneštevilski `let r = fn()`, pri katerem `fn()` spreminja stanje), prevajalnik prevajanje v celoti zavrne. Funkcija ostane v sprehajalniku dreves.

Primer, ki ponazarja težavo: neneštevilska dodelitev na hitri poti izvede `fn()`, sproži `Bailout` in se nato znova izvede v sprehajalniku dreves — s tem se `fn()` izvede dvakrat, vsakič z novimi stranskimi učinki. Zavrnitev med prevajanjem prepreči, da bi se `fn()` sploh izvedel na hitri poti, zato sprehajalnik dreves zazna edini klic.

## Dispečiranje v navideznem stroju (drugi sloj)

Nekatere funkcije se prevedejo v zaprtja PHP, druge pa preidejo v še preprostejši tolmač bajtne kode v `src/Bytecode/VM.php`. Bajtna koda je ravno polje opkod (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), ki jih obdeluje dispečiranje `switch`.

Navidezni stroj je namenjen funkcijam, ki se jih ne splača prevajati v zaprtje PHP, vendar se izvajajo dovolj pogosto, da jim koristi preskok sprehoda po AST-ju. Klici metod na vgrajenih objektih (Array.prototype.map, String.prototype.split, …) uporabljajo hitre poti v navideznem stroju, ki zaobidejo generični posrednik `callFunction`.

## Sklad klicev po meri

`Op::CALL`, `Op::CALL_METHOD` in `Op::NEW_CALL` uporabljajo vgrajeno pot klicev, ki ohranja celoten sklad klicev JS znotraj `VM::execute` — za vsak klic JS ni novega okvira PHP. Ob vstopu se stanje klicatelja (pc, tok kode, sklad operandov, lokalne spremenljivke, env, `this`, zastavica načina strict) shrani v področje `savedFrames` za posamezni navidezni stroj, sestavljeno iz primerkov `SavedFrame`, nato pa zanka dispečiranja preveže svoje lokalne spremenljivke na klicano funkcijo. `Op::RET` odstrani shranjeni okvir s sklada in obnovi stanje. Izjeme se odvijejo prek shranjenih okvirov, preden se izvajanje vrne v sprehajalnik dreves.

Upravičenost se predpomni na `JsFunction`: `vmInlineCallCache` za običajne klice (telo, prevedeno v bajtno kodo; ni vezano z `bind()` in ni izvorno), `vmInlineConstructCache` za izraze `new` (dodatno `isConstructable`, ne izpeljan, brez inicializatorjev polj primerka ali zasebnih metod). Vse drugo še vedno poteka prek kanoničnega posrednika `callFunction` / `vmNewExpression`.

Arhitekturna prednost: omejitev globine sklada PHP ne omejuje več globine rekurzije JS. Zdaj velja samo še omejitev `CallStack::maxDepth`, nastavljena na **4 096**; to je precej več od vsake legitimne oblike test262 (večina jih potrebuje manj kot 2 k okvirov) in hkrati dovolj malo, da patološki testi neskončne rekurzije hitro spodletijo znotraj časovne omejitve izvajalnika. Pot prevedenih zaprtij (`JsToPhp`) ima za nadomestno pot, ki ni vgrajena, lastno omejitev 8 okvirov 192.

## Generatorji s posnetki okvirov

Preprosta telesa generatorjev — brez `yield*`, brez `yield` znotraj try/catch/finally in brez `await` — se prevedejo v bajtno kodo. Prevajalnik na mesto prekinitve telesa vstavi novo opkodo `Op::YIELD`; `Op::YIELD` zajame stanje dispečiranja (cf, pc+1, sklad, sp, lokalne spremenljivke, env, thisValue, strict) v objekt `GeneratorSnapshot` na kopici in vrne označevalno vrednost `YieldResult` iz `VM::execute`. Pogonski `JsGenerator` shrani posnetek in ga ob naslednjem `next()` / `throw()` znova posreduje v `VM::execute`, pri čemer nadomesti vrednost za nadaljevanje na vrhu sklada operandov.

To za podprto podmnožico nadomesti pot prek vlakna PHP. Pogonski mehanizem, ki temelji na vlaknih (`executeGeneratorBody`), ostaja za oblike, ki jih prevajalnik zavrne — delegiranje `yield*`, generatorje s prekinitvijo znotraj bloka try, asinhrone generatorje in asinhrone funkcije z `await`. `JsGenerator` podpira oba načina: vsak `next()` / `throw()` / `return()` preveri, ali ima primerek posnetek ali vlakno, in ustrezno izvede dispečiranje.

Nastavitev, ki bi jo bilo mogoče prilagajati s primerjalnimi testi, ni; upravičenost se določi med prevajanjem in predpomni na `JsFunction`. Prednost poti s posnetki je eno preklapljanje konteksta PHP manj pri vsaki prekinitvi; pot z vlakni za vse drugo ostane nespremenjena.

## Vgrajeni predpomnilniki

`Op::LOAD_MEMBER` ima vgrajeni predpomnilnik za vsak PC, ključan po identiteti prototipa; uporablja se za vzorec metode prototipa na primerku (`c.sum(...)`, `arr.push(...)`, vsako dispečiranje metode razreda). Pogoji zadetka: prejemnik nima lastnega mesta za to ime, predpomnjena referenca prototipa je enaka `getPrototype()` prejemnika, različica `PropertyMap::$version` prototipa pa se še ujema z zajeto različico.

Razveljavljanje predpomnilnika temelji na `PropertyMap::$version`. Strukturne spremembe (nastavitev ali izbris) ga vedno povečajo. Različica se pri prepisu vrednosti podatkovnega mesta poveča le, če velja `PropertyMap::$isUsedAsProto` — zastavica za zaščito pred pisanjem, ki se prvič vklopi, ko je objekt opažen kot `[[Prototype]]` drugega objekta. Primerki objektov, ki nikoli niso prototipi, pri vsakem `obj.x = v` ne plačajo režije infrastrukture IC.

Dostop do pridobivalnikov se nikoli ne predpomni (ob vsakem branju jih je treba znova izvesti).

`PropertyMap` ima drugi števec, `$mutationVersion`, ki se *vedno* poveča pri vsakem pisanju (brez zaščite pred pisanjem). Predpomnilnik `JSON.stringify` shrani ta števec za vsak obiskani ugnezdeni objekt ali polje, tako da sprememba, ki vpliva samo na primerek, kot je `o.a = 99`, pravilno razveljavi predpomnjeni niz, tudi kadar bi IC zaradi `$version` preskočil povečanje števca.

## Predpomnilnik JSON.stringify

`JsObject` vsebuje `$jsonCacheString` + `$jsonCacheVersions` (seznam parov `[obj, mutationVersion]` za vsak ugnezdeni objekt, obiskan med preprostim sprehodom za pretvorbo v niz). Ob naslednjem klicu `JSON.stringify(o)` brez zamenjevalnika ali presledkov pred izhodno kodo hitra pot preveri, ali so vse shranjene različice nespremenjene, in vrne predpomnjeni niz brez ponovnega sprehoda ali kodiranja. Pogosti vzorci globokega kloniranja, kot je `JSON.parse(JSON.stringify(o))`, pospešijo del pretvorbe v niz za približno 2×.

Če pride do zgrešitve (shranjena različica se razlikuje), se predpomnilnik tiho zavrže in znova napolni ob naslednjem sprehodu. Objekti z `toJSON`, pridobivalniki, dostopniki, simboli ali mesti `[[PrimitiveValue]]` preidejo na serializator, skladen s specifikacijo, in ne napolnijo predpomnilnika, zato predpomnjena pot obravnava le strukturno enotne podatke.

## Hitre poti za vgrajene elemente

Največje pohitritve so nastale, ko smo navidezni stroj naučili obravnavati določene vgrajene elemente:

- **`Atomics.load` / `store` / `compareExchange`** s sodelovalnim razporejanjem `loadSpinHook` / `storeNotifyHook` — znova delujejo 4 testi spin-zank Atomics.
- **`Date.prototype.getTimezoneOffset`** z neposrednim predpomnilnikom zgoščevalne tabele za DST — 45-odstotno pohitritev pri obremenitvenem testu DST SpiderMonkey.
- **`String.fromCharCode`** z vgrajeno hitro potjo za ASCII — pri običajnem pregledu prihrani približno 1 M dispečiranj.
- **`decodeURI` / `decodeURIComponent`** z izjemno hitro potjo za `%XX%XX%XX%XX` z 12 znaki, ki uporablja množično dekodiranje `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** z neposrednim dostopom do preslikave lastnosti.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** z izvornim ponavljanjem po osnovnem polju PHP.

Vsako hitro pot varuje »preverjanje oblike« — če se prejemnik ne ujema s pričakovano obliko (e.g, pogled TypedArray, ki ne temelji na navadnem PHP `string`), se izvajanje v navideznem stroju nadaljuje po počasni poti, skladni s specifikacijo.

## Česa to ne počne

Navidezni stroj z bajtno kodo v smislu V8 **ni JIT**. Ne ustvarja izvorne strojne kode za vsako funkcijo in ne špekulira na podlagi opaženih tipov, razen preverjanja enomodne identitete prototipa v IC. Vgrajevanje na mejah funkcij približno dosega sklad klicev po meri (brez dodatnega okvira PHP za vsak klic JS), namesto da bi telo klicane funkcije prevedel v bajtno kodo klicatelja.

Transpiler JsToPhp je motorju najbližje JIT-u: ustvarja izvorno kodo PHP za vroča telesa funkcij, ki jo sledilni JIT v PHP 8.5 nato prevede v izvorno strojno kodo. JsToPhp deluje prek `eval()`, zato zahteva enkratni strošek prevajanja, sledilni JIT pa mora funkcijo odkriti, kot da bi šlo za uporabniško kodo. Preskok koraka `eval()` z neposrednim ustvarjanjem opkod opcache bi zmanjšal vrzel — glejte [Primerjalni testi § Načrt razvoja](https://phasis.inline0.dev/sl/docs/advanced/benchmarks#roadmap).

## Primerjalni testi

`bench/microbench.js` izvede malo več kot ducat drobnih primerjalnih testov (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). Srednji stenski čas:

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

Trenutne številke se po vsakem zagonu delovnega postopka `bench` zapišejo v `BENCH.md`. Delovna postopka za matriko `bench` in test262 se oba zaženeta ročno.
