
# Maszyna wirtualna bajtkodu

Domyślną ścieżką wykonywania jest walker drzewa — odwiedza każdy węzeł AST i zwraca wartość. W przypadku często wywoływanych funkcji, uruchamianych miliony razy, wąskim gardłem jest narzut obsługi każdego węzła. Phasis dodaje do tego spekulacyjny kompilator bajtkodu do domknięć PHP, aby ograniczyć ten narzut.

## Kiedy uruchamia się maszyna wirtualna

Każde `JsFunction` ma licznik częstości wywołań. Gdy przekroczy próg (domyślnie 100), silnik próbuje skompilować treść funkcji do domknięcia PHP za pomocą `src/Bytecode/JsToPhp.php`.

Kompilacja się powiedzie, jeśli treść funkcji pasuje do znanego zestawu kształtów:

- Przypisania do zmiennych lokalnych o przewidywalnych typach (numerycznym, obiektowym, funkcyjnym, tablicowym, tekstowym).
- Operacje arytmetyczne na liczbach.
- Odczyt właściwości obiektowych zmiennych lokalnych ORAZ `this` (`this.v`, `obj.x`).
- Wywołania metod na zmiennych lokalnych typu obiektowego (`obj.method(args)`), z odbiornikiem przekazywanym jako `$thisValue`.
- Wywołania konstruktorów `new Ctor(args)` — promują typ zmiennej lokalnej po lewej stronie do `object` i zabezpieczają przed wynikiem niebędącym obiektem.
- Pętle `for` / `while` z prostymi warunkami.
- `return` z wyrażeniem, które można obliczyć.
- `if` / `else` z typowanymi warunkami.

Kompilacja kończy się niepowodzeniem (a funkcja pozostaje obsługiwana przez walker drzewa), gdy jej treść zawiera:

- Instrukcje `with`.
- `try / catch / finally` (obsługiwane przez walker drzewa, aby zapewnić prawidłową semantykę zakończenia).
- Wywołania `eval()`.
- Bezpośrednią mutację `arguments`.
- Dowolny element, którego kompilator nie potrafi jeszcze przekształcić.

Po udanej kompilacji silnik przełącza ścieżkę wywołań funkcji na skompilowane domknięcie. Kolejne wywołania wykonują natywny kod PHP z narzutem jednego wywołania funkcji, zamiast ponosić taki narzut dla każdego węzła AST.

## Przerwanie kompilacji

Szybka ścieżka skompilowanego kodu opiera się na założeniach (liczby pozostają liczbami, a właściwości pozostają w tym samym kształcie). Gdy założenie zostanie naruszone w czasie wykonywania, domknięcie zgłasza `Bailout` i wykonywanie tego wywołania rozpoczyna się ponownie w walkerze drzewa.

Właśnie tu zgodność ze specyfikacją ma największe znaczenie. Walker drzewa musi móc ponownie wykonać wywołanie, które spowodowało przerwanie, od początku i bez efektów ubocznych po nieudanej szybkiej ścieżce. Phasis obsługuje to na dwa sposoby:

1. **Czyste prefiksy** — kompilator wyszukuje efekty uboczne przed rozpoczęciem spekulacji. Jeśli funkcja wykonuje operacje I/O (console.log, funkcje hosta) przed punktem spekulacji, szybka ścieżka ich nie pomija, lecz zatwierdza je i kontynuuje.
2. **Odmowa kompilacji** — w przypadku kształtów wymagających wycofania widocznych efektów ubocznych (nienumeryczne `let r = fn()`, gdy `fn()` modyfikuje stan) kompilator w ogóle odmawia kompilacji. Funkcja pozostaje obsługiwana przez walker drzewa.

Przykład, który motywuje to rozwiązanie: przypisanie wartości nienumerycznej wykonuje `fn()` na szybkiej ścieżce, zgłasza `Bailout`, a następnie jest wykonywane ponownie przez walker drzewa — przez co `fn()` uruchamia się dwa razy i powoduje dwa zestawy efektów ubocznych. Odmowa kompilacji zapobiega uruchomieniu `fn()` na szybkiej ścieżce, więc walker drzewa obsługuje tylko jedno wywołanie.

## Mechanizm dispatch maszyny wirtualnej (druga warstwa)

Niektóre funkcje są kompilowane do domknięć PHP, a pozostałe trafiają do jeszcze prostszego interpretera bajtkodu w `src/Bytecode/VM.php`. Bajtkod to płaska tablica kodów operacji (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …), przetwarzana przez mechanizm dispatch `switch`.

Maszyna wirtualna obsługuje funkcje, których nie warto kompilować do domknięcia PHP, ale które są wywoływane wystarczająco często, by skorzystać z pominięcia przechodzenia przez AST. Wywołania metod wbudowanych obiektów (Array.prototype.map, String.prototype.split, …) korzystają z szybkich ścieżek maszyny wirtualnej, które omijają ogólny trampolinowy mechanizm `callFunction`.

## Własny stos wywołań

`Op::CALL`, `Op::CALL_METHOD` i `Op::NEW_CALL` korzystają ze ścieżki wywołań inline, która utrzymuje cały stos wywołań JS wewnątrz `VM::execute` — bez tworzenia nowej ramki PHP dla każdego wywołania JS. Przy wejściu stan wywołującego (pc, strumień kodu, stos operandów, zmienne lokalne, env, `this`, flaga trybu ścisłego) jest odkładany w puli `savedFrames` instancji `SavedFrame` przypisanej do maszyny wirtualnej, a pętla dispatch przestawia zmienne lokalne na zmienne wywoływanego elementu. `Op::RET` zdejmuje zapisaną ramkę i przywraca stan. Wyjątki są rozwijane przez zapisane ramki, zanim sterowanie wróci do walkera drzewa.

Kwalifikowalność jest zapamiętywana w `JsFunction`: `vmInlineCallCache` dla zwykłych wywołań (treść skompilowana do BC, bez powiązania przez `bind()` i niebędąca kodem natywnym), `vmInlineConstructCache` dla wyrażeń `new` (dodatkowo `isConstructable`, bez klasy pochodnej oraz inicjalizatorów pól instancji ani metod prywatnych). Pozostałe przypadki nadal przechodzą przez kanoniczny mechanizm trampolinowy `callFunction` / `vmNewExpression`.

Korzyść architektoniczna: limit stosu PHP nie ogranicza już głębokości rekurencji JS. Teraz jedynym limitem jest `CallStack::maxDepth` — ustawiony na **4 096**, czyli znacznie powyżej każdej uzasadnionej sytuacji w test262 (większość przypadków wymaga mniej niż 2 k ramek), a zarazem na tyle nisko, by testy patologicznej nieskończonej rekurencji szybko kończyły się w ramach limitu czasu runnera. Ścieżka transpilowanych domknięć (`JsToPhp`) ma własny limit 8 ramek 192 dla przypadków awaryjnych, które nie są obsługiwane inline.

## Generatory z migawką ramki

Proste treści generatorów — bez `yield*`, bez `yield` wewnątrz try/catch/finally i bez `await` — są kompilowane do bajtkodu. Kompilator emituje nowy kod operacji `Op::YIELD` w miejscu zawieszenia treści; `Op::YIELD` zapisuje stan dispatch (cf, pc+1, stos, sp, zmienne lokalne, env, thisValue, strict) w obiekcie sterty `GeneratorSnapshot` i zwraca z `VM::execute` znacznik zastępczy `YieldResult`. Sterujący `JsGenerator` zapisuje migawkę i przy następnym `next()` / `throw()` przekazuje ją ponownie do `VM::execute`, zastępując wartość wznowienia na szczycie stosu operandów.

To rozwiązanie zastępuje ścieżkę PHP-Fiber w obsługiwanym podzbiorze. Sterownik oparty na Fiberze (`executeGeneratorBody`) nadal obsługuje kształty odrzucane przez kompilator — delegowanie `yield*`, generatory wykonujące yield wewnątrz bloku try, generatory asynchroniczne oraz funkcje asynchroniczne z `await`. `JsGenerator` działa w dwóch trybach: każde `next()` / `throw()` / `return()` sprawdza, czy instancja ma migawkę czy Fiber, i odpowiednio przekazuje sterowanie.

Brak parametru do strojenia w benchmarkach; kwalifikowalność jest ustalana w czasie kompilacji i zapamiętywana w `JsFunction`. Zysk to o jedno przełączenie kontekstu PHP mniej na każde yield w ścieżce z migawką; pozostałe przypadki nadal korzystają z niezmienionej ścieżki Fiber.

## Pamięci podręczne inline

`Op::LOAD_MEMBER` ma pamięć podręczną inline dla każdego PC, opartą na tożsamości prototypu i używaną w przypadku wywołania metody prototypu na instancji (`c.sum(...)`, `arr.push(...)` i każde wywołanie metody klasy). Warunki trafienia: odbiornik nie ma własnego pola o danej nazwie, buforowana referencja do prototypu jest identyczna z `getPrototype()` odbiornika, a `PropertyMap::$version` prototypu nadal odpowiada zapisanej wersji.

Unieważnianie pamięci podręcznej opiera się na `PropertyMap::$version`. Modyfikacje strukturalne (set / delete) zawsze zwiększają jego wartość. Wartość zwiększa się po nadpisaniu wartości pola danych tylko wtedy, gdy `PropertyMap::$isUsedAsProto` ma wartość true — jest to flaga bariery zapisu ustawiana przy pierwszym wykryciu, że obiekt stanowi `[[Prototype]]` innego obiektu. Instancje, które nigdy nie służą jako prototypy, nie ponoszą narzutu infrastruktury IC przy każdym `obj.x = v`.

Akcesory getterów nigdy nie są buforowane (muszą być wykonywane przy każdym odczycie).

`PropertyMap` ma drugi licznik, `$mutationVersion`, którego wartość zwiększa się *bezwarunkowo* przy każdym zapisie (bez bariery zapisu). Pamięć podręczna `JSON.stringify` zapisuje wartość tego licznika dla każdego odwiedzanego zagnieżdżonego obiektu lub tablicy, aby mutacja dotycząca tylko instancji, taka jak `o.a = 99`, prawidłowo unieważniała buforowany tekst, nawet gdy `$version` IC pomijałoby zwiększenie wartości licznika.

## Pamięć podręczna JSON.stringify

`JsObject` przechowuje `$jsonCacheString` + `$jsonCacheVersions` (listę par `[obj, mutationVersion]` dla każdego zagnieżdżonego obiektu odwiedzonego podczas prostego przebiegu serializacji do tekstu). Przy kolejnym wywołaniu `JSON.stringify(o)` bez funkcji replacer i bez parametru space, prosta szybka ścieżka sprawdza, czy żadna z zapisanych wersji się nie zmieniła, i zwraca buforowany tekst bez ponownego przechodzenia po strukturze i kodowania. Często używane wzorce głębokiego klonowania, takie jak `JSON.parse(JSON.stringify(o))`, przyspieszają część serializacji do tekstu mniej więcej dwukrotnie.

Pamięć podręczna jest po cichu odrzucana w razie chybienia (gdy różni się dowolna zapisana wersja) i wypełniana przy następnym przebiegu. Obiekty z `toJSON`, getterami, akcesorami, symbolami lub polami `[[PrimitiveValue]]` przechodzą do serializatora zgodnego ze specyfikacją i nigdy nie zasilają pamięci podręcznej — dlatego buforowana ścieżka obsługuje wyłącznie dane o jednolitej strukturze.

## Szybkie ścieżki dla wbudowanych elementów

Największe przyspieszenia przyniosło dostosowanie maszyny wirtualnej do konkretnych wbudowanych elementów:

- **`Atomics.load` / `store` / `compareExchange`** z kooperacyjnym planowaniem `loadSpinHook` / `storeNotifyHook` — odzyskano 4 testy pętli aktywnego oczekiwania Atomics.
- **`Date.prototype.getTimezoneOffset`** z bezpośrednią pamięcią podręczną DST opartą na hashmapie — przyspieszenie o 45 % w teście obciążeniowym DST SpiderMonkey.
- **`String.fromCharCode`** z szybką ścieżką inline dla ASCII — oszczędza około 1 mln operacji dispatch podczas typowego przebiegu.
- **`decodeURI` / `decodeURIComponent`** z ultraszybką ścieżką `%XX%XX%XX%XX` dla ciągów 12-znakowych, wykorzystującą zbiorcze dekodowanie `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** z bezpośrednim dostępem do mapy właściwości.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** z natywną iteracją po bazowej tablicy PHP.

Każda szybka ścieżka jest zabezpieczona „sprawdzeniem kształtu” — jeśli odbiornik nie odpowiada oczekiwanemu kształtowi (e.g, widok TypedArray, który nie jest oparty na zwykłym PHP `string`), maszyna wirtualna przechodzi do wolnej ścieżki zgodnej ze specyfikacją.

## Czego to rozwiązanie nie robi

Maszyna wirtualna bajtkodu **nie jest JIT-em** w rozumieniu V8. Nie generuje natywnego kodu maszynowego dla każdej funkcji i nie przeprowadza spekulacji na podstawie zaobserwowanych typów, poza monomorficznym sprawdzeniem tożsamości prototypu w IC. Inlining na granicy funkcji jest przybliżany przez własny stos wywołań (bez dodatkowej ramki PHP dla każdego wywołania JS), a nie przez obniżanie treści wywoływanej funkcji do bajtkodu wywołującej.

Transpilator JsToPhp jest najbliższy temu, co w silniku przypomina JIT: generuje kod źródłowy PHP dla często wywoływanych treści funkcji, który tracing JIT z wersji 8.5 PHP kompiluje następnie do natywnego kodu. JsToPhp korzysta z `eval()`, więc ponosi jednorazowy koszt kompilacji, a tracing JIT musi wykryć tę funkcję tak, jakby była kodem użytkownika. Pominięcie kroku `eval()` przez bezpośrednie emitowanie kodów operacji opcache pozwoliłoby jeszcze bardziej zmniejszyć tę różnicę — zobacz [Benchmarki § Plan rozwoju](https://phasis.inline0.dev/pl/docs/advanced/benchmarks#roadmap).

## Benchmarki

`bench/microbench.js` uruchamia kilkanaście niewielkich testów wydajnościowych (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). Mediana czasu rzeczywistego:

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

Aktualne wyniki są zatwierdzane w `BENCH.md` po każdym uruchomieniu workflow `bench`. Workflow macierzy `bench` i test262 są uruchamiane ręcznie.
