VM
One VM runs JavaScript and Python programs: vm/, built as gizvm. This document states what is built.
1. Run
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.gizFlags 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). 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.gizpropHit), 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 accesso[k]whose key is one atom or symbol that is no index (Node'sthis[kHeaders]) has a keyed cache: a raw record of that key and a property cache, which compiled code guards on the key and embeds likeo.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'slengthis 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;newof a known class inlines the constructor with the new object asthis. - 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,sumor 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
lenorrangecalls its runtime helper with no native call. A call with keywords,inandawaitare 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,instanceofand theargumentsarray. A call of a frequent native (builtins.giznativeIntrinsic:toLowerCase,charCodeAt,indexOf,slice,Array.isArrayand others) seen at the site is the helperH_NATIVE: its plain case runs with no frame, else the native call.===against undefined, null, a boolean or an object, and oftypeofagainst a literal, compares the words. Anewthat is not inlined takes the generic call with its mark (jitCallconstructs). 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).