Runtime memory management
Runtime memory management is implemented in the WebAssembly runtime prelude. The prelude is emitted only for modules that need managed values or runtime helpers.
Current model
Regulus uses a resettable bump arena in linear memory.
- Static managed objects are emitted as data segments before the dynamic heap.
$__heapis the next dynamic allocation offset.__allocaligns each request to 8 bytes.- Allocation advances the heap pointer and never frees individual objects.
- Objects are non-moving until the Wasm instance is reset or explicit arena reset runs.
This keeps allocation small and makes raw i32 managed-value pointers stable for generated code and Wasmtime tests.
Helper inventory
Runtime helpers are grouped by use:
- allocation:
__alloc,__allocation_fail,__last_panic,__arena_mark, and__arena_reset - managed values: tuple, record, custom, closure, opaque, option, order, error, and panic constructors, plus raw field readers
- closures: allocation and indirect-call capture layout helpers
- equality and ordering: structural equality and comparison for runtime values
- debug: debug tags, panic/error reasons, and payload readers
- dynamic values: constructors, classifiers, field readers, decoder constructors, and decoder runners
- host adapters: JS exports for allocation, strings, managed value tags, arity, constructors, fields, and opaque handle readers
Growth and failure
Every allocator path must call __alloc or a helper that delegates to it. __alloc checks whether the aligned allocation end fits in current linear memory. If it does not fit, it computes the required page count and calls memory.grow.
Allocation fails when:
- size arithmetic overflows,
- the required page count cannot be represented, or
memory.growreturns-1.
Allocation failure records a tag-10 panic payload through __last_panic. The payload reason tag is 1; slot 0 is the requested allocation size and slot 1 is the heap pointer before allocation.
Runtime allocation tests cover:
- growth without moving existing managed objects
- page-boundary allocations
- exact heap-limit allocations
- deterministic failure before the configured heap limit
- deterministic failure when
memory.growcannot grow - structured panic payloads for allocation failure
- arena reset reuse of dynamic allocations after a mark
- rejection of invalid arena reset marks
Host ownership
Managed pointers exported to a host are borrowed. The guest runtime owns the object. A borrowed pointer remains stable after the exporting call returns, but the host must not retain it across Wasm instance reset or a future arena reset.
Browser adapters must refresh typed array views after memory growth. Wasmtime tests can read the exported memory directly after each call.
Host-provided managed pointers must come from the same guest instance or from that instance's exported adapter helpers. Hosts must not synthesize pointers, reuse pointers across instances or resets, or pass pointers to directly mutated runtime memory.
Opaque host handles split ownership. The guest runtime owns only the opaque wrapper object containing a type tag and adapter handle id. The adapter owns the host value behind that id. Clearing or replacing the adapter handle table invalidates handle ids even if old wrapper pointers still exist.
JS host ABI validation rejects ownership-ambiguous managed imports. Imports may receive scalars, strings, or opaque handles. Structured managed values may be returned from exports through reader helpers, but they are not accepted as JS host import parameters until writer and ownership rules are explicit.
Host reader validation
Exported JS adapter reader helpers validate object headers before reading.
These failures trap:
- non-zero pointers outside memory
- unknown runtime object tags
- object payloads whose declared size extends past memory
- string readers called for non-string objects
- handle readers called for non-opaque objects
- field readers called for strings, bit arrays, closures, or opaque objects
- field indexes greater than or equal to the object arity
Only these reader results are sentinel values:
__regulus_value_tag(0)returns0for nil-list/null.__regulus_value_constructor(ptr)returns0for valid non-constructor objects.__regulus_value_arity(ptr)returns0for valid strings and bit arrays.
All other malformed helper calls are caller bugs.
Arena reset reclamation
Arena reset is the selected reclamation strategy. It is the smallest step beyond the bump allocator: save a heap mark before a bounded scope, then reset $__heap to that mark when the scope ends.
__arena_mark() -> i32 returns the current heap pointer. __arena_reset(mark) sets $__heap back to a previous mark. Reset traps if the mark is before the dynamic heap start, after the current heap, or not 8-byte aligned.
Reset invalidates every dynamic object allocated after the mark. Static data and dynamic objects allocated before the mark remain valid. Later allocations may reuse reset-owned memory.
Generated JavaScript adapters wrap exported Gleam calls in an arena scope. The adapter marks before encoding JS arguments, calls the Wasm export, decodes the return into JS-owned data, and resets in a finally block. Raw Wasm and Wasmtime exports are not automatically reset because those callers may inspect borrowed managed pointers after the call.
The CLI run command is ABI-aware for managed returns. When arena helpers are available, it marks before calling an export, decodes the result for display, and resets before printing. String returns are printed as text. Other managed returns use the runtime debug renderer.
Compiler-generated code must not return or retain pointers allocated after a mark that will be reset. General internal reset scopes still require escape analysis or region tracking.
Reference counting is not the selected strategy for this milestone. It would require generated retain/release operations for every managed assignment, field store, capture, return, and host boundary. It would also need handle-table integration and a cycle policy.
Tracing garbage collection is also not selected. Wasm does not expose the operand stack or locals to the runtime, so Regulus would need an explicit shadow stack, stack maps, or generated root-registration code before a tracing collector can find live managed values.
See also: