Mach VM Model
Overview
Mach is the register-based execution form for streamlined Mcode IR. The compiler lowers source to mcode, the optimizer simplifies it, and the Mach serializer turns the result into compact bytecode for the VM.
Mcode remains the authoritative instruction-set reference. This page describes what the VM model means at runtime: registers, frames, values, heap objects, calls, tail calls, disruption handlers, immutability, and garbage collection boundaries.
Instruction Words
Mach bytecode uses fixed-width instruction words. The opcode selects the operation; the remaining fields are interpreted as register indexes, constant indexes, or jump offsets.
The ordinary form is one 32-bit word with 8-bit A/B/C fields. When an A, B, or
C register index does not fit, the instruction is preceded by one wide word.
The prefix’s A/B/C bytes are the high bytes of the following word’s fields, so
each register index is then 16 bits. For iABx, the prefix’s Bx is the high 16
bits of the following word’s Bx, producing a 32-bit unsigned index. The prefix
does not widen signed jump offsets: iAsBx keeps its signed 16-bit offset and
isJ keeps its signed 24-bit offset.
The prefix and its following word are one logical instruction. Branches and debug locations name the prefix, never the second word. A narrow instruction has no prefix, remains byte-for-byte unchanged, and the VM executes it with the same one indexed instruction fetch as before. Wide call-argument payload words use the same prefix rule.
Constant operands are pool-global VALUES indices. Lowering records every
constant-bearing logical instruction and branch target in its fragment; after
pool-wide interning, the Pit linker rewrites those operands, inserts wide at
the u16 boundary, and rebases branches, entry/disruption PCs, PGO sites, and
origin locations. REGEXP literals are normalized through two LOADK operations
into scratch registers, so REGEXP’s B/C fields remain register operands.
Four logical formats are used:
iABC - Three Small Operands
Used for operations such as dest = left + right, property access, or setting
call arguments.
op, A, B, C
iABx - Register and Constant Index
Used when an instruction needs one register plus a larger constant-pool index.
op, A, Bx
iAsBx - Register and Signed Offset
Used for conditional jumps.
op, A, signed_offset
isJ - Signed Jump
Used for unconditional jumps.
op, signed_offset
The exact packed bit layout is a current-runtime bytecode detail. Artifact records say when a payload is Mach bytecode; portable interchange should use the higher-level executable and mcode records instead.
Registers and Frames
Mach is a register VM. Each function runs with a fixed set of register slots computed by the compiler.
- Argument slots hold the function inputs.
- Local slots hold
varanddefvalues. - Closure slots hold captured values from outer functions.
- Temporary slots hold intermediate expression results.
A frame is the live execution state for one function call. Conceptually it contains the function being executed, the caller to resume when it returns, the current instruction position, and the register slots for that function.
Frames are also heap-managed values because closures can keep outer variables alive after the creating function has returned. The VM treats frames as managed runtime objects, not raw stack memory exposed to user code.
The frame budget
A frame carries at most 65535 register slots. The FUNCTIONS row already
stores arity, close_slots, and frame_slots as u16; the wide prefix lets
instructions address the same range. Slot index 0xffff remains unavailable
to a frame of that size because the count’s valid indexes end at 0xfffe; the
runtime can therefore continue using 0xffff as its no-return-slot sentinel.
Modules with more than 255 live slots are ordinary wide functions. A module lowers as a root function, and every live top-level binding holds one of that function’s slots for the whole body, but crossing the old 8-bit boundary adds prefix words only to instructions that actually name the high slots. The compiler still refuses a function whose frame count exceeds the u16 descriptor, by name and as an ordinary compile error.
Values in Registers
Every register slot holds a Pit value. Small integers, logical values, null, and some short text values can be immediate. Arrays, records, longer text, blobs, functions, code, and frames are heap-managed values referenced from the slot.
The VM does not expose the representation to programs. User code observes the language types and operations documented in the language reference. The representation matters to Mach because it determines whether an operation can run directly, needs a runtime helper, or must disrupt.
See Values for the current value model.
Heap Objects
Mach works with a small set of heap object categories:
| Object | VM role |
|---|---|
| Array | Ordered, integer-indexed storage used by array literals, push, pop, and indexed access |
| Record | Key/value storage used by records, modules, runtime contexts, and messages |
| Text | Immutable script-visible text, plus temporary mutable text during optimized concatenation |
| Blob | Bit-addressed binary data used by binary modules and I/O |
| Function | Callable code, whether implemented by Mach bytecode or by C |
| Code | Compiled function bodies and constant pools |
| Frame | Live or captured function execution state |
Forwarding objects can appear internally while the copying collector is moving objects, but they are a GC implementation detail rather than a script-visible category.
The exact in-memory layout, table sizes, and allocation formulas are runtime implementation details. The VM contract is higher level: slots hold managed values, heap objects can move during GC, and every operation must keep live values reachable through VM slots or runtime roots.
Object Operations
The compiler emits mcode guard sequences before memory operations. After streamlining, Mach should receive canonical operations whose runtime shape is already known:
loadreads from a valid array or record.storewrites to a valid mutable array or record.- Assigning
nullto a record field removes that field. - Invalid array indexes and writes to immutable objects disrupt.
lengthreads the logical length of arrays, text, blobs, records, or functions as defined by the language.pushandpopoperate on arrays.
This split keeps type dispatch mostly in mcode, leaving Mach to execute compact operations against checked values.
Text Construction
Script-visible text is immutable, but repeated concatenation needs an efficient path. The compiler lowers text concatenation to a temporary mutable text object:
- Create mutable text with enough capacity.
- Append the left text.
- Append the right text.
- Stone the text before it escapes the defining slot.
The streamline optimizer inserts stone slot, slot when mutable text would
escape into a return value, call argument, closure, record, array, or any other
observable location. See Stone Values for the immutability model.
Calls
Normal function calls are decomposed into:
- Create a call frame for the callee.
- Copy arguments into that frame.
- Invoke the frame and write the result to a destination register.
For Mach functions, invocation switches to the callee frame and later resumes the caller. For C or other native functions, invocation crosses into the host runtime and writes the returned value back into the caller frame.
Explicit Tail Calls
The source-level go statement is an explicit tail call. It is lowered to a
tail-call frame followed by a tail invoke.
Conceptually:
- The current function does not resume after
go. - The callee receives the supplied arguments.
- The callee’s result becomes the current function’s result.
- The caller chain is preserved as if the current frame had been replaced by the callee frame.
This is different from an ordinary call followed by return: go states the
tail-call intent directly in the source language and lets Mach reuse or replace
the current frame.
Disruption Handlers
Functions may carry a disruption handler entry point. If a disrupting operation occurs and the current function has a handler, control transfers to that handler with the same managed frame context. When the call chain reaches its end with the disruption still unhandled, the actor crashes.
Because disruption handling needs a resumable current frame, the compiler
rejects some combinations that would make explicit tail calls ambiguous, such
as go inside a function with a disruption clause.
Arithmetic and Comparisons
Mcode emits explicit type checks and dispatch branches before arithmetic when types are not statically known. Once Mach executes arithmetic instructions, it can use fast numeric paths because mcode has already established the expected shape: number or null at the arithmetic seam.
Arithmetic is total. A non-number operand yields null rather than disrupting, so the checks in front of an arithmetic instruction select between the numeric path and producing null. Null is absorbing.
Comparisons preserve language semantics: equality is defined over every pair of values, while ordering is defined for two numbers or two texts and disrupts on any other pairing. Concatenation converts number operands to text and disrupts when an operand has no text form.
Immutability and GC Boundary
Mach runs inside one actor heap at a time. Actor heaps are collected by a copying collector, so any heap object reference may move during allocation. The VM and native helpers must keep live values in managed slots or explicit runtime roots.
Stone values are immutable. Attempts to write through Mach operations into a stoned array, record, blob, or text disrupt. Stoned heap values still belong to the actor heap unless they are runtime constants; the collector can move them like other heap objects.
Relationship to Other Specs
- Mcode IR lists the instruction set that Mach encodes.
- Streamline simplifies mcode before it is lowered to mach.
- Values describes the current value model.
- Stone Values describes immutability.
- Artifact Formats describes where Mach bytecode appears in executable and boot artifacts.