
# Bájtkód-VM

Az alapértelmezett végrehajtási útvonal egy fastruktúra-bejáró: végigjár minden AST-csomópontot, és visszaad egy értéket. A milliószor meghívott, gyakran futó függvényeknél a csomópontonkénti diszpécselés többletköltsége jelenti a szűk keresztmetszetet. A Phasis egy spekulatív, bájtkódból PHP lezárást fordító réteget épít rá, hogy csökkentse ezt a többletköltséget.

## Mikor lép működésbe a VM

Minden `JsFunction` rendelkezik egy gyakori hívásokat számlálóval. Amikor ez átlép egy küszöböt (alapértelmezés szerint 100), a motor megpróbálja a függvénytörzset `src/Bytecode/JsToPhp.php` segítségével PHP lezárássá fordítani.

A fordítás akkor sikerül, ha a függvénytörzs az ismert minták valamelyikébe illik:

- Lokális változók értékadása kiszámítható típusokkal (numerikus, objektum, függvény, tömb, karakterlánc).
- Számokon végzett aritmetika.
- Objektumtípusú lokális változókon ÉS `this`-n végzett tagolvasások (`this.v`, `obj.x`).
- Objektumtípusú lokális változókon végzett metódushívások (`obj.method(args)`), ahol a fogadó `$thisValue`-ként kerül továbbításra.
- `new Ctor(args)` konstruktorhívások — az értékadás bal oldalán lévő lokális változó típusát `object`-ra emelik, és visszaesésellenőrzéssel kezelik a nem objektum visszatérési értéket.
- Egyszerű feltételmintákat használó `for` / `while` ciklusok.
- Számítható kifejezéssel rendelkező `return`.
- `if` / `else` tipizált feltételek felett.

A fordítás sikertelen (a függvény a fastruktúra-bejárón marad), ha a törzs a következőket tartalmazza:

- `with` utasítások.
- `try / catch / finally` (a helyes befejezési szemantika érdekében a fastruktúra-bejáró kezeli).
- `eval()` hívások.
- Közvetlen `arguments` módosítás.
- Bármi, amit a fordító még nem tud lefordítani.

Sikeres fordításkor a motor a függvény hívási útvonalát a lefordított lezárásra cseréli. A további hívások natív PHP kódot futtatnak, hívásonként egyetlen függvényhívási többletköltséggel, nem pedig AST-csomópontonként.

## Visszaesések

A lefordított gyors útvonal feltételezésekkel él (a számok számok maradnak, a tulajdonságok alakja nem változik). Ha futásidőben valamelyik feltételezés sérül, a lezárás `Bailout` hibát dob, és az adott hívás a fastruktúra-bejáróban indul újra.

Itt a legfontosabb a specifikációnak való megfelelés. A fastruktúra-bejárónak képesnek kell lennie az egész visszaeső hívást elölről végrehajtani, anélkül hogy a sikertelen gyors útvonal mellékhatásai megfigyelhetők lennének. A Phasis ezt kétféleképpen kezeli:

1. **Mellékhatásmentes előtagok** — a fordító minden spekuláció előtt megkeresi a mellékhatásokat. Ha a függvény a spekuláció előtt I/O-t végez (console.log, gazdafüggvények), a gyors útvonal nem hagyja ki: véglegesíti, majd folytatja a végrehajtást.
2. **Fordításkori elutasítás** — azoknál a mintáknál, amelyeknél egy megfigyelhető mellékhatás visszagörgetése kellene (nem numerikus `let r = fn()`, amelynél a `fn()` módosítja az állapotot), a fordító teljesen elutasítja a fordítást. A függvény a fastruktúra-bejárón marad.

A motiváló eset: egy nem numerikus értékadás végrehajtja a `fn()` kódot a gyors útvonalon, `Bailout` hibát dob, majd újra végrehajtódik a fastruktúra-bejáróban — így a `fn()` kétszer fut le, két készletnyi mellékhatással. A fordításkori elutasítás megakadályozza, hogy a `fn()` egyáltalán lefusson a gyors útvonalon, így a fastruktúra-bejáró az egyetlen hívást látja.

## VM-diszpécselés (a második réteg)

Egyes függvények PHP lezárásokká fordulnak; mások az `src/Bytecode/VM.php` még egyszerűbb bájtkód-értelmezőjéhez kerülnek. A bájtkód egy lapos opcode-tömb (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), amelyet egy `switch` diszpécselés dolgoz fel.

A VM az olyan függvényekhez kell, amelyeket nem érdemes PHP lezárássá fordítani, de elég gyakran futnak ahhoz, hogy megérje kihagyni az AST bejárását. A beépített objektumokon végzett metódushívások (Array.prototype.map, String.prototype.split, …) a VM gyors útvonalain haladnak át, megkerülve az általános `callFunction` közvetítőt.

## Egyedi hívásiverem-kezelés

A `Op::CALL`, `Op::CALL_METHOD` és `Op::NEW_CALL` inline hívási útvonalat használ; ez a teljes JS hívási vermet a `VM::execute`-n belül tartja — minden JS híváshoz nem jön létre új PHP keret. Belépéskor a hívó állapota (pc, kódfolyam, operandusverem, lokális változók, env, `this`, szigorú mód jelzője) egy VM-enkénti, `SavedFrame` példányokból álló `savedFrames` készletbe kerül, a diszpécselési ciklus pedig átállítja a lokális változóit a hívott függvényre. A `Op::RET` kiveszi és visszaállítja az elmentett keretet. A kivételek a mentett kereteken keresztül bontják le a hívási vermet, mielőtt a végrehajtás visszatér a fastruktúra-bejáróhoz.

A jogosultság `JsFunction`-n van eltárolva: `vmInlineCallCache` normál hívásoknál (BC-fordított törzs, nincs `bind()`-hoz kötve, nem natív), `vmInlineConstructCache` pedig a `new` kifejezéseknél (továbbá `isConstructable`, nem származtatott, nincsenek példánymező- vagy privátmetódus-inicializálók). Minden más a kanonikus `callFunction` / `vmNewExpression` közvetítőn halad tovább.

Ennek a felépítésnek az előnye, hogy a PHP-verem korlátja többé nem szab határt a JS rekurzió mélységének. Most már csak a `CallStack::maxDepth` szab korlátot — az értéke **4 096**, kényelmesen minden szabályos test262 eset fölött van (a legtöbbhöz 2 k-nál kevesebb keret kell), ugyanakkor elég alacsony ahhoz, hogy a kóros végtelen rekurziós tesztek gyorsan meghiúsuljanak a futtató időkorlátján belül. A transzpilált lezárások útvonalának (`JsToPhp`) saját, 8 192 keretes korlátja van a nem inline végrehajtású visszaeséshez.

## Keretpillanatképes generátorok

Az egyszerű generátortörzsek — `yield*` nélkül, try/catch/finally blokkon belüli `yield` nélkül, `await` nélkül — bájtkódra fordulnak. A fordító új `Op::YIELD` opcode-ot bocsát ki ott, ahol a törzs felfüggeszti a végrehajtást; a `Op::YIELD` a diszpécselés állapotát (cf, pc+1, verem, sp, lokális változók, env, thisValue, strict) egy `GeneratorSnapshot` halomobjektumba menti, majd `VM::execute`-ből egy `YieldResult` jelzőértéket ad vissza. A vezérlő `JsGenerator` eltárolja a pillanatképet, és a következő `next()` / `throw()` során visszaadja a `VM::execute`-nek, lecserélve az operandusverem tetején lévő folytatási értéket.

Ez felváltja a támogatott részhalmaznál a PHP-Fiber útvonalat. A Fiber-alapú vezérlő (`executeGeneratorBody`) továbbra is megmarad azokhoz a mintákhoz, amelyeket a fordító elutasít — `yield*`-delegálás, try-blokkon belüli yieldet tartalmazó generátorok, aszinkron generátorok, valamint `await`-t tartalmazó aszinkron függvények. A `JsGenerator` kétmódú: minden `next()` / `throw()` / `return()` ellenőrzi, hogy a példány pillanatképet vagy Fiber-t tartalmaz-e, majd ennek megfelelően indítja el a diszpécselést.

Nincs a méréshez hangolható beállítás; a jogosultság fordításkor dől el, majd `JsFunction`-n van eltárolva. A pillanatképes útvonal előnye, hogy yieldenként eggyel kevesebb PHP kontextusváltás történik; minden másnál a Fiber-útvonal változatlan.

## Inline gyorsítótárak

A `Op::LOAD_MEMBER` minden PC-hez prototípusazonosság alapján kulcsolt inline gyorsítótárat tart fenn, amelyet a prototípusmetódus-példányon minta használ (`c.sum(...)`, `arr.push(...)`, minden osztálymetódus-hívás). Találati feltétel: a fogadónak nincs saját mezője az adott névhez, a gyorsítótárazott prototípust ugyanaz a hivatkozás azonosítja, mint a fogadó `getPrototype()` értékét, és a prototípus `PropertyMap::$version` értéke továbbra is megegyezik az eltárolt verzióval.

A gyorsítótár érvénytelenítése a `PropertyMap::$version` értékén alapul. A strukturális módosítások (set / delete) feltétel nélkül növelik. Az adatmezők értékének felülírása csak akkor növeli, ha `PropertyMap::$isUsedAsProto` igaz — ez egy írási korlátot jelző flag, amely akkor vált át, amikor az objektumot először egy másik objektum `[[Prototype]]` értékeként észlelik. Azok a példányobjektumok, amelyek soha nem szolgálnak prototípusként, minden `obj.x = v` esetén megússzák az IC-infrastruktúra költségét.

A getter hozzáférők soha nincsenek gyorsítótárazva (minden olvasáskor újra le kell futniuk).

A `PropertyMap` egy második számlálót is fenntart, a `$mutationVersion` értékét, amely minden íráskor *feltétel nélkül* nő (írási korlát nélkül). A `JSON.stringify` gyorsítótár minden bejárt beágyazott objektumhoz/tömbhöz rögzíti ezt a számlálót, így egy csak példányt érintő módosítás, például a `o.a = 99`, helyesen érvényteleníti a gyorsítótárazott karakterláncot akkor is, ha az IC `$version` értéke kihagyta volna a növelést.

## JSON.stringify gyorsítótár

A `JsObject` tárolja a `$jsonCacheString` + `$jsonCacheVersions` értékét (az összes bejárt beágyazott objektumhoz tartozó `[obj, mutationVersion]` párok listáját a triviális karakterlánccá alakítás során). A következő `JSON.stringify(o)` híváskor, ha nincs replacer / space, a triviális gyorsút ellenőrzi, hogy minden rögzített verzió változatlan-e, majd újrajárás és újrakódolás nélkül visszaadja a gyorsítótárazott karakterláncot. Az olyan gyakori mélymásolási minták, mint a `JSON.parse(JSON.stringify(o))`, nagyjából kétszeres gyorsulást érnek el a stringify fázisban.

Találat hiányakor (ha bármelyik rögzített verzió eltér) a gyorsítótár csendben törlődik, és a következő bejárás újratölti. A `toJSON` értékkel, getterekkel, hozzáférőkkel, szimbólumokkal vagy `[[PrimitiveValue]]` mezőkkel rendelkező objektumok visszatérnek a specifikáció szerinti szerializálóhoz, és soha nem töltik fel a gyorsítótárat — így a gyorsítótárazott útvonal csak szerkezetileg egységes adatokat lát.

## Beépített gyorsutak

A legnagyobb teljesítménynövekedést az hozta, hogy a VM-et konkrét beépített elemekre optimalizáltuk:

- **`Atomics.load` / `store` / `compareExchange`**, `loadSpinHook` / `storeNotifyHook` együttműködő ütemezéssel — 4 Atomics spin-loop tesztet sikerült helyreállítani.
- **`Date.prototype.getTimezoneOffset`**, közvetlen hashmap-alapú DST-gyorsítótárral — 45%-os gyorsulás a SpiderMonkey DST-terhelési tesztben.
- **`String.fromCharCode`**, inline ASCII gyorsúttal — egy tipikus menetben körülbelül 1 millió hívást takarít meg.
- **`decodeURI` / `decodeURIComponent`**, 12 karakteres `%XX%XX%XX%XX` ultragyors úttal, amely `hex2bin` tömeges dekódolást használ.
- **`Object.keys` / `Object.values` / `Object.entries`**, közvetlen tulajdonságtérkép-hozzáféréssel.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`**, a mögöttes PHP tömb natív bejárásával.

Minden gyorsutat „alakellenőrzés” véd — ha a fogadó nem a várt alakú (e.g. egy olyan TypedArray-nézet, amelyet nem egy egyszerű PHP `string` támaszt alá), a VM a specifikációnak megfelelő lassú útra tér át.

## Mire nem szolgál

A bytecode VM a V8 értelmében **nem JIT**. Nem generál natív gépi kódot függvényenként, és az IC monomorf prototípusazonosság-ellenőrzésén túl nem következtet a megfigyelt típusok alapján. A függvényhatárokon átívelő inline-olást az egyedi hívásverem közelíti meg (minden JS híváskor nincs plusz PHP keret), nem pedig a hívott függvény törzsének beemelése a hívó bytecode-jába.

A JsToPhp transzpiler áll a legközelebb a motor JIT-jéhez: PHP forráskódot generál a gyakran futó függvénytörzsekhez, amelyet a PHP 8.5 nyomkövető JIT-je natív kódra fordít. A JsToPhp a `eval()` útján működik, ezért egyszeri fordítási költséggel jár, a nyomkövető JIT-nek pedig úgy kell felfedeznie a függvényt, mintha felhasználói kód lenne. A `eval()` lépés kihagyása közvetlen opcache-opkódok kibocsátásával tovább csökkentené a különbséget — lásd: [Benchmarks § Roadmap](https://phasis.inline0.dev/hu/docs/advanced/benchmarks#roadmap).

## Benchmarkok

A `bench/microbench.js` tucatnyi apró benchmarkot futtat (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 futásidő:

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

Az aktuális eredményeket minden `bench` munkafolyamat-futás után rögzítjük a `BENCH.md` fájlban. A `bench` és test262 mátrixmunkafolyamatokat kézzel indítják el.
