
# Baitkodo VM

Numatytasis vykdymo kelias yra medžio vaikščiotuvas — pereinama per kiekvieną AST mazgą ir grąžinama reikšmė. Aktyvioms funkcijoms, iškviečiamoms milijonus kartų, kliūtimi tampa kiekvieno mazgo iškvietimų apdorojimo sąnaudos. Phasis šalia prideda spekuliatyvų baitkodo kompiliatorių, kuris kodą paverčia PHP uždarymais, kad šios sąnaudos sumažėtų.

## Kada įsijungia VM

Kiekviena `JsFunction` turi aktyvių iškvietimų skaitiklį. Jam viršijus slenkstį (numatytoji reikšmė 100), variklis bando funkcijos turinį per `src/Bytecode/JsToPhp.php` kompiliuoti į PHP uždarymą.

Kompiliuoti pavyksta, kai funkcijos turinys atitinka vieną iš žinomų formų:

- Nuspėjamų tipų vietinių kintamųjų priskyrimai (skaitinių reikšmių, objektų, funkcijų, masyvų, eilučių).
- Skaičių aritmetika.
- Objektinio tipo vietinių kintamųjų IR `this` (`this.v`, `obj.x`) elementų skaitymas.
- Objektinio tipo vietinių kintamųjų metodų iškvietimai (`obj.method(args)`), gavėją perduodant kaip `$thisValue`.
- `new Ctor(args)` konstruktoriaus iškvietimai — LHS vietiniam kintamajam priskiriamas `object` tipas, o ne objektinės grąžinamos reikšmės atveju aktyvinama kompiliavimo atsisakymo apsauga.
- `for` / `while` ciklai su paprastomis sąlygomis.
- `return` su apskaičiuojama išraiška.
- `if` / `else`, kurių sąlygos yra tipizuotos.

Kompiliuoti nepavyksta (funkcija lieka medžio vaikščiotuve), jei turinyje yra:

- `with` sakinių.
- `try / catch / finally` (tinkamą užbaigimo semantiką užtikrina medžio vaikščiotuvas).
- `eval()` iškvietimų.
- Tiesioginis `arguments` keitimas.
- Bet kas, ko kompiliatorius dar nemoka paversti žemesnio lygmens kodu.

Kai kompiliuoti pavyksta, variklis pakeičia funkcijos iškvietimo kelią į kompiliuotą uždarymą. Vėlesni iškvietimai vykdo savąjį PHP kodą, patirdami vieno funkcijos iškvietimo sąnaudas, o ne sąnaudas už kiekvieną AST mazgą.

## Kompiliavimo atsisakymas

Kompiliuotas greitasis kelias daro prielaidas (skaičiai lieka skaičiais, savybės išlieka tos pačios formos). Kai vykdymo metu prielaida pažeidžiama, uždarymas išmeta `Bailout`, o vykdymas tam iškvietimui pradedamas iš naujo medžio vaikščiotuve.

Čia specifikacijos laikymasis svarbiausias. Medžio vaikščiotuvas turi galėti nuo pradžių pakartotinai vykdyti kompiliavimo atsisakymą sukėlusį iškvietimą, nepastebėdamas jokių nepavykusio greitojo kelio šalutinių poveikių. Phasis tai užtikrina dviem būdais:

1. **Šalutinio poveikio neturinčios pradžios dalys** — kompiliatorius prieš bet kokias spekuliacijas ieško šalutinių poveikių. Jei funkcija iki spekuliavimo taško atlieka įvesties ar išvesties operacijas (console.log, pagrindinės aplinkos funkcijas), greitasis kelias jų nepraleidžia, o patvirtina ir tęsia vykdymą.
2. **Atsisakymas kompiliuoti** — kai formai reikėtų atšaukti pastebimus šalutinius poveikius (ne skaitinė `let r = fn()`, kurioje `fn()` keičia būseną), kompiliatorius atsisako ją kompiliuoti. Funkcija lieka medžio vaikščiotuve.

Pagrindinis pavyzdys: greitajame kelyje atliekamas nepriskaitinis priskyrimas, vykdomas `fn()`, išmetama `Bailout`, o tada medžio vaikščiotuvas jį vykdo iš naujo — `fn()` paleidžiama du kartus ir sukeliami du šalutinių poveikių rinkiniai. Atsisakant kompiliuoti užtikrinama, kad `fn()` apskritai nebus vykdomas greitajame kelyje, todėl medžio vaikščiotuvas matys tik vieną iškvietimą.

## VM iškvietimų apdorojimas (antrasis sluoksnis)

Kai kurios funkcijos kompiliuojamos į PHP uždarymus, o kitos nukreipiamos į dar paprastesnį baitkodo interpretatorių, esantį `src/Bytecode/VM.php`. Baitkodas yra plokščias operacijų kodų masyvas (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), kurį apdoroja `switch`.

VM naudojama funkcijoms, kurių neapsimoka kompiliuoti į PHP uždarymą, bet kurios vis tiek pakankamai dažnai iškviečiamos, kad būtų naudinga praleisti AST perėjimą. Įtaisytųjų objektų (Array.prototype.map, String.prototype.split, …) metodų iškvietimai apdorojami VM greituosiuose keliuose, kurie apeina bendrąjį `callFunction` tarpinį iškvietimą.

## Tinkinta iškvietimų steko realizacija

`Op::CALL`, `Op::CALL_METHOD` ir `Op::NEW_CALL` naudoja įterptinių iškvietimų kelią, kuriame visas JS iškvietimų stekas laikomas `VM::execute` viduje — kiekvienam JS iškvietimui nesukuriamas naujas PHP kadras. Įeinant iškvietusiojo būsena (pc, kodo srautas, operandų stekas, vietiniai kintamieji, env, `this`, griežtojo režimo vėliavėlė) įrašoma į kiekvienos VM `savedFrames` telkinį, kuriame laikomi `SavedFrame` egzemplioriai, o iškvietimų ciklo vietiniai kintamieji perkonfigūruojami pagal iškviečiamą funkciją. `Op::RET` išima išsaugotą kadrą ir atkuria būseną. Išimtys išvynioja išsaugotus kadrus, o tada vykdymas grąžinamas medžio vaikščiotuvui.

Tinkamumas įsimenamas `JsFunction`: `vmInlineCallCache` įprastiems iškvietimams (baitkodu sukompiliuotas turinys, nesusietas su `bind()`, nesavasis), `vmInlineConstructCache`, skirtiems `new` išraiškoms (papildomai `isConstructable`, ne išvestinis, be egzempliorių laukų ar privačių metodų inicializatorių). Visa kita apdorojama įprastu `callFunction` / `vmNewExpression` tarpiniu iškvietimu.

Architektūrinis pranašumas: PHP steko riba neberiboja JS rekursijos gylio. Dabar vienintelė riba yra `CallStack::maxDepth` — nustatyta į **4 096**. Ji gerokai aukštesnė už visų teisėtų test262 atvejų poreikius (daugumai reikia mažiau nei 2 kadrų) ir pakankamai žema, kad patologiniai begalinės rekursijos bandymai greitai pasiektų vykdyklės skirtąjį laiką. Transpiliuojamų uždarymų kelias (`JsToPhp`) turi atskirą 8 192 kadrų apsaugą atsarginiam keliui, kai iškvietimai neįterpiami.

## Generatoriai su kadrų momentinėmis kopijomis

Paprastų generatorių turiniai — be `yield*`, be `yield` bloke try/catch/finally ir be `await` — kompiliuojami į baitkodą. Kompiliatorius sustabdymo vietoje pateikia naują `Op::YIELD` operacijos kodą; `Op::YIELD` įrašo iškvietimų būseną (cf, pc+1, stekas, sp, vietiniai kintamieji, env, thisValue, strict) į `GeneratorSnapshot` krūvos objektą ir iš `VM::execute` grąžina `YieldResult` žymeklį. Vykdantysis `JsGenerator` saugo momentinę kopiją ir kito `next()` / `throw()` metu grąžina ją į `VM::execute`, pakeisdamas viršuje esantį operandų steko reikšmę, grąžintą po tęsimo.

Tai pakeičia palaikomam poaibiui skirtą PHP Fiber kelią. Fiber pagrįstas vykdiklis (`executeGeneratorBody`) tebeveikia formoms, kurias kompiliatorius atmeta — `yield*` delegavimui, generatoriams, kurie pateikia reikšmę try bloke, asinchroniniams generatoriams, asinchroninėms funkcijoms su `await`. `JsGenerator` veikia dviem režimais: kiekvienas `next()` / `throw()` / `return()` patikrina, ar egzempliorius turi momentinę kopiją, ar Fiber, ir atitinkamai vykdo.

Nėra našumo bandymams skirto keičiamo parametro; tinkamumas nustatomas kompiliavimo metu ir įsimenamas `JsFunction`. Momentinių kopijų kelyje kiekvienam pateikimui reikia vienu PHP konteksto perjungimu mažiau; Fiber kelias visa kita apdoroja kaip ir anksčiau.

## Įterptiniai podėliai

`Op::LOAD_MEMBER` kiekvienam PC saugo įterptinį podėlį, kurio raktas yra prototipo tapatybė. Jis naudojamas prototipo metodo egzemplioriuje šablonui (`c.sum(...)`, `arr.push(...)`, kiekvienam klasės metodo iškvietimui). Podėlio atitikties sąlygos: gavėjas neturi nuosavo lizdo šiam pavadinimui, podėlyje išsaugota prototipo nuoroda sutampa su gavėjo `getPrototype()`, o prototipo `PropertyMap::$version` vis dar sutampa su užfiksuota versija.

Podėlio anuliavimas vykdomas per `PropertyMap::$version`. Struktūriniai pakeitimai (nustatymas / trynimas) jį padidina be išimčių. Duomenų lizdo reikšmės perrašymas skaitiklį padidina tik tada, kai `PropertyMap::$isUsedAsProto` yra true — tai rašymo barjero vėliavėlė, pirmą kartą įjungiama pastebėjus, kad objektas naudojamas kaip kito objekto `[[Prototype]]`. Egzempliorių objektai, kurie niekada nenaudojami kaip prototipai, už kiekvieną `obj.x = v` nemoka už įterptinių podėlių infrastruktūrą.

Gavimo prieigos priemonės niekada nepodėliuojamos (jas būtina iš naujo vykdyti kiekvieną kartą skaitant).

`PropertyMap` turi antrą skaitiklį `$mutationVersion`, kuris didinamas *kiekvieno* įrašymo metu (be rašymo barjero). `JSON.stringify` podėlis kiekvienam peržiūrėtam įdėtam objektui ar masyvui užfiksuoja šį skaitiklį, kad vien egzemplioriuje atliktas pakeitimas, pavyzdžiui, `o.a = 99`, tinkamai anuliuotų podėliuotą eilutę net tada, kai įterptinio podėlio `$version` būtų praleidęs skaitiklio padidinimą.

## JSON.stringify podėlis

`JsObject` saugo `$jsonCacheString` + `$jsonCacheVersions` (`[obj, mutationVersion]` porų sąrašą kiekvienam per įprastą eilutės serializavimą peržiūrėtam įdėtam objektui). Kito `JSON.stringify(o)` iškvietimo metu, jei nėra keitiklio / tarpų, įprastas greitasis kelias patikrina, ar nepasikeitė nė viena užfiksuota versija, ir grąžina podėliuotą eilutę iš naujo neperžiūrėdamas ar nekoduodamas duomenų. Dažnai pasikartojantys gilaus klonavimo šablonai, tokie kaip `JSON.parse(JSON.stringify(o))`, eilutės serializavimo dalį paspartina maždaug 2 kartus.

Praleidus podėlį, jis tyliai pašalinamas (jei skiriasi kuri nors užfiksuota versija), o kitos peržiūros metu vėl užpildomas. Objektai su `toJSON`, gavimo prieigos priemonėmis, simboliais ar `[[PrimitiveValue]]` lizdais perduodami specifikacijos serializatoriui ir niekada nepatenka į podėlį — todėl podėliuotas kelias apdoroja tik struktūriškai vienodus duomenis.

## Įtaisytųjų funkcijų spartieji keliai

Didžiausias našumo prieaugis pasiektas išmokius VM tvarkyti konkrečias įtaisytąsias funkcijas:

- **`Atomics.load` / `store` / `compareExchange`** su kooperatyviu planavimu per `loadSpinHook` / `storeNotifyHook` — vėl sėkmingai veikia 4 Atomics sukimosi ciklo bandymai.
- **`Date.prototype.getTimezoneOffset`** su tiesiogine maišos lentelės DST podėlio paieška — SpiderMonkey DST apkrovos bandymas 45 % greitesnis.
- **`String.fromCharCode`** su įterptu ASCII greituoju keliu — per įprastą perėjimą išsaugoma apie 1 M iškvietimų apdorojimų.
- **`decodeURI` / `decodeURIComponent`** su 12 simbolių `%XX%XX%XX%XX` itin sparčiuoju keliu, naudojančiu `hex2bin` masinį dekodavimą.
- **`Object.keys` / `Object.values` / `Object.entries`** su tiesiogine prieiga prie savybių žemėlapio.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** su savuoju iteravimu per pagrindinį PHP masyvą.

Kiekvienas spartusis kelias apsaugotas „formos patikra“ — jei gavėjo forma neatitinka numatytosios (e.g, TypedArray peržiūra, kurios pagrindas nėra įprastas PHP `string`), VM pereina prie lėtesnio kelio, atitinkančio specifikaciją.

## Ko tai nedaro

Baitkodo VM **nėra JIT** V8 prasme. Ji negeneruoja kiekvienai funkcijai savojo mašininio kodo ir nedaro spekuliacijų pagal pastebėtus tipus, išskyrus monomorfinę prototipo tapatybės patikrą įterptiniame podėlyje. Funkcijų ribų įterpimas apytikriai pasiekiamas tinkinta iškvietimų steko realizacija (kiekvienam JS iškvietimui nėra papildomo PHP kadro), o ne iškviečiamos funkcijos turinį perkeliant į iškvietusios funkcijos baitkodą.

JsToPhp transpiliatorius yra artimiausias variklio JIT atitikmuo: aktyvių funkcijų turiniui jis generuoja PHP kodą, kurį PHP 8.5 sekimo JIT kompiliatorius paverčia savuoju kodu. JsToPhp veikia per `eval()`, todėl patiria vienkartines kompiliavimo sąnaudas, o sekimo JIT kompiliatorius turi aptikti funkciją taip, lyg ji būtų naudotojo kodas. Dar labiau sumažinti atsilikimą būtų galima praleidus `eval()` žingsnį ir tiesiogiai generuojant opcache operacijų kodus — žr. [Našumo bandymai § Kūrimo planas](https://phasis.inline0.dev/lt/docs/advanced/benchmarks#roadmap).

## Našumo bandymai

`bench/microbench.js` vykdo keliolika nedidelių našumo bandymų (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). Medianinis sieninis laikas:

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

Naujausi skaičiai po kiekvieno `bench` darbo eigos vykdymo įrašomi į `BENCH.md`. `bench` ir test262 matricų darbo eigos paleidžiamos rankiniu būdu.
