
# Bytecode-VM

Standardvägen för exekvering är en trädtolkare — besök varje AST-nod och returnera ett värde. För heta funktioner som anropas miljontals gånger blir dispatchkostnaden per nod flaskhalsen. Phasis lägger till en spekulativ kompilator från bytekod till PHP-closure för att minska den kostnaden.

## När VM:n aktiveras

Varje `JsFunction` har en räknare för heta anrop. När den passerar en tröskel (standardvärde 100) försöker motorn kompilera funktionskroppen till en PHP-closure via `src/Bytecode/JsToPhp.php`.

Kompileringen lyckas när funktionskroppen passar in i en känd uppsättning former:

- Tilldelningar till lokala variabler med förutsägbara typer (numerisk, objekt, funktion, array, sträng).
- Aritmetik med tal.
- Medlemsläsningar från lokala variabler av objekttyp OCH från `this` (`this.v`, `obj.x`).
- Metodanrop på lokala variabler av objekttyp (`obj.method(args)`), där mottagaren skickas med som `$thisValue`.
- `new Ctor(args)`-konstruktoranrop — uppgraderar LHS-lokalen till typen `object` och skyddar med Bailout mot returvärden som inte är objekt.
- `for`- och `while`-loopar med enkla villkorsformer.
- `return` med ett beräkningsbart uttryck.
- `if` / `else` med typade villkor.

Kompileringen misslyckas (och funktionen stannar i trädtolkaren) när kroppen innehåller:

- `with`-satser.
- `try / catch / finally` (hanteras av trädtolkaren för korrekt semantik vid slutförande).
- `eval()`-anrop.
- Direkt mutation av `arguments`.
- Sådant kompilatorn ännu inte vet hur den ska sänka.

När kompileringen lyckas byter motorn funktionens anropsväg till den kompilerade closuren. Efterföljande anrop kör inbyggd PHP med kostnaden för ett funktionsanrop, i stället för en kostnad per AST-nod.

## Avbrott

Den kompilerade snabbvägen bygger på antaganden (att tal förblir tal och egenskaper behåller samma form). När ett antagande bryts vid körning kastar closuren en `Bailout`, och exekveringen startas om i trädtolkaren för det anropet.

Det är här korrekthet enligt specifikationen är som viktigast. Trädtolkaren måste kunna köra om det avbrutna anropet från början utan att observera några sidoeffekter från den misslyckade snabbvägen. Phasis hanterar detta på två sätt:

1. **Rena prefix** — kompilatorn letar efter sidoeffekter före varje spekulationspunkt. Om funktionen utför I/O (console.log, värdfunktioner) före spekulationspunkten hoppar snabbvägen inte över det; den genomför det och fortsätter.
2. **Avböjning vid kompilering** — för former som skulle kräva att observerbara sidoeffekter rullas tillbaka (en `let r = fn()` som inte är numerisk, där `fn()` ändrar tillstånd) vägrar kompilatorn att kompilera över huvud taget. Funktionen stannar i trädtolkaren.

Det motiverande fallet: en tilldelning som inte är numerisk kör `fn()` i snabbvägen, kastar `Bailout` och körs sedan igen i trädtolkaren — vilket kör `fn()` två gånger med två uppsättningar sidoeffekter. Avböjning vid kompilering hindrar `fn()` från att köras i snabbvägen över huvud taget, så trädtolkaren ser det enda anropet.

## VM-dispatch (det andra lagret)

Vissa funktioner kompileras till PHP-closures; andra faller vidare till en ännu enklare bytekodstolkare i `src/Bytecode/VM.php`. Bytekoden är en platt array med opkoder (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …) som behandlas av en `switch`-dispatch.

VM:n används för funktioner som inte är värda att kompilera till en PHP-closure men ändå är tillräckligt heta för att vinna på att hoppa över AST-genomgången. Metodanrop på inbyggda objekt (Array.prototype.map, String.prototype.split, …) går via snabbvägar i VM:n som kringgår den generiska `callFunction`-trampolinen.

## Anpassad anropsstack

`Op::CALL`, `Op::CALL_METHOD` och `Op::NEW_CALL` använder en inline-anropsväg som håller hela JS-anropsstacken i `VM::execute` — ingen ny PHP-ram per JS-anrop. Vid inträde läggs anroparens tillstånd (pc, kodström, operandstack, lokala variabler, env, `this`, strict-mode-flagga) i en `savedFrames`-pool per VM med `SavedFrame`-instanser, och dispatchloopen kopplar om sina lokala variabler till den anropade funktionen. `Op::RET` tar bort den sparade stackramen och återställer tillståndet. Undantag vecklas upp genom sparade stackramar innan exekveringen återgår till trädtolkaren.

Behörighet sparas memoiserad på `JsFunction`: `vmInlineCallCache` för vanliga anrop (BC-kompilerad kropp, inte `bind()`-bunden, inte inbyggd), `vmInlineConstructCache` för `new`-uttryck (dessutom `isConstructable`, inte härledd, inga initierare för instansfält eller privata metoder). Allt annat går fortfarande via den kanoniska `callFunction` / `vmNewExpression`-trampolinen.

Den arkitektoniska sidovinsten: taket för PHP-stacken begränsar inte längre JSs rekursionsdjup. `CallStack::maxDepth` är nu den enda gränsen — satt till **4 096**, gott och väl över alla legitima test262-fall (de flesta kräver färre än 2 k stackramar) och tillräckligt låg för att patologiska oändlighetsrekursionstester snabbt ska misslyckas inom körarens tidsgräns. Den transpilerade closure-vägen (`JsToPhp`) har en egen spärr vid 8 stackramar av typen 192 för reservvägen utan inline-anrop.

## Generatorer med stackramsögonblicksbilder

Enkla generatorkroppar — utan `yield*`, utan `yield` inuti try/catch/finally och utan `await` — kompileras till bytekod. Kompilatorn genererar en ny `Op::YIELD`-opkod där kroppen pausar; `Op::YIELD` fångar dispatchtillståndet (cf, pc+1, stack, sp, lokala variabler, env, thisValue, strict) i ett `GeneratorSnapshot`-heap-objekt och returnerar en `YieldResult`-sentinel från `VM::execute`. Den drivande `JsGenerator` sparar ögonblicksbilden och skickar tillbaka den till `VM::execute` vid nästa `next()` / `throw()`, och ersätter återupptagningsvärdet överst på operandstacken.

Detta ersätter PHP-Fiber-vägen för den delmängd som stöds. Den Fiber-baserade drivrutinen (`executeGeneratorBody`) finns kvar för former som kompilatorn avvisar — delegering med `yield*`, generatorer som ger ett värde inuti ett try-block, asynkrona generatorer och asynkrona funktioner med `await`. `JsGenerator` har två lägen: varje `next()` / `throw()` / `return()` kontrollerar om instansen har en ögonblicksbild eller en Fiber och skickar vidare därefter.

Ingen inställning kan finjusteras för bänktester; behörighet avgörs vid kompilering och memoiseras på `JsFunction`. Vinsten är en färre PHP-kontextväxling per yield i vägen med ögonblicksbilder; Fiber-vägen är oförändrad för allt annat.

## Inline-cacher

`Op::LOAD_MEMBER` har en inline-cache per PC, nycklad efter prototypidentitet. Den används för mönstret med prototypmetoder på instanser (`c.sum(...)`, `arr.push(...)` och varje dispatch av klassmetod). Träffvillkor: mottagaren har ingen egen plats för namnet, den cachade prototypreferensen är identisk med mottagarens `getPrototype()` och prototypens `PropertyMap::$version` matchar fortfarande den fångade versionen.

Cacheogiltigförklaring bygger på `PropertyMap::$version`. Strukturella mutationer (set / delete) ökar det alltid. Överskrivningar av dataplatsvärden ökar det bara när `PropertyMap::$isUsedAsProto` är sant — en skrivspärrflagga som slås på första gången objektet observeras som ett annat objekts `[[Prototype]]`. Instansobjekt som aldrig fungerar som prototyper betalar ingen kostnad för IC-infrastrukturen vid varje `obj.x = v`.

Getter-accessorer cachas aldrig (de måste köras om vid varje läsning).

`PropertyMap` har en andra räknare, `$mutationVersion`, som ökar *ovillkorligt* vid varje skrivning (utan skrivspärr). `JSON.stringify`-cachen fäster den här räknaren på varje besökt nästlat objekt/array, så att en mutation som endast berör en instans, som `o.a = 99`, korrekt ogiltigförklarar den cachade strängen även när IC:ns `$version` skulle ha hoppat över ökningen.

## JSON.stringify-cache

`JsObject` har `$jsonCacheString` + `$jsonCacheVersions` (en lista med `[obj, mutationVersion]`-par för varje nästlat objekt som besöks under den triviala stringify-genomgången). Vid nästa `JSON.stringify(o)`-anrop utan replacer / space verifierar den triviala snabbvägen att varje fäst version är oförändrad och returnerar den cachade strängen utan att gå igenom eller koda om objekten. Heta mönster för djupkloning, som `JSON.parse(JSON.stringify(o))`, blir ungefär 2× snabbare vid stringify-steget.

Cachen tas bort tyst vid miss (om någon fäst version skiljer sig) och fylls på igen vid nästa genomgång. Objekt med `toJSON`, getters, accessorer, symboler eller `[[PrimitiveValue]]`-platser faller tillbaka till serialiseraren enligt specifikationen och fyller aldrig cachen — den cachade vägen hanterar därför enbart strukturellt enhetliga data.

## Snabbvägar för inbyggda funktioner

De största prestandavinsterna kom av att lära VM:n om specifika inbyggda funktioner:

- **`Atomics.load` / `store` / `compareExchange`** med kooperativ schemaläggning via `loadSpinHook` / `storeNotifyHook` — återställde 4 Atomics-spinnlooptester.
- **`Date.prototype.getTimezoneOffset`** med en direkt hashmapp för DST-cache — 45 % snabbare i SpiderMonkeys DST-stresstest.
- **`String.fromCharCode`** med en inbyggd ASCII-snabbväg — sparar ~1 M dispatchar per typisk genomgång.
- **`decodeURI` / `decodeURIComponent`** med en ultrasnabb 12-teckensväg för `%XX%XX%XX%XX`, med massavkodning via `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** med direkt åtkomst till egenskapskartor.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** med inbyggd iteration över den underliggande PHP-arrayen.

Varje snabbväg skyddas av en "formkontroll" — om mottagaren inte har den förväntade formen (e.g. en TypedArray-vy som inte stöds av en vanlig PHP `string`) går VM:n vidare till den långsamma vägen som följer specifikationen.

## Vad detta inte gör

Bytekod-VM:n är **inte en JIT** i betydelsen V8. Den genererar inte inbyggd maskinkod per funktion och gör inga spekulationer utifrån observerade typer, bortsett från IC:ns monomorfa kontroll av prototypidentitet. Inlining över funktionsgränser approximeras med den anpassade anropsstacken (ingen extra PHP-ram per JS-anrop) i stället för att den anropade funktionens kropp sänks till anroparens bytekod.

JsToPhp-transpileraren är motorns närmaste motsvarighet till en JIT: den genererar PHP-kod för heta funktionskroppar, som PHP 8.5:s spårande JIT sedan kompilerar till inbyggd kod. JsToPhp går via `eval()`, så den betalar en engångskostnad för kompilering och den spårande JIT-kompilatorn måste upptäcka funktionen som om den vore användarkod. Om man hoppar över steget `eval()` genom att generera opcache-opkoder direkt minskar gapet ytterligare — se [Benchmarks § Färdplan](https://phasis.inline0.dev/sv/docs/advanced/benchmarks#roadmap).

## Bänktester

`bench/microbench.js` kör ett dussintal små bänktester (loop-aritmetik, 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). Medianväggtid:

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

Aktuella siffror checkas in i `BENCH.md` efter varje körning av `bench`-arbetsflödet. `bench`- och test262-matrisarbetsflödena startas båda manuellt.
