# VM

One VM runs JavaScript and Python programs: `vm/`, built as `gizvm`. This document states what is built.

## 1. Run

```sh
bin/giz FILE.py ARG...                                # a Python program: run builds the VM and runs it
bin/giz FILE.js|FILE.ts ARG...                        # a JavaScript or TypeScript program, the same way
bin/giz build FILE.js -o app                          # the VM with the script's project, see below
bin/giz build --fast vm/package.json -o /tmp/gizvm    # a fast build of the VM
/tmp/gizvm FILE.js|FILE.py ARG...
bin/giz test vm                                       # tests/vm/run.giz
```

Flags go before the file:

| Flag | Effect |
| --- | --- |
| `--no-jit` | The interpreter only |
| `--jit-eager` | Each function compiles at its first call, each loop at its first turn |
| `--exit-stress` | `--jit-eager`, and a guard exits at a random countdown |
| `--gc-stress` | A collection at each slow allocation (each eighth one full); a dead object is poisoned, and each minor collection checks that no marked object holds an unmarked one |
| `--jit-trace` | Reports each compile and exit; the code memory goes to `gizvm-code.bin` |
| `--jit-mir` | Prints each graph before and after the optimizer |
| `--vm-stats` | Counts compiled code's slow operations by opcode, native calls by id, the generic paths and the keys that take them; writes them to standard error at the end |
| `--dump` | Prints the bytecode and stops |

## 2. Layers

| Layer | Files | What it does |
| --- | --- | --- |
| Values and objects | `core.giz`, `object.giz`, `bigint.giz` | One 64-bit word per value: an address, a small special, an int of 48 bits (top 16 bits 0xFFFE), or a double's bits plus 2^49. An object has a header, a map (hidden class), out-of-line slots, elements and 4, 8 or 16 inline slots. A Python instance is an object whose map's prototype is its class. The maps of a chain that adds one key after another share one keys array and one lookup table |
| Memory | `heap.giz` | Mark-region (Immix) collector with two generations that do not move. A small object's mark is a bit in a bitmap beside the start bits, and the marking marks its lines, so the sweep reads no dead object. Marks stay set, so the marked objects are the old ones: a minor collection (each 8 MiB allocated) marks from the roots and the dirty cards of old objects; a full one clears the marks when the old bytes doubled. A value store marks the 512-byte card of its address (`core.giz` written): the heap is reserved at 2^44 and the card of address A is at A >> 9. The native stack and the registers `getcontext` saved are scanned conservatively, so a collection needs no machine code. The code memory (a memory file mapped writable and executable at two addresses) is made at the first code the VM writes, so `--no-jit` maps no executable page |
| Front ends | `jsparse.giz`, `pyparse.giz`, `ast.giz`, `compile.giz` | Both languages to one tree, then to one register bytecode (`bytecode.giz`) |
| Interpreter | `interp.giz`, `runtime.giz`, `builtins.giz`, `pybuiltins.giz`, `realm.giz` | The dispatch loop with inline caches; the full meaning of each operation once, per language |
| JavaScript built-ins | `jsobject.giz`, `jsasync.giz` | Symbols, property attributes (read-only, hidden, fixed) and extensibility in the maps, Object reflection, Reflect; promises with their jobs, async functions (an await yields to a driver that waits with no closure), async generators, for await, async-from-sync iterators. An object of a built-in class with state (a promise, a timer) is a host object: its map carries the class brand, and its internal words come before its inline slots, so its properties have the usual inline caches |
| Event loop | `loop.giz` | One loop for every language: the job queue (microtasks), timers (a binary heap by due time) and immediates, run after the program in Node's order until no referenced timer or immediate is left |
| Binary data | `jsbinary.giz` | ArrayBuffer (resizable, transferable), SharedArrayBuffer, the twelve typed arrays, DataView and Atomics: a buffer is a host object over a raw block, a typed array or view a host object over a buffer, an offset and a length; elements are read and written in place and reach `o[i]`, `in`, keys and iteration through the runtime hooks |
| Date | `jsdate.giz` | A host object holding its time value; local time from the C library zone (`localtime_r`), ISO and English date parsing, the setters and text forms |
| Weak references | `jsweak.giz`, `heap.giz` | WeakMap, WeakSet, WeakRef and FinalizationRegistry, and Python's `weakref.ref`, over weak tables (raw blocks the collector does not trace): after marking, an ephemeron keeps its value only while its key lives, and a registry entry whose target died is queued for the event loop's cleanup |
| Iterator helpers | `jsiter.giz` | `Iterator`, `Iterator.from` and the lazy and eager methods of `Iterator.prototype` over the VM's own iterators and any object with `next` |
| Modules | `jsmodule.giz`, `jseval.giz` | CommonJS and ES modules resolved as Node resolves them (package.json `main`, `exports`, `imports`, `node_modules`, `.ts`), JSON modules, `eval` and `new Function` with completion values, syntax errors named by file and line |
| Decorators | `jsdecorate.giz`, `compile.giz` | A class with decorators or `accessor` elements is compiled to a call of a JavaScript runtime (TC39 2023: kinds, `context`, `addInitializer`, `access`, `metadata`) that the first such class evaluates; a `.ts` file takes the same path |
| Node | `host.giz`, `node/*.giz` | Node's own `lib/` runs on the VM, fetched by tag and commit into the cache. `host.giz` is the embedding API of the bindings; each `node/*.giz` is one `internalBinding` (`fs`, `buffer`, `net`, `crypto`, `zlib`, `os`, `url`, `v8` serialization, ...). Node starts at the first builtin `require` or the first read of a Node global (`process`, `Buffer`, `URL`, `fetch`, `structuredClone`, ...), so a plain script pays nothing |
| Script programs | `app.giz` | A binary built from a script: the VM, the files of the script's project and a trailer |
| Embedding | `main.giz`, `cli.giz` | `main.giz` is the library `gizvm`: a Gizlang program opens a JavaScript realm and a Python realm in its own process and calls them ([SYSTEM](system.md#javascript-and-python)). Each realm has its own heap (`HEAP_AT + slot << 37`); handles are rooted in `R_HANDLES`; a task enters a realm under `R_ENTERED`, and the collector scans that task's stack (`Task.stackTop`). `cli.giz` is the `gizvm` executable |
| JIT | `flow.giz`, `mir.giz`, `opt.giz`, `gen.giz`, `x64.giz`, `jit.giz` | Below |

`build FILE.js -o app` (also `.ts` and `.py`) copies the VM and appends the files of the script's project (the folder of
the nearest `package.json` at or above the script, else the script's folder; never a folder with `.git`; `.git` itself is
left out) and a trailer. The first start unpacks the project into `$GIZLANG_CACHE/app/KEY` and runs the entry there, so
requires, `node_modules` and file reads find the project as built. Every argument of the program is the script's.

`node:http` servers take a face of their own (`node/http.giz`): a connection of a server with Node's classes is parsed
natively, its requests and responses are objects with Node's prototypes and fields, a request's stream state is made at
its first read, and a response's head and body go out through one native write (`writeHttp` in `node/net.giz`, the
head as Node's `_storeHeader` makes it). Anything else (other classes, async hooks, diagnostics channels) runs Node's
own server code. `tests/vm/node/http-face.js` checks the bytes against Node's.

Not built yet: workers (`worker_threads`), `vm` contexts, function source text in `toString`.

## 3. JIT

- **Property caches.** Each access site has an inline cache: one map, its location and holder, and a record of up to
  4 maps. Past 4 maps (megamorphic) the site stops filling, and the global property cache serves it: (map, key) to
  the location and holder, for data slots and accessors, filled by every generic lookup (`runtime.giz` `propHit`), so
  computed keys and natives use it too. Entries hold the prototype epoch and the collection count. The epoch moves
  only when a prototype or a class gains, loses or changes a key, or gets a new prototype: an object that is no
  prototype moves to another map instead (Express sets the prototype of each request). A prototype set on such an
  object reuses the map it made before for the same map and prototype.
  An element access `o[k]` whose key is one atom or symbol that is no index (Node's `this[kHeaders]`) has a keyed
  cache: a raw record of that key and a property cache, which compiled code guards on the key and embeds like
  `o.k`. A second key makes the site generic. Each collection marks the maps, holders and keys of the feedback
  words, the polymorphic records and the keyed records.
  A read cache also holds a getter (the interpreter and the helper call it on the receiver) and an absent key
  (`IC_ABSENT`: each object of the chain is made a prototype, so a key added to one moves the epoch; compiled code
  gives undefined after the map and epoch guards). A string's methods are cached on the map all strings share. An
  index key (`{0: x} = array`) names an element, which no map holds, so it fills no cache. A string's `length` is
  cached on the strings' map; the helper reads it (compiled code has no UTF-16 count to load).
- **When.** A function compiles after enough calls; a loop compiles at its header after enough turns (OSR) and is
  entered from the running interpreter frame.
- **Graph (MIR, `mir.giz`).** SSA over basic blocks, built from the bytecode and the inline-cache feedback. Types:
  Value, Int, F64, Bool, Obj, Word. Each instruction that can exit has a frame state: the interpreter frames (inlined
  ones too) and the SSA value of each live register. Calls to a known function are inlined up to depth 3; `new` of a
  known class inlines the constructor with the new object as `this`.
- **Speculation.** A guard checks what the feedback saw: an int, a double, an object's map, a function's identity, the
  prototype epoch (prototypes and Python classes only; a JavaScript own property needs none). An int that enters a
  loop (an OSR register, a word the loop keeps in a register, an invariant read) is guarded to [0, B], with B at least
  twice what it holds as the compile runs (at least 2^20, below 2^31). A property access or method call that saw 2 to
  4 maps (polymorphic) dispatches on the map to the code of each.
- **Optimizer (`opt.giz`).** In order: phis of one value go; straight-line blocks merge; value numbering; new objects
  that do not escape become the SSA values of their slots; per loop, inner first, invariant guards and pure
  instructions move before the header and each word the loop reads and writes stays in a register; value numbering
  again; int ranges; unused pure instructions go. Int ranges come from constants, arithmetic, the compares of
  dominating branches, the entry guards and the trip count of a counted loop. A sum or product that fits needs no
  overflow check, and a remainder or quotient of a dividend that is not negative by a constant needs no sign
  correction (below 2^32: two multiplies, Lemire's method).
- **Code (`gen.giz`).** Linear-scan registers, x86-64. An unguarded result is computed in its own register; a guarded
  one is written there after its last exit. Rare paths (a JavaScript double that holds an int) are out of line.
- **Generators.** A generator keeps its frame in a VALUES object and runs it on the VM stack at each resume. Its
  code compiles like a loop's: each place it resumes is an entry, and a yield is an exit that returns to the resume.
  A generator expression that `list`, `tuple`, `set`, `sum` or another built-in consumer takes at once (and no
  assignment hides that name) compiles as a list comprehension, with no generator.
- **Helpers.** A call of the builtin `len` or `range` calls its runtime helper with no native call. A call with
  keywords, `in` and `await` are helper calls too, and so are closures, object literal keys (a literal's own key
  takes its inline cache: the next map and a store), getters, setters, computed keys, `typeof`, `~`, template strings,
  `throw`, `instanceof` and the `arguments` array. A call of a frequent native (`builtins.giz` `nativeIntrinsic`: `toLowerCase`,
  `charCodeAt`, `indexOf`, `slice`, `Array.isArray` and others) seen at the site is the helper `H_NATIVE`: its plain
  case runs with no frame, else the native call. `===` against undefined, null, a boolean or an object, and of
  `typeof` against a literal, compares the words. A `new` that is not inlined takes the generic call with its mark (`jitCall` constructs). No helper flushes the frame, so a function that holds them can still be inlined.
- **Calls.** A call of a known function, or of one FuncInfo, enters its compiled code. A JavaScript call of a simple
  function with another count of arguments is entered or inlined the same way: a missing argument is undefined, an
  extra one is dropped. JavaScript parameter defaults run in the function's prologue, so a function with them is still
  simple. A call of a known native goes from the generic call straight to `callNative`. A callee seen only at run time
  is checked inline (a compiled simple function of that many parameters) and entered the same way; any other callee
  takes the generic call.
- **Exits.** A failed guard writes the live values of each frame (boxed), makes the objects and iterators the
  optimizer removed, and the interpreter continues at that instruction. A failed guard marks its site, so the next
  compile takes the generic path there; a failed range guard stops only the range guards of its loop. After too many
  exits the code is dropped and the function compiles again; after too many drops it stays in the interpreter. An exit of a loop that the interpreter entered (OSR) returns to that
  interpreter, which runs on from the exit's instruction, so a loop that exits and enters again keeps the native stack
  flat.

## 4. Tests

`tests/vm/run.giz` runs each program in `tests/vm/cases` in four modes (`--no-jit`, `--jit-eager`, `--exit-stress`,
`--exit-stress --gc-stress`) and compares the output with `NAME.expected`, what Node 24 or CPython 3.12 prints
(`PYTHONHASHSEED=0`). A program named `fail.*` ends with an uncaught error. `tests/vm/node` holds the programs that
start Node's lib (`--node`: modules, event loop, `structuredClone`, `net` and `http`); they run in three modes.

## 5. Performance

`bench/vm` holds the step-2 programs. The gate host runs each program as a whole process, best of 5, the VM and the
reference alternately (`fleet bench any`; Bun 1.3.11, PyPy 7.3.20). Ratio = reference time / VM time (above 1: the VM
is faster).

| Program | VM ms | Reference ms | Ratio |
| --- | --- | --- | --- |
| fib.js (fib 35) | 59.6 | Bun 64.3 | 1.08 |
| loop.js | 206.9 | Bun 356.2 | 1.72 |
| obj.js | 48.5 | Bun 90.4 | 1.86 |
| prop.js | 327.5 | Bun 409.9 | 1.25 |
| method.js | 200.6 | Bun 361.6 | 1.80 |
| fib.py (fib 35) | 59.8 | PyPy 98.7 | 1.65 |
| loop.py | 206.9 | PyPy 350.1 | 1.69 |
| nbody.py | 216.6 | PyPy 238.5 | 1.10 |
| attr.py | 98.3 | PyPy 130.7 | 1.33 |
| method.py | 64.6 | PyPy 122.9 | 1.90 |
| empty.js | 3.5 | Bun 7.2 | 2.06 |
| empty.py | 4.0 | PyPy 24.3 | 6.08 |

A named property read whose map the feedback saw is two loads: the map compare and the slot.

FastAPI (`/bench` returning a small dict, the ASGI app called in process, 10000 calls after 3000): VM 45.3 us,
CPython 3.15 27.1 us a call (a build host bench CPUs, 2026-10-20). A regex match (`re` over the native `_sre`) is 0.43 us
on the VM and 0.12 us on CPython (a build host; the bridge took 1.25 us).
