
# Bytecode-VM

Standardstien til afvikling er en træfortolker — besøg hver AST-node, og returnér en værdi. For varme funktioner, der kaldes millioner af gange, er dispatch-overheadet pr. node flaskehalsen. Phasis tilføjer en spekulativ compiler fra bytecode til PHP-closures for at reducere dette overhead.

## Hvornår VM'en aktiveres

Hver `JsFunction` har en tæller for varme kald. Når den overskrider en tærskel (som standard 100), forsøger motoren at kompilere funktionens body til en PHP-closure via `src/Bytecode/JsToPhp.php`.

Kompileringen lykkes, når funktionens body passer til et kendt sæt former:

- Tildelinger til lokale variabler med forudsigelige typer (tal, objekt, funktion, array, streng).
- Aritmetik med tal.
- Medlemsopslag på lokale variabler med objekttype OG på `this` (`this.v`, `obj.x`).
- Metodekald på lokale variabler med objekttype (`obj.method(args)`), hvor modtageren sendes med som `$thisValue`.
- Kald af `new Ctor(args)`-konstruktører — opgraderer LHS-lokalvariablen til `object` som type og sætter en Bailout-vagt ved retur af andet end et objekt.
- Løkker af typen `for` / `while` med enkle betingelser.
- `return` med et beregneligt udtryk.
- `if` / `else` over typede betingelser.

Kompileringen mislykkes (og funktionen forbliver på træfortolkeren), når bodyen indeholder:

- `with`-sætninger.
- `try / catch / finally` (håndteres af træfortolkeren for at sikre korrekt completion-semantik).
- `eval()`-kald.
- Direkte mutation af `arguments`.
- Alt, som compileren endnu ikke ved, hvordan den skal sænke.

Når kompileringen lykkes, skifter motoren funktionens kaldesti til den kompilerede closure. Efterfølgende kald afvikler native PHP med overhead fra ét funktionskald i stedet for ét pr. AST-node.

## Fallbacks

Den kompilerede hurtige sti bygger på antagelser (at tal forbliver tal, og at egenskaber forbliver på samme form). Når en antagelse brydes under kørsel, kaster closuren en `Bailout`, og afviklingen genstartes i træfortolkeren for det pågældende kald.

Det er her, korrekthed i forhold til specifikationen betyder mest. Træfortolkeren skal kunne genafvikle det kald, der udløste fallback, fra begyndelsen uden at observere bivirkninger fra den mislykkede hurtige sti. Phasis håndterer dette på to måder:

1. **Rene præfikser** — compileren undersøger for bivirkninger før enhver spekulation. Hvis funktionen udfører I/O (console.log, værtsfunktioner) før spekulationspunktet, springer den hurtige sti ikke over dette; den gennemfører handlingen og fortsætter.
2. **Afvisning under kompilering** — for former, der kræver tilbageførsel af observerbare bivirkninger (en ikke-numerisk `let r = fn()`, hvor `fn()` ændrer tilstand), afviser compileren at kompilere overhovedet. Funktionen bliver på træfortolkeren.

Det motiverende tilfælde: en ikke-numerisk tildeling udfører `fn()` på den hurtige sti, kaster `Bailout` og afvikles derefter igen i træfortolkeren — så `fn()` køres to gange med to sæt bivirkninger. Afvisning under kompilering forhindrer `fn()` i overhovedet at blive kørt på den hurtige sti, så træfortolkeren kun observerer det ene kald.

## VM-dispatch (det andet lag)

Nogle funktioner kompileres til PHP-closures; andre falder videre til en endnu enklere bytecodefortolker i `src/Bytecode/VM.php`. Bytecoden er et fladt array af opcodes (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), som afvikles af en `switch`-dispatch.

VM'en bruges til funktioner, der ikke er værd at kompilere til en PHP-closure, men som stadig er varme nok til at drage fordel af at springe AST-gennemgangen over. Metodekald på indbyggede objekter (Array.prototype.map, String.prototype.split, …) går gennem hurtige stier i VM'en, der omgår den generiske `callFunction`-trampolin.

## Brugerdefineret kaldestak

`Op::CALL`, `Op::CALL_METHOD` og `Op::NEW_CALL` bruger en inline-kaldesti, der holder hele JS-kaldestakken inden for `VM::execute` — ingen ny PHP-frame for hvert JS-kald. Ved indgang lægges kalderens tilstand (pc, kodestrøm, operandstak, lokale variabler, env, `this`, strict-mode-flag) i en pulje pr. VM med `SavedFrame`-instanser af typen `savedFrames`, hvorefter dispatch-løkken kobler sine lokale variabler om til den kaldte funktion. `Op::RET` fjerner den gemte frame fra puljen og gendanner den. Undtagelser afvikles gennem gemte frames, før de sendes videre til træfortolkeren.

Egnetheden gemmes i cache på `JsFunction`: `vmInlineCallCache` for almindelige kald (BC-kompileret body, ikke bundet til `bind()`, ikke native), `vmInlineConstructCache` for `new`-udtryk (derudover `isConstructable`, ikke nedarvet, ingen initialisering af instansfelter eller private metoder). Alt andet går stadig gennem den kanoniske `callFunction`-/`vmNewExpression`-trampolin.

Den arkitektoniske sidegevinst: grænsen for PHP-stakken begrænser ikke længere rekursionsdybden for JS. Nu er `CallStack::maxDepth` den eneste grænse — sat til **4 096**, hvilket er komfortabelt over alle legitime test262-tilfælde (de fleste kræver under 2 k frames) og lavt nok til, at patologiske tests med uendelig rekursion fejler hurtigt inden for runnerens timeout. Stien med transpilerede closures (`JsToPhp`) har sin egen vagt på 8 192-frames for fallback uden inlining.

## Generators med framesnapshot

Enkle generator-bodies — uden `yield*`, uden `yield` i try/catch/finally og uden `await` — kompileres til bytecode. Compileren udsender en ny `Op::YIELD`-opcode dér, hvor bodyen suspenderes; `Op::YIELD` indfanger dispatch-tilstanden (cf, pc+1, stack, sp, lokale variabler, env, thisValue, strict) i et `GeneratorSnapshot`-heapobjekt og returnerer en `YieldResult`-sentinel fra `VM::execute`. Den drivende `JsGenerator` gemmer snapshot'et og sender det tilbage til `VM::execute` ved næste `next()` / `throw()`, hvor resume-værdien øverst på operandstakken erstattes.

Dette erstatter PHP-Fiber-stien for det understøttede delmængde. Den Fiber-baserede driver (`executeGeneratorBody`) bruges stadig til former, som compileren afviser — `yield*`-delegering, generators, der giver værdier inde i en try-blok, async generators og async-funktioner med `await`. `JsGenerator` har to tilstande: hver `next()` / `throw()` / `return()` kontrollerer, om instansen har et snapshot eller en Fiber, og sender afviklingen videre derefter.

Der er ingen indstilling, som kan finjusteres med benchmarks; egnetheden afgøres ved kompilering og gemmes i cache på `JsFunction`. Gevinsten er ét færre kontekstskift i PHP pr. yield på snapshot-stien; Fiber-stien er uændret for alt andet.

## Inline-caches

`Op::LOAD_MEMBER` har en inline-cache pr. pc, der er indekseret efter prototypeidentitet og bruges til mønsteret med prototypemetode på instans (`c.sum(...)`, `arr.push(...)`, alle dispatch af klassemetoder). Betingelse for cache-hit: modtageren har ingen egen slot for navnet, den cachelagrede prototypehenvisning er identisk med modtagerens `getPrototype()`, og prototypens `PropertyMap::$version` svarer stadig til den registrerede version.

Cache-invalidering bygger på `PropertyMap::$version`. Strukturelle ændringer (set / delete) øger den altid. Overskrivning af dataplotsværdi øger kun tælleren, når `PropertyMap::$isUsedAsProto` er sand — et write-barrier-flag, der slås til første gang objektet observeres som et andet objekts `[[Prototype]]`. Instansobjekter, der aldrig fungerer som prototyper, betaler intet for IC-infrastrukturen ved hver `obj.x = v`.

Getter-adgangsfunktioner cachelagres aldrig (de skal køres igen ved hver læsning).

`PropertyMap` har en ekstra tæller, `$mutationVersion`, som øges *altid* ved hver skrivning (uden write-barrier). `JSON.stringify`-cachen fastlåser denne tæller for hvert besøgt indlejret objekt/array, så en ændring, der kun påvirker instansen, som `o.a = 99`, korrekt invaliderer den cachelagrede streng, selv når IC'ens `$version` ville have sprunget tællerøgningen over.

## JSON.stringify-cache

`JsObject` har `$jsonCacheString` + `$jsonCacheVersions` (en liste med `[obj, mutationVersion]`-par for hvert indlejret objekt, der blev besøgt under den trivielle stringify-gennemgang). Ved næste `JSON.stringify(o)`-kald uden replacer / space kontrollerer den trivielle hurtige sti, at alle fastlåste versioner er uændrede, og returnerer den cachelagrede streng uden at gennemgå eller kode data igen. Hyppige mønstre med dyb kloning som `JSON.parse(JSON.stringify(o))` bliver cirka 2× hurtigere i stringify-delen.

Cachen droppes uden varsel ved cache-miss (hvis en fastlåst version afviger) og udfyldes igen ved næste gennemgang. Objekter med `toJSON`, getters, adgangsfunktioner, symboler eller `[[PrimitiveValue]]`-slots falder tilbage til specifikationens serialiseringsfunktion og udfylder aldrig cachen — den cachelagrede sti ser derfor kun strukturelt ensartede data.

## Indbyggede hurtige stier

De største ydelsesforbedringer kom af at lære VM'en om specifikke indbyggede funktioner:

- **`Atomics.load` / `store` / `compareExchange`** med `loadSpinHook` / `storeNotifyHook` kooperativ planlægning — genfandt 4 Atomics-tests med spin-løkker.
- **`Date.prototype.getTimezoneOffset`** med en DST-cache baseret på direkte hashmap — 45 % hurtigere ved SpiderMonkey DST-belastningstest.
- **`String.fromCharCode`** med en inline-hurtig sti til ASCII — sparer ~1 M dispatches pr. typisk gennemløb.
- **`decodeURI` / `decodeURIComponent`** med en ultrahurtig sti til `%XX%XX%XX%XX` på 12 tegn, der bruger `hex2bin`-masséafkodning.
- **`Object.keys` / `Object.values` / `Object.entries`** med direkte adgang til egenskabskortet.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** med indbygget iteration over det underliggende PHP-array.

Hver hurtig sti styres af et "formtjek" — hvis modtageren ikke har den forventede form (e.g. en TypedArray-visning, der ikke er understøttet af en almindelig PHP `string`), falder VM'en tilbage til den langsomme sti, der følger specifikationen.

## Hvad dette ikke gør

Bytekode-VM'en er **ikke en JIT** i V8-forstand. Den genererer ikke native maskinkode pr. funktion og spekulerer ikke ud fra observerede typer ud over IC'ens monomorfe tjek af prototypeidentitet. Inlining ved funktionsgrænser efterlignes med den tilpassede kaldestak (ingen ekstra PHP-ramme pr. JS-kald) i stedet for at sænke den kaldte funktions krop til den kaldende funktions bytekode.

JsToPhp-transpileren er det nærmeste, motoren kommer på en JIT: den genererer PHP-kildekode til varme funktionskroppe, som PHP 8.5's sporings-JIT derefter kompilerer til native kode. JsToPhp går gennem `eval()` og betaler derfor en engangsudgift til kompilering, mens sporings-JIT'en skal opdage funktionen, som om den var brugerkode. Hvis `eval()`-trinnet springes over ved at generere opcache-opkoder direkte, ville forskellen blive mindre — se [Benchmarks § Roadmap](https://phasis.inline0.dev/da/docs/advanced/benchmarks#roadmap).

## Benchmark

`bench/microbench.js` kører et dusin og lidt til små benchmarks (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). Median køretid:

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

De aktuelle tal gemmes i `BENCH.md` efter hver kørsel af `bench`-workflowen. Workflowene for `bench` og test262-matricen startes begge manuelt.
