Skip to content

THE FIELD GUIDE

Runtime memory specification

Normative object layout, roots, GC interaction, and runtime memory contracts.

View source · lang/docs/spec/runtime-memory.md
On this page

Version: 1.1 Status: Accepted Date: 2026-09-11

1. Scope#

This specification defines the observable runtime-memory contract shared by the interpreter, JIT, native extensions, Native hosts, and WebAssembly hosts. Implementation rationale and workload guidance live in docs/game-memory-architecture.md.

The keywords MUST, MUST NOT, SHOULD, and MAY are normative.

2. Island ownership#

  1. Every Island MUST own an independent managed heap, collector, root set, lease table, memory telemetry snapshot, and terminal memory-error state.
  2. A mutable managed object MUST have exactly one owning Island.
  3. A GcRef MUST NOT cross an Island boundary as a raw pointer.
  4. Cross-Island values MUST be reconstructed from validated typed packets, transferred as owned boundary bytes, or represented by a validated runtime handle.
  5. A child Island MUST inherit the parent's memory admission and collector policy at creation. Live occupancy, roots, counters, and errors MUST remain child-local.
  6. A managed object's address MUST remain stable for its complete lifetime.

3. Managed heap#

The runtime managed heap uses 64KiB blocks.

  • Small allocations use one of the power-of-two size classes from 16 bytes through 32768 bytes.
  • A small block serves one size class.
  • Larger allocations use a contiguous block run.
  • Allocation size includes the collector header and object data.
  • Newly allocated object storage is zero initialized.
  • Small-block allocation metadata MUST be the authoritative source for object identity, enumeration, and sweep membership. A second per-object registry MUST NOT be required for correctness.
  • The heap directory MUST retain the exact requested extent independently of the object header, so a corrupted header cannot enlarge an object's logical range merely because the forged range fits in the same size class.
  • An empty small block becomes reusable free-block capacity.
  • A small block MAY be reclaimed as one sweep operation only when the mark cycle observed no survivor, every resident object is young, and no resident object requires a native finalizer.
  • A dead large allocation enters bounded block-by-block reclaim before its complete extent becomes reusable.

hard_limit_bytes, when set, MUST bound managed heap committed bytes. It does not include JIT code, Rust collector metadata, host memory, GPU memory, audio memory, JavaScript objects, or extension-provider allocations.

Native JIT code MUST have a finite executable-page budget per Island family. The JIT memory provider MUST reserve a bounded arena during construction and release that arena when the complete Island family is destroyed. The artifact budget check MUST use page-granular committed bytes after machine-code generation and before executable allocation. Telemetry MUST report emitted bytes separately from charged page bytes. Published entry points remain valid until family destruction, so budget exhaustion disables the new artifact without evicting live code. Best-effort JIT execution falls back to the interpreter; strict JIT execution reports a typed resource rejection. Arena reservation failure MUST fail strict JIT construction before guest execution.

JIT analysis retention and per-artifact compiler work MUST have independent finite limits. Full-function and loop-OSR compilation MUST share one immutable function analysis. Evictable analysis state MUST NOT be pinned by a second VM manager cache.

JIT safepoint metadata MUST have a separate finite per-family limit and MUST be retained for exactly the lifetime of its published code. Every complete function and loop-OSR artifact that contains direct or conditional roots and can reach a native GC safepoint MUST link an active native-frame record while it executes. Scalar-only and safepoint-free artifacts MAY omit that record. Direct GcRef values live across a safepoint MUST have precise SP-relative stack-map entries in an explicit shadow-root area. Ordinary non-GC calls MUST NOT require root maps. Conditional roots whose pointer interpretation depends on runtime type tags MUST publish the complete adjacent header/payload pair in a precise shadow-root entry. The collector MAY trace that pair directly using its runtime tag; an untagged payload MUST NOT be treated as an unconditional root. Collection MUST wait for typed VM-frame materialization whenever an exact published representation is unavailable. If a bounded native root scan must yield to the scheduler, the native chain MUST first materialize its live state; native stack addresses MUST NOT survive that return.

Allocation-capable generated code MUST poll before consuming new managed-heap capacity. The no-work path MAY use runtime-owned raw GC field offsets. The taken path MUST publish the current bytecode pc and all materializable frame state before returning to the scheduler. Native-frame validation and the subsequent VM root scan MUST each have finite work budgets. Allocation helpers MUST NOT start an implicit collection while generated frames remain active. Generated code MAY allocate a verified constant-layout small object from a runtime-owned lane. Each consumed cell MUST commit allocation membership, initialize its header, and update live-byte, object-count, allocation-total, and debt counters before the object becomes visible. A collector step, an ordinary runtime allocation, or disabling allocation MUST invalidate the lane and return every unconsumed tail cell. Outstanding lane admission across all size classes MUST remain within max_objects and collector-work capacity. Helpers that allocate only on a structural slow path SHOULD defer that allocation, let generated code poll, and retry with explicit allocation permission. A replay credential MUST be scoped to one exact function and instruction, MUST authorize at most one retry, and MUST be issued only after a collector slice completed. That exact retry MAY allocate while debt remains or a collection cycle is active; the next allocation-capable instruction MUST poll again. This guarantees progress by alternating bounded collector work with at least one mutator allocation.

Each VM MUST also enforce finite limits for scheduled Fiber identities, stack slots per Fiber, call frames per Fiber, and aggregate native Fiber stack/frame storage. A runtime transition that publishes multiple spawns MUST reserve all identities and all required Fiber storage before it publishes the first spawn. Allocation or limit failure MUST reject the whole transition without a partially visible spawn batch.

initial_reserve_bytes MUST be admitted before guest execution starts. Reserve is rounded to the allocator's block granularity.

When growth_allowed is false:

  • the managed heap MUST NOT obtain another segment from its page provider;
  • free cells and free blocks already owned by the Island remain usable;
  • collector tracing-work and lease metadata MUST have an admitted capacity;
  • exhaustion of admitted collector metadata MUST produce MetadataExhausted.

When allocation_allowed is false, every managed allocation entry point MUST fail with AllocationForbidden.

3.1 Immutable string descriptors#

The Rust runtime represents a non-empty string by a three-slot StringData descriptor: an exact-base reference to a canonical packed Uint8 array, a pointer to the view's first byte, and its byte length. Nested views MUST retain the original array directly. The complete byte range MUST fit in that array. String consumers MUST use this descriptor contract; mutable slice capacity, element layout and backing geometry MUST NOT be read from a string object.

Dynamic string construction and non-empty slicing preserve their allocation entry points, admission checks, and sticky failure propagation. Literal expression evaluation MAY reuse immutable storage as specified in §3.3. Telemetry MUST count actual requested bytes and allocations. Empty strings retain the null representation. Descriptor size reduction alone does not authorize allocation elimination or shared managed ownership across Islands.

3.2 Slice descriptors#

The Rust runtime represents a slice with a seven-slot common prefix containing its owner, data pointer, length, capacity, element metadata, layout tag, element byte width and storage stride. The 32-bit metadata and 32-bit tag share one slot. A canonical packed array view derives its complete backing range from its exact-base Array owner. An extended descriptor appends two slots containing the complete backing pointer and length; this representation covers inline subobjects, permanently rooted global storage and flat-slot views.

Compact array views MUST match their owner's element metadata and width, and their storage stride MUST equal that width. Boundary validation MUST establish the physical descriptor shape and canonical owner before reading derived backing data. Extended tails MUST NOT be read through a compact descriptor. Nested views MUST preserve complete backing geometry, three-index capacity and alias relationships, including across Island transfer. Capacity and stride retain the full target address width, including zero-width element layouts.

Both representations preserve allocation entry points, admission, precise owner tracing, typed mutation barriers and sticky allocation failures. Actual requested sizes and allocation counts remain observable in memory telemetry.

3.3 Immutable literal evaluation and allocation observability#

A string literal expression MAY reuse already-existing immutable storage within its Island. An implementation MAY also represent literals using immutable program-image storage whose lifetime covers every reference to that image. Shared program bytes MUST NOT introduce a foreign managed object into an Island heap. String value, byte, slicing and conversion semantics remain unchanged; a mutable byte conversion MUST preserve its required independence.

A reuse hit performs no managed allocation and MAY succeed while allocation is disabled. Materialization on a miss MUST use ordinary allocation admission, hard limits, accounting and failure propagation. A pending sticky memory error MUST stop execution before the reused value can enable later guest effects. Memory telemetry describes actual storage operations; evaluating a literal is not a guarantee of a fresh allocation, a stable object identity or a fixed allocation count across backends, collections or optimization choices.

Optional reuse metadata MUST have an explicit finite bound and owner. Admission failure MAY fall back to ordinary construction without introducing an extra guest memory error. Reuse MUST NOT extend an object's lifetime through an unreported root, return reclaimed or repurposed storage, or retain a value from a replaced module scope. A weak cache MUST validate object liveness without adding unbounded work to any collector slice. Existing conservative allocation polls and exactly-once retry/progress rules remain valid when reuse is possible.

This permission applies to immutable literal evaluation. Fixed-array local values have the representation permission in §3.4. Other dynamic constructors and non-empty slice/string views retain their existing allocation entry points.

3.4 Independent local fixed-array values#

A fixed-array value held by a local that requires no escaping identity MAY be constructed and copied in precisely typed frame slots. Initializer evaluation MUST preserve source order and produce an independent value snapshot before any newly declared binding becomes visible. Later initializers, aliases, closures, failures and deferred calls MUST observe the same language values and side effects. Typed roots MUST describe every live reference, including nested values and conditional interface payloads.

An implementation MAY omit an intermediate managed array used only to stage such a local value. Stack representation performs no managed allocation and MAY succeed with managed allocation disabled. Telemetry counts actual storage operations. A pending sticky memory error MUST still stop later guest effects; this permission does not waive frame capacity limits or collector progress.

An array requiring canonical identity through address escape, slicing, capture, global storage or a heap-owning boundary MUST retain the required independent stable storage. Construction/materialization of that storage MUST use ordinary allocation admission, accounting, precise publication and sticky failure propagation. This local-value permission does not authorize delaying a required canonical allocation past guest effects, merging independently mutable objects, or retaining a pointer to an expired frame interval. Large and zero-element-width canonical arrays MUST remain representable without forcing their logical length through a flattened frame ABI.

4. Collector#

The collector is precise, stable-address, single-threaded, and incremental. It supports:

  • GcMode::Incremental, in which every cycle covers all generations;
  • GcMode::Generational, in which ordinary minor cycles collect young generations and major cycles cover all generations.

Both modes MUST share the same heap layout, object header, write barrier, root semantics, and failure contract.

A mode change is valid only while the collector is idle. An attempted mode change during an active cycle MUST return CollectorBusy.

4.1 Verified runtime type metadata#

Before a module becomes executable, bytecode verification MUST produce the runtime type facts used by object tracing and typed write barriers. The module, its type metadata, and those facts MUST remain one immutable unit. Parent and child Islands in one VM family MUST share that unit; their heaps, roots, collectors, and mutable execution state remain Island-local.

A tracing or barrier operation MUST use the verified facts without rebuilding a recursive type layout or allocating temporary storage. Missing facts, type kind drift, and slot-width mismatches MUST fail closed instead of selecting an approximate layout.

4.2 Bounded work#

gc_step_units(N) MUST complete at most N collector work units. A call with N == 0 MUST perform no collector work.

The following work MUST be resumable:

  • root scanning;
  • object and nested inline-layout scanning;
  • gray propagation;
  • remembered-parent scanning and retirement;
  • atomic remark and fixed-point detection;
  • sweeping;
  • large-span reclaim.

Collector cursors MUST survive between scheduler boundaries. A large object, deep inline array, large container, or large root set MUST NOT force one call to scan the complete structure. Runtime type lookup MUST take constant time per physical slot and MUST NOT hide recursive layout work from the work-unit budget.

4.3 Roots#

The precise root set includes all live guest references in:

  • fiber frames and registers;
  • defer and panic state;
  • globals;
  • runtime queue, endpoint, transport, and scheduler state;
  • active GcLease entries;
  • pending runtime state that can retain guest objects.

A root mutation during remark or sweep rescue MUST invalidate the affected root-domain scan and participate in fixed-point completion.

An object whose rescue scan completes during sweep MUST be normalized to the current white before the collector can return to pause. Correctness MUST NOT depend on the persistent sweep cursor visiting that object again.

4.4 Write barrier#

Every heap mutation that stores a GC-bearing value MUST execute the typed new-value barrier.

The barrier MUST:

  1. shade a white child written by a black parent during incremental marking;
  2. record an old-to-young edge during generational collection.

Remembered membership MUST live in heap-owned metadata. An implementation MAY use a segment/block summary to skip heap regions with no remembered objects. Generated code MAY inline the verified no-action predicate; every state that can mutate remembered membership, marking state, or sweep reachability MUST enter the shared runtime slow path.

Interpreter operations, JIT lowering, runtime containers, standard-library native helpers, and native-extension host callbacks MUST preserve this contract.

5. Runtime containers#

Map bucket backing and queued guest payload slots MUST be represented by managed runtime-backing objects and precisely traced.

An incremental map scan MUST record the backing generation. If resize replaces and rehashes the backing between bounded scan chunks, scanning MUST restart at the beginning of the new generation and remain work-budgeted.

Native protocol metadata, including waiter and endpoint bookkeeping, MAY remain in Rust containers. Hosts that enforce a total-process limit MUST budget this memory separately.

Container allocation failure MUST leave a pending Island memory error. Container helpers MUST NOT consume that error before the VM scheduler observes it.

6. Host configuration and control#

The canonical creation configuration is:

pub struct VmMemoryConfig {
    pub initial_reserve_bytes: usize,
    pub hard_limit_bytes: Option<usize>,
    pub gc_mode: GcMode,
    pub automatic_gc: bool,
    pub oom_policy: OomPolicy,
    pub growth_allowed: bool,
    pub allocation_allowed: bool,
    pub max_objects: Option<usize>,
    pub max_leases: Option<usize>,
}

The default configuration uses generational automatic collection, permits growth and allocation, has no hard limit, and uses CollectThenTerminateIsland.

The host control surface consists of:

  • reserve managed capacity;
  • set the managed hard limit;
  • allow or deny managed growth;
  • allow or deny managed allocation;
  • report external/provider memory;
  • read memory statistics;
  • select GC mode while idle;
  • stop or restart automatic GC;
  • request bounded GC work;
  • request a complete major collection at a safe host boundary.

Disabling growth is fallible because the runtime may need to reserve collector metadata first.

Guest code MUST NOT directly change reserve, hard limit, growth permission, or allocation permission.

7. Standard library#

Package runtime/mem exposes:

func ReadStats() Stats
func GCStep(workUnits uint64) bool
func GCCollect() bool

GCStep and GCCollect schedule work for a VM scheduler boundary. Their boolean result reports whether a VM owner accepted the request.

The Stats layout is platform independent and includes:

  • reserved, committed, live, young, old, large, and runtime-backing bytes;
  • free blocks, partial spans, fragmentation, and reclaim backlog;
  • externally reported bytes and unknown external-provider count;
  • allocation totals and failures;
  • minor/major cycle, work-unit, remembered-parent/root, remark, and lease counters;
  • growth/allocation permissions, hard-limit presence, GC mode/state, and automatic-GC state;
  • WebAssembly current/maximum pages.

Native and WebAssembly runtimes MUST preserve the same public field meanings.

8. Allocation failure and Island termination#

The memory error classes are:

  • AllocationForbidden;
  • GrowthDisabled;
  • HardLimitExceeded;
  • MetadataExhausted;
  • SystemAllocationFailed;
  • InvalidPointer;
  • CollectorBusy.

An allocation failure MUST be retained as the Island's pending memory error. Generated and interpreted code MUST return to the scheduler before dereferencing a null allocation result or executing a later guest side effect.

The scheduler MUST convert the pending error to a sticky VmError::IslandMemory. Later host polls MUST observe the same terminal error. The failed instruction MUST NOT be replayed.

Under CollectThenTerminateIsland, a scheduler at a clean collector boundary MAY perform one final major collection before teardown. Its purpose is reclamation and terminal telemetry. Under TerminateIsland, the scheduler skips this collection.

Termination clears the affected Island's executable and runtime state. Other Islands remain independent failure domains.

9. Native FFI roots#

Native extension ABI 10 defines:

pub struct GcLease {
    pub index: u32,
    pub generation: u32,
}

A native extension that retains a managed object across a call or safe point MUST create a lease through the host callback table. It MUST resolve the lease before use and release it when finished.

A released, stale, foreign, or out-of-range lease MUST fail closed. Extensions MUST NOT retain an unleased raw GcRef across a safe point.

The ABI exposes create, resolve, and release callbacks through ExtHostOpsV10. Host services use the independently versioned ExtHostServicesV2 table.

10. WebAssembly admission#

WebAssembly memory admission uses 64KiB pages.

Before guest execution:

  1. reserve bytes are rounded up to pages;
  2. current_pages + reserve_pages MUST fit the declared maximum, if present;
  3. a configured hard limit MUST conservatively fit as current_pages + hard_limit_pages <= maximum_pages;
  4. the runtime MUST request exactly reserve_pages of linear-memory growth;
  5. a failed growth request MUST fail VM construction;
  6. current and maximum pages MUST be published through memory telemetry.

WebAssembly maximum pages bound the complete linear memory. The Island hard limit bounds the managed heap committed within that memory.

11. Conformance#

A conforming implementation MUST test:

  • small and large allocation, reuse, hard-limit, no-growth, and bounded reclaim;
  • bounded root/object/card/remark/sweep work;
  • minor/major reachability under mutation;
  • Interpreter and generated JIT OOM exits;
  • full-function and loop-OSR precise stack maps, nested native-frame chains, tagged conditional roots, materialization on bounded scan exhaustion, and bounded safepoint scans;
  • managed map and queue backing reachability;
  • child-Island configuration inheritance and heap isolation;
  • lease generation and capacity failures;
  • cross-Island unpack failure without null dereference;
  • Native and WebAssembly admission/stat field equivalence;
  • CLI and runtime/mem surface contracts.

Built with Volang UI · A work in progress, made to be explored.
Opening Studio…