
# Tavukoodikäyttöympäristö

Oletussuorituspolku on puukävelijä: se vierailee jokaisessa AST-solmussa ja palauttaa arvon. Usein miljoonia kertoja kutsutuissa funktioissa solmukohtainen käsittelykuorma muodostuu pullonkaulaksi. Phasis lisää päälle spekulatiivisen tavukoodista PHP-sulkeumiksi kääntävän kääntäjän tämän kuorman pienentämiseksi.

## Milloin VM käynnistyy

Jokaisella `JsFunction`-funktiolla on usein kutsumisen laskuri. Kun se ylittää kynnyksen (oletusarvo 100), moottori yrittää kääntää funktion rungon `src/Bytecode/JsToPhp.php`-sulkeumaksi kohteen PHP avulla.

Käännös onnistuu, kun funktion runko sopii tunnettuihin rakenteisiin:

- Paikallismuuttujien sijoitukset, joiden tyypit ovat ennakoitavissa (numeerinen, olio, funktio, taulukko tai merkkijono).
- Lukujen aritmetiikka.
- Jäsenen lukeminen oliotyyppisistä paikallismuuttujista SEKÄ kohteista `this` (`this.v`, `obj.x`).
- Metodikutsut oliotyyppisistä paikallismuuttujista (`obj.method(args)`); vastaanottaja välitetään mukana muodossa `$thisValue`.
- `new Ctor(args)`-konstruktorikutsut — paikallismuuttujan LHS:n tyyppi ylennetään muotoon `object`, ja Bailout-tarkistus varmistaa, ettei palautusarvo ole muu kuin olio.
- `for`- ja `while`-silmukat, joissa on yksinkertaiset ehtorakenteet.
- `return`, jonka lauseke voidaan laskea.
- `if` / `else` tyypitetyillä ehdoilla.

Käännös epäonnistuu (ja funktio jää puukävelijälle), jos runko sisältää seuraavia:

- `with`-lauseet.
- `try / catch / finally` (puukävelijä käsittelee sen oikeiden valmistumissemanttiikkojen mukaisesti).
- `eval()`-kutsut.
- Suora `arguments`-muutos.
- Kaikki, mitä kääntäjä ei vielä osaa muuntaa.

Kun käännös onnistuu, moottori vaihtaa funktion kutsupoluksi käännetyn sulkeuman. Seuraavat kutsut suorittavat natiivia PHP-koodia yhden funktiokutsun käsittelykuormalla AST-solmukohtaisen kuorman sijaan.

## Keskeytykset

Käännetty nopea polku perustuu oletuksiin (luvut pysyvät lukuina, ominaisuudet säilyvät samanmuotoisina). Jos oletusta rikotaan suorituksen aikana, sulkeuma nostaa `Bailout`-poikkeuksen ja suoritus aloitetaan uudelleen puukävelijässä kyseistä kutsua varten.

Tässä määrityksen mukaisuus on tärkeintä. Puun läpikäyjän on voitava suorittaa epäonnistumisen aiheuttanut kutsu alusta uudelleen havaitsematta epäonnistuneen nopean polun sivuvaikutuksia. Phasis hoitaa tämän kahdella tavalla:

1. **Puhtaat etuliitteet** — kääntäjä etsii sivuvaikutuksia ennen spekulointia. Jos funktio suorittaa I/O-toimintoja (console.log, isäntäfunktiot) ennen spekulointikohtaa, nopea polku ei ohita niitä, vaan sitoutuu ja jatkaa.
2. **Kieltäytyminen käännösaikana** — muodoissa, joissa havaittavien sivuvaikutusten peruuttaminen olisi tarpeen (ei-numeerinen `let r = fn()`, jossa `fn()` muuttaa tilaa), kääntäjä kieltäytyy kääntämästä lainkaan. Funktio jää puun läpikäyjälle.

Esimerkki: ei-numeerinen sijoitus suorittaa nopealla polulla `fn()`, aiheuttaa poikkeuksen `Bailout` ja suoritetaan sitten uudelleen puun läpikäyjällä — jolloin `fn()` suoritetaan kahdesti ja sivuvaikutuksia syntyy kahdesti. Käännösaikainen kieltäytyminen estää `fn()`-kutsun suorittamisen nopealla polulla, joten puun läpikäyjä näkee vain yhden kutsun.

## VM:n käskyjen suoritus (toinen kerros)

Osa funktioista kääntyy PHP-sulkeumiksi; muut siirtyvät yksinkertaisempaan tavukooditulkkiin `src/Bytecode/VM.php`. Tavukoodi on litteä joukko käskykoodeja (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), joita `switch`-suoritusmekanismi käsittelee.

VM on tarkoitettu funktioille, joita ei kannata kääntää PHP-sulkeumiksi mutta jotka ovat tarpeeksi usein suoritettavia, jotta AST:n läpikäynnin ohittamisesta on hyötyä. Sisäänrakennettujen objektien metodikutsut (Array.prototype.map, String.prototype.split, …) kulkevat VM:n nopeita polkuja pitkin ja ohittavat yleisen `callFunction`-välittäjän.

## Mukautettu kutsupino

`Op::CALL`, `Op::CALL_METHOD` ja `Op::NEW_CALL` käyttävät sisäkkäistä kutsupolkua, joka pitää koko JS-kutsupinon `VM::execute`:ssä — jokaista JS-kutsua varten ei luoda uutta PHP-kehystä. Kutsun alussa kutsujan tila (pc, koodivirta, operandipino, paikalliset muuttujat, env, `this`, strict-tilan lippu) tallennetaan VM-kohtaiseen `savedFrames`-pinoon, johon sijoitetaan `SavedFrame`-instansseja, ja suorituslooppi päivittää paikalliset muuttujansa kutsuttavan funktion mukaan. `Op::RET` poistaa tallennetun kehyksen pinosta ja palauttaa sen. Poikkeukset purkautuvat tallennettujen kehysten läpi ennen paluuta puun läpikäyjälle.

Soveltuvuus tallennetaan välimuistiin kohteessa `JsFunction`: `vmInlineCallCache` tavallisille kutsuille (BC-käännetty runko, ei `bind()`-sidottu, ei natiivi), `vmInlineConstructCache` kohteelle `new`-lausekkeet (lisäksi `isConstructable`, ei johdettu, ei instanssikenttä- tai yksityisen metodin alustajia). Kaikki muu kulkee yhä kanonisen `callFunction` / `vmNewExpression`-välittäjän kautta.

Arkkitehtuurin sivuhyöty: PHP-pinon yläraja ei enää rajoita JS-rekursion syvyyttä. Nyt ainoa raja on `CallStack::maxDepth` — arvona **4 096**, mikä ylittää reilusti kaikkien kelvollisten test262-tapausten tarpeet (useimmat tarvitsevat alle 2 k kehystä) ja pitää arvon riittävän pienenä, jotta patologiset äärettömän rekursion testit epäonnistuvat nopeasti suorittajan aikakatkaisun puitteissa. Transpiloidun sulkeuman polulla (`JsToPhp`) on oma 8 192-kehyksen rajoitin sisäkkäistämättömälle varapolulle.

## Kehysvedoksiin perustuvat generaattorit

Yksinkertaiset generaattorien rungot — ei `yield*`, ei `yield` try/catch/finally-lohkossa, ei `await` — käännetään tavukoodiksi. Kääntäjä tuottaa uuden `Op::YIELD`-käskykoodin kohtaan, jossa runko keskeytyy; `Op::YIELD` tallentaa suorittimen tilan (cf, pc+1, pino, sp, paikalliset muuttujat, env, thisValue, strict) `GeneratorSnapshot`-keon objektiin ja palauttaa `YieldResult`-sentinelin kohteesta `VM::execute`. Ohjaava `JsGenerator` tallentaa vedoksen ja syöttää sen takaisin kohteelle `VM::execute` seuraavan `next()` / `throw()`-kutsun yhteydessä korvaten operandipinon huipulla olevan jatkoarvon.

Tämä korvaa tuetussa osajoukossa PHP-Fiber-polun. Fiber-pohjainen ohjain (`executeGeneratorBody`) on yhä käytössä muodoille, jotka kääntäjä hylkää — `yield*`-delegointi, generaattorit, jotka tuottavat arvoja try-lohkon sisällä, asynkroniset generaattorit sekä asynkroniset funktiot, joissa on `await`. `JsGenerator` toimii kahdessa tilassa: jokainen `next()` / `throw()` / `return()` tarkistaa, onko instanssilla vedos vai Fiber, ja suorittaa sen mukaisen polun.

Säädettävää suorituskykyasetusta ei ole; soveltuvuus ratkaistaan käännösaikana ja tallennetaan välimuistiin kohteessa `JsFunction`. Vedospolku säästää yhden PHP-kontekstinvaihdon jokaista arvon tuottoa kohti; Fiber-polku toimii ennallaan kaikessa muussa.

## Sisäiset välimuistit

`Op::LOAD_MEMBER` sisältää ohjelmalaskurikohtaisen sisäisen välimuistin, jonka avaimena on prototyypin identiteetti. Sitä käytetään tilanteissa, joissa prototyypin metodia kutsutaan instanssilla (`c.sum(...)`, `arr.push(...)` ja jokaisen luokan metodin suoritus). Osuman ehdot: vastaanottajalla ei ole omaa paikkakenttää nimelle, välimuistiin tallennettu prototyyppiviite on identtinen vastaanottajan `getPrototype()`-viitteen kanssa ja prototyypin `PropertyMap::$version` vastaa yhä tallennettua versiota.

Välimuistin mitätöinti perustuu kohteeseen `PropertyMap::$version`. Rakenteelliset muutokset (asetus / poisto) kasvattavat sen arvoa aina. Data-paikkakentän arvon korvaaminen kasvattaa sitä vain, kun `PropertyMap::$isUsedAsProto` on tosi — kyseessä on kirjoituksenvalvontalippu, joka asetetaan ensimmäisellä kerralla, kun objekti havaitaan toisen objektin `[[Prototype]]`-ominaisuutena. Instanssiobjekteille, jotka eivät koskaan toimi prototyyppeinä, välimuisti-infrastruktuurista ei koidu lisäkustannuksia jokaisella `obj.x = v`-kerralla.

Getter-ominaisuuksia ei koskaan tallenneta välimuistiin (ne on suoritettava uudelleen jokaisella lukukerralla).

`PropertyMap` sisältää toisen laskurin, `$mutationVersion`, jonka arvoa kasvatetaan *aina* jokaisen kirjoituksen yhteydessä (ilman kirjoituksenvalvontaa). `JSON.stringify`-välimuisti kiinnittää tämän laskurin jokaiseen läpikäytyyn sisäkkäiseen objektiin/taulukkoon, jotta pelkkään instanssiin kohdistuva muutos, kuten `o.a = 99`, mitätöi välimuistissa olevan merkkijonon oikein silloinkin, kun IC:n `$version` olisi muuten ohittanut laskurin kasvattamisen.

## JSON.stringify-välimuisti

`JsObject` sisältää kohteet `$jsonCacheString` + `$jsonCacheVersions` (luettelon `[obj, mutationVersion]`-pareista jokaiselle triviaalin merkkijonoksi muunnon aikana läpikäydylle sisäkkäiselle objektille). Seuraavalla `JSON.stringify(o)`-kutsulla ilman korvaajaa tai välilyöntiä triviaali nopea polku tarkistaa, ettei minkään kiinnitetyn version arvo ole muuttunut, ja palauttaa välimuistissa olevan merkkijonon käymättä rakennetta uudelleen läpi tai koodaamatta sitä uudelleen. Usein käytetyt syvän kopioinnin mallit, kuten `JSON.parse(JSON.stringify(o))`, nopeuttavat merkkijonoksi muunnon puoliskoa noin kaksinkertaisesti.

Välimuisti tyhjennetään hiljaisesti osuman puuttuessa (kun jonkin kiinnitetyn version arvo eroaa) ja täytetään seuraavalla läpikäynnillä. Objektit, joissa on `toJSON`, gettereitä, asettajia, symboleja tai `[[PrimitiveValue]]`-paikkakenttiä, siirtyvät määrityksen mukaiseen sarjallistajaan eivätkä koskaan täytä välimuistia — näin välimuistipolulla käsitellään vain rakenteeltaan yhdenmukaista dataa.

## Sisäänrakennettujen toimintojen nopeat polut

Suurimmat suorituskykyparannukset saatiin opettamalla VM tuntemaan tietyt sisäänrakennetut toiminnot:

- **`Atomics.load` / `store` / `compareExchange`** ja `loadSpinHook` / `storeNotifyHook`-yhteistoiminnallinen ajoitus — palautti käyttöön 4 Atomics-spin-loop-testiä.
- **`Date.prototype.getTimezoneOffset`** suoralla hajautustaulun DST-välimuistilla — 45 % nopeutus SpiderMonkey-DST-rasitustestissä.
- **`String.fromCharCode`** sisäkkäisellä ASCII-nopealla polulla — säästää noin 1 M suoritusaskelta tyypillistä läpikäyntiä kohti.
- **`decodeURI` / `decodeURIComponent`** 12 merkin `%XX%XX%XX%XX`-ultranopealla polulla, joka käyttää kohteen `hex2bin` massapurkua.
- **`Object.keys` / `Object.values` / `Object.entries`** suoralla ominaisuuskarttaan pääsyllä.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** alla olevan PHP-taulukon natiivilla läpikäynnillä.

Jokainen nopea polku on rajattu "rakennetarkistuksella" — jos vastaanottaja ei vastaa odotettua rakennetta (e.g. TypedArray-näkymä, joka ei perustu tavalliseen PHP `string`:ään), VM siirtyy määrityksen mukaiseen hitaaseen polkuun.

## Mitä tämä ei tee

Tavukoodi-VM **ei ole JIT** merkityksessä V8. Se ei tuota natiivia konekoodia funktiokohtaisesti eikä spekuloi havaittujen tyyppien perusteella IC:n monomorfista prototyypin identiteettitarkistusta pidemmälle. Funktiorajojen sisäkkäistä kutsua jäljitellään mukautetulla kutsupinolla (jokaiselle JS-kutsulle ei luoda ylimääräistä PHP-kehystä), eikä kääntämällä kutsuttavan funktion runkoa kutsujan tavukoodiksi.

JsToPhp-transpilointiohjelma on moottorin JIT:iä lähinnä oleva osa: se tuottaa PHP-lähdekoodia usein suoritettaville funktiorungoille, jonka PHP 8.5:n jäljittävä JIT kääntää sitten natiiviksi koodiksi. JsToPhp kulkee kohteen `eval()` kautta, joten siitä aiheutuu kertaluonteinen käännöskustannus ja jäljittävän JIT:n on löydettävä funktio aivan kuin se olisi käyttäjän koodia. Eroa saataisiin pienennettyä ohittamalla `eval()`-vaihe ja tuottamalla opcache-käskyt suoraan — katso [Vertailutestit § Tiekartta](https://phasis.inline0.dev/fi/docs/advanced/benchmarks#roadmap).

## Vertailutesti

`bench/microbench.js` suorittaa runsaat tusinan verran pieniä vertailutestejä (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). Ajoajan mediaani:

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

Nykyiset tulokset tallennetaan kohteeseen `BENCH.md` jokaisen `bench`-työnkulkusuorituksen jälkeen. Työnkulut `bench` ja test262-matriisi käynnistetään kumpikin käsin.
