GizStack
View .mdOpen in ChatGPTOpen in Claude

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:

FlagEffect
--no-jitThe interpreter only
--jit-eagerEach 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-stressA 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-traceReports each compile and exit; the code memory goes to gizvm-code.bin
--jit-mirPrints each graph before and after the optimizer
--vm-statsCounts 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
--dumpPrints the bytecode and stops

2. Layers

LayerFilesWhat it does
Values and objectscore.giz, object.giz, bigint.gizOne 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
Memoryheap.gizMark-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 endsjsparse.giz, pyparse.giz, ast.giz, compile.gizBoth languages to one tree, then to one register bytecode (bytecode.giz)
Interpreterinterp.giz, runtime.giz, builtins.giz, pybuiltins.giz, realm.gizThe dispatch loop with inline caches; the full meaning of each operation once, per language
JavaScript built-insjsobject.giz, jsasync.gizSymbols, 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 looploop.gizOne 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 datajsbinary.gizArrayBuffer (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
Datejsdate.gizA 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 referencesjsweak.giz, heap.gizWeakMap, 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 helpersjsiter.gizIterator, Iterator.from and the lazy and eager methods of Iterator.prototype over the VM's own iterators and any object with next
Modulesjsmodule.giz, jseval.gizCommonJS 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
Decoratorsjsdecorate.giz, compile.gizA 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
Nodehost.giz, node/*.gizNode'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 programsapp.gizA binary built from a script: the VM, the files of the script's project and a trailer
Embeddingmain.giz, cli.gizmain.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
JITflow.giz, mir.giz, opt.giz, gen.giz, x64.giz, jit.gizBelow

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).

ProgramVM msReference msRatio
fib.js (fib 35)59.6Bun 64.31.08
loop.js206.9Bun 356.21.72
obj.js48.5Bun 90.41.86
prop.js327.5Bun 409.91.25
method.js200.6Bun 361.61.80
fib.py (fib 35)59.8PyPy 98.71.65
loop.py206.9PyPy 350.11.69
nbody.py216.6PyPy 238.51.10
attr.py98.3PyPy 130.71.33
method.py64.6PyPy 122.91.90
empty.js3.5Bun 7.22.06
empty.py4.0PyPy 24.36.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).