
# VM Bytecode

Jalur eksekusi default adalah tree-walker — kunjungi setiap node AST, lalu kembalikan nilai. Untuk fungsi yang sering dipanggil hingga jutaan kali, overhead dispatch per node menjadi hambatan. Phasis menambahkan compiler bytecode-ke-closure PHP spekulatif di atasnya untuk mengurangi overhead tersebut.

## Kapan VM mulai bekerja

Setiap `JsFunction` memiliki penghitung pemanggilan panas. Setelah melewati ambang batas (default 100), engine mencoba mengompilasi isi fungsi menjadi closure PHP melalui `src/Bytecode/JsToPhp.php`.

Kompilasi berhasil jika isi fungsi sesuai dengan sekumpulan bentuk yang dikenal:

- Penetapan variabel lokal dengan tipe yang dapat diprediksi (numerik, objek, fungsi, array, string).
- Operasi aritmetika pada angka.
- Pembacaan anggota pada variabel lokal bertipe objek DAN pada `this` (`this.v`, `obj.x`).
- Pemanggilan metode pada variabel lokal bertipe objek (`obj.method(args)`), dengan receiver diteruskan sebagai `$thisValue`.
- Pemanggilan konstruktor `new Ctor(args)` — mempromosikan tipe variabel lokal LHS menjadi `object` dan menjaga dengan Bailout jika nilai kembalian bukan objek.
- Loop `for` / `while` dengan bentuk kondisi sederhana.
- `return` dengan ekspresi yang dapat dihitung.
- `if` / `else` dengan kondisi bertipe.

Kompilasi gagal (dan fungsi tetap menggunakan tree-walker) jika isi fungsi memuat:

- Pernyataan `with`.
- `try / catch / finally` (ditangani oleh tree-walker agar semantik penyelesaian tetap benar).
- Pemanggilan `eval()`.
- Mutasi `arguments` langsung.
- Apa pun yang belum diketahui cara menurunkannya oleh compiler.

Jika kompilasi berhasil, engine mengalihkan jalur pemanggilan fungsi ke closure hasil kompilasi. Pemanggilan berikutnya menjalankan PHP native dengan satu overhead pemanggilan fungsi, bukan satu overhead untuk setiap node AST.

## Batal kompilasi

Jalur cepat hasil kompilasi membuat asumsi (angka tetap berupa angka, properti tetap memiliki bentuk yang sama). Jika asumsi dilanggar saat runtime, closure akan memunculkan `Bailout` dan eksekusi dimulai ulang di tree-walker untuk pemanggilan tersebut.

Di sinilah ketepatan terhadap spesifikasi paling penting. Tree-walker harus dapat mengeksekusi ulang pemanggilan yang batal dari awal tanpa melihat efek samping apa pun dari jalur cepat yang gagal. Phasis menangani hal ini dengan dua cara:

1. **Awalan murni** — compiler memindai efek samping sebelum spekulasi apa pun. Jika fungsi melakukan I/O (console.log, fungsi host) sebelum titik spekulasi, jalur cepat tidak menghilangkannya; jalur itu melakukan commit lalu melanjutkan.
2. **Penolakan saat kompilasi** — untuk bentuk yang memerlukan pembatalan efek samping yang dapat diamati (`let r = fn()` nonnumerik yang `fn()` memutasi state), compiler menolak melakukan kompilasi. Fungsi tetap menggunakan tree-walker.

Kasus yang mendasari: penetapan nonnumerik menjalankan `fn()` pada jalur cepat, memunculkan `Bailout`, lalu dijalankan ulang di tree-walker — sehingga `fn()` berjalan dua kali dengan dua rangkaian efek samping. Penolakan saat kompilasi mencegah `fn()` berjalan sama sekali di jalur cepat, sehingga tree-walker hanya melihat satu pemanggilan.

## Dispatch VM (lapisan kedua)

Sebagian fungsi dikompilasi menjadi closure PHP; fungsi lainnya diteruskan ke interpreter bytecode yang lebih sederhana di `src/Bytecode/VM.php`. Bytecode berupa array opcode datar (`LOAD_LOCAL`, `STORE_PROP`, `CALL_METHOD`, …) yang dijalankan oleh dispatch `switch`.

VM digunakan untuk fungsi yang tidak layak dikompilasi menjadi closure PHP tetapi cukup sering dipanggil sehingga diuntungkan dengan melewati traversal AST. Pemanggilan metode pada objek bawaan (Array.prototype.map, String.prototype.split, …) melalui jalur cepat di VM yang melewati trampoline `callFunction` generik.

## Call stack khusus

`Op::CALL`, `Op::CALL_METHOD`, dan `Op::NEW_CALL` menggunakan jalur pemanggilan inline yang menjaga seluruh call stack JS di dalam `VM::execute` — tanpa frame PHP baru untuk setiap pemanggilan JS. Saat masuk, state pemanggil (pc, aliran kode, stack operand, variabel lokal, env, `this`, flag mode ketat) didorong ke pool `savedFrames` per-VM yang berisi instans `SavedFrame`, lalu loop dispatch mengalihkan variabel lokalnya ke callee. `Op::RET` mengambil frame tersimpan dan memulihkannya. Exception diurai melintasi frame tersimpan sebelum kembali ke tree-walker.

Kelayakan dimemoisasi pada `JsFunction`: `vmInlineCallCache` untuk pemanggilan biasa (isi terkompilasi BC, tidak terikat ke `bind()`, bukan native), `vmInlineConstructCache` untuk ekspresi `new` (selain itu `isConstructable`, bukan turunan, tanpa inisialisasi field instans atau metode privat). Selebihnya tetap melalui trampoline kanonis `callFunction` / `vmNewExpression`.

Manfaat arsitektural lainnya: batas stack PHP tidak lagi membatasi kedalaman rekursi JS. Kini hanya ada batas `CallStack::maxDepth` — ditetapkan sebesar **4 096**, jauh di atas semua kasus test262 yang wajar (sebagian besar memerlukan kurang dari 2 k frame) dan cukup ketat agar pengujian rekursi tak terbatas yang patologis gagal dengan cepat dalam batas waktu runner. Jalur closure yang ditranspilasi (`JsToPhp`) memiliki penjaga tersendiri untuk 8 frame 192 pada jalur fallback yang tidak di-inline.

## Generator snapshot frame

Isi generator sederhana — tanpa `yield*`, tanpa `yield` di dalam try/catch/finally, tanpa `await` — dikompilasi menjadi bytecode. Compiler menghasilkan opcode `Op::YIELD` baru di lokasi isi fungsi ditangguhkan; `Op::YIELD` menangkap status dispatch (cf, pc+1, stack, sp, variabel lokal, env, thisValue, strict) ke dalam objek heap `GeneratorSnapshot` lalu mengembalikan sentinel `YieldResult` dari `VM::execute`. `JsGenerator` penggerak menyimpan snapshot dan mengirimkannya kembali ke `VM::execute` pada `next()` / `throw()` berikutnya, mengganti nilai resume di puncak stack operand.

Ini menggantikan jalur Fiber PHP untuk subset yang didukung. Driver berbasis Fiber (`executeGeneratorBody`) tetap digunakan untuk bentuk yang ditolak compiler — delegasi `yield*`, generator yang melakukan yield di dalam blok try, generator asinkron, fungsi asinkron dengan `await`. `JsGenerator` mendukung dua mode: setiap `next()` / `throw()` / `return()` memeriksa apakah instans memiliki snapshot atau Fiber lalu melakukan dispatch sesuai kondisi.

Tidak ada opsi yang dapat disetel untuk benchmark; kelayakan ditentukan saat kompilasi dan dimemoisasi pada `JsFunction`. Keuntungannya adalah satu pergantian konteks PHP lebih sedikit untuk setiap yield pada jalur snapshot; jalur Fiber untuk semua bentuk lainnya tidak berubah.

## Cache inline

`Op::LOAD_MEMBER` memiliki cache inline per-PC yang dikunci berdasarkan identitas prototipe, digunakan untuk pola metode prototipe pada instans (`c.sum(...)`, `arr.push(...)`, setiap dispatch metode kelas). Kondisi cache hit: receiver tidak memiliki slot sendiri untuk nama tersebut, referensi prototipe yang disimpan identik dengan `getPrototype()` milik receiver, dan `PropertyMap::$version` prototipe masih sama dengan versi yang diambil.

Invalidasi cache mengikuti `PropertyMap::$version`. Mutasi struktural (set / delete) selalu menaikkan nilainya. Penimpaan nilai slot data hanya menaikkannya jika `PropertyMap::$isUsedAsProto` bernilai true — flag write-barrier yang diaktifkan saat pertama kali objek diamati sebagai `[[Prototype]]` milik objek lain. Objek instans yang tidak pernah berfungsi sebagai prototipe tidak menanggung biaya infrastruktur IC pada setiap `obj.x = v`.

Accessor getter tidak pernah di-cache (harus dijalankan kembali pada setiap pembacaan).

`PropertyMap` memiliki penghitung kedua, `$mutationVersion`, yang naik *tanpa syarat* pada setiap penulisan (tanpa write-barrier). Cache `JSON.stringify` menyematkan penghitung ini pada setiap objek/array bertingkat yang dikunjungi, sehingga mutasi yang hanya terjadi pada instans seperti `o.a = 99` dengan benar membatalkan cache string meskipun `$version` milik IC seharusnya melewatkan kenaikan tersebut.

## Cache JSON.stringify

`JsObject` memuat `$jsonCacheString` + `$jsonCacheVersions` (daftar pasangan `[obj, mutationVersion]` untuk setiap objek bertingkat yang dikunjungi selama proses trivial-stringify). Pada pemanggilan `JSON.stringify(o)` berikutnya tanpa replacer / tanpa spasi, jalur cepat trivial memverifikasi bahwa setiap versi yang disematkan tidak berubah dan mengembalikan string yang tersimpan di cache tanpa menelusuri ulang atau mengodekan ulang. Pola deep-clone yang sering digunakan seperti `JSON.parse(JSON.stringify(o))` mendapatkan peningkatan kecepatan sekitar 2× pada bagian stringify.

Cache dihapus tanpa pemberitahuan saat terjadi miss (versi yang disematkan berbeda) dan diisi kembali pada penelusuran berikutnya. Objek dengan `toJSON`, getter, accessor, simbol, atau slot `[[PrimitiveValue]]` beralih ke serializer yang sesuai spesifikasi dan tidak pernah mengisi cache — sehingga jalur cache hanya melihat data yang seragam secara struktural.

## Jalur cepat bawaan

Peningkatan performa terbesar berasal dari mengajari VM tentang built-in tertentu:

- **`Atomics.load` / `store` / `compareExchange`** dengan penjadwalan kooperatif `loadSpinHook` / `storeNotifyHook` — memulihkan 4 pengujian loop spin Atomics.
- **`Date.prototype.getTimezoneOffset`** dengan cache DST direct-hashmap — peningkatan kecepatan 45 % pada pengujian stres DST SpiderMonkey.
- **`String.fromCharCode`** dengan jalur cepat ASCII inline — menghemat ~1 juta dispatch per pemindaian tipikal.
- **`decodeURI` / `decodeURIComponent`** dengan jalur supercepat `%XX%XX%XX%XX` sepanjang 12 karakter yang menggunakan decoding bulk `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** dengan akses langsung ke peta properti.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** dengan iterasi native pada array PHP yang mendasarinya.

Setiap jalur cepat dikendalikan oleh "pemeriksaan bentuk" — jika penerima tidak cocok dengan bentuk yang diharapkan (e.g, tampilan TypedArray yang tidak didukung oleh PHP `string` biasa), VM beralih ke jalur lambat yang sesuai spesifikasi.

## Hal yang tidak dilakukan

VM bytecode **bukan JIT** dalam pengertian V8. VM ini tidak menghasilkan kode mesin native untuk tiap fungsi dan tidak membuat spekulasi berdasarkan tipe yang diamati di luar pemeriksaan identitas prototipe monomorfik IC. Inlining pada batas fungsi didekati melalui callstack khusus (tanpa frame PHP tambahan per pemanggilan JS), bukan dengan menurunkan isi callee ke bytecode caller.

Transpiler JsToPhp adalah bagian engine yang paling mendekati JIT: transpiler ini menghasilkan kode sumber PHP untuk isi fungsi yang sering digunakan, lalu JIT tracing milik PHP 8.5 mengompilasinya menjadi kode native. JsToPhp berjalan melalui `eval()`, sehingga memerlukan biaya kompilasi satu kali dan JIT tracing harus menemukan fungsi tersebut seolah-olah merupakan kode pengguna. Melewati langkah `eval()` dengan menghasilkan opcode opcache secara langsung akan memperkecil kesenjangan — lihat [Tolok Ukur § Peta Jalan](https://phasis.inline0.dev/id/docs/advanced/benchmarks#roadmap).

## Tolok ukur

`bench/microbench.js` menjalankan belasan benchmark kecil (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). Waktu dinding median:

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

Angka terkini di-commit ke `BENCH.md` setelah setiap proses workflow `bench`. Workflow matriks `bench` dan test262 sama-sama dijalankan secara manual.
