Skip to main content

import ModuleStatus, { LifecycleBadge, TierBadge, TestCovBadge, } from '@site/src/components/StdlibBadge';

core.mem — Memory management

The implementation of CBGR (Capability-Based Generational References), the three-tier reference model, and the allocator stack. User code typically interacts with Heap, Shared, and references via base; this page documents the full mem API for systems programmers.

Module status

Each stdlib module declares its maturity via the @arch_module(..., lifecycle: Lifecycle.X("vN.M")) directive at the top of its source. The four lifecycle states are:

StatusMeaningAPI stability
🟢 Theorem(v0.1+)Implementation complete, mechanised proof attached, tier-aligned test suite green on both --interp and --aot.Stable — additions only, no breaking changes inside the major version.
🟡 Conjecture(v0.1)Implementation complete; proof in progress; tests passing in at least one tier.Stable in spirit — may receive bug-fix breakage if a defect surface forces a rename.
🟠 Draft(vX)Implementation incomplete or partially gated behind feature flags; API may still shift.Unstable — pin a specific Verum version.
⚫ DeprecatedSuperseded by another module; kept for source-level compatibility until next major version.Will be removed — migrate per the deprecation note.

Orthogonal coverage axes:

AxisSymbolsMeaning
Tier--interp ✓ / --aotBoth tiers exercise the module's public surface; --interp is mandatory, --aot is "tier-aligned" gate.
Test coverage🟢 full / 🟡 partial / 🔴 none"Full" requires core-tests/<x>/<y>/{unit,property,integration,regression}_test.vr + audit.md.

core.mem per-module status

The table reflects @arch_module(... lifecycle: ...) declared in each source file PLUS the test-coverage state in core-tests/mem/.

FileLifecycleTierTestsNotes
capability.vrcore-tests/mem/capability/ — 4 files + audit
header.vrcore-tests/mem/header/ — 4 files + audit
size_class.vrcore-tests/mem/size_class/ — 4 files + audit; uncovered clz_u64 → ctlz + PAGE_HEADER_SIZE drift defects (both closed)
thin_ref.vrcore-tests/mem/thin_ref/ — 4 files + audit. D2/CLASS-9 CLOSED 2026-05-29: UseAfterFreeError.new(...) cross-module field round-trip now correct — root was resolve_field_index's descriptor path comparing fd.name.0 (a ctx.strings StringId) against field_name_indices[field] (a separate intern namespace), false-matching at index 0; fixed by string-authoritative resolution. .new(...)/.message()/.eq() tests un-ignored, all GREEN under --interp (audit §8).
fat_ref.vrcore-tests/mem/fat_ref/ — 4 files + audit (static-shape only)
hazard.vrcore-tests/mem/hazard/47/47 GREEN 2026-07-05 (was whole-module SIGSEGV): live hazard_stats() / force_reclaim_all() / cleanup_thread_hazards() all pass after TYPE-NAME-INFERENCE-1 + PROTOCOL-ITER-1 + CALLSYNC-R0-CLOBBER-1 (audit §8)
epoch.vrcore-tests/mem/epoch/ — read + write surface. D1 CLOSED 2026-05-29: increment_epoch_for_tests re-routed through the scalar-shadow cell + atomic ops (the implemented epoch mechanism that current_epoch/reset_for_tests already use), instead of the unimplemented record-shaped static mut method path; 3 increment tests un-ignored, GREEN under --interp. The general &mut static_mut_struct.scalar_field codegen cell-backing remains a separate language-level gap (task #9).
allocator.vrcore-tests/mem/allocator/ — static-shape + live cbgr_alloc round-trip via public Heap<T> / Shared<T> (audit §A closed); §B realloc-cross-boundary + §C ctx-allocator + §D protocol-impls + §E AllocStats + §F AOT sweep open
arena.vrcore-tests/mem/arena/ — constants + ArenaConfig.{default,fixed,custom} constructors + ArenaError 4-variant is-disjoint sweep + per-variant .message() payload-content assertions all green; live-lifecycle suite GREEN (task #8 closed 2026-05-24); single-field record-variant Eq pin un-@ignore'd 2026-07-05 — 60/60/0
segment.vrcore-tests/mem/segment/ — Mimalloc-style 32 MiB chunks
heap.vrcore-tests/mem/heap/ — thread-local fast path; live heap_alloc lifted via public Heap.new (audit §B closed); HeapError 7-variant + HeapStats 8-field surface exhausted + From<SegmentError> lift covered (audit §D closed)
diagnostics.vrcore-tests/mem/diagnostics/ — read-only observer surface
cap_audit.vrcore-tests/mem/cap_audit/ — capability transition events
cap_audit_ring.vrcore-tests/mem/cap_audit_ring/ — lock-free SPMC ring
mem_raw.vr (re-exported)memcpy/memmove/memset/memcmp/strlen/strcmp — see core-tests/intrinsics/
mod.vr (module root)core-tests/mem/mod/ — 4 files + audit. Module-root surface: UseAfterFreeError (5-field record + 3 ctors + message + Debug + Display + Eq) + RevocationError (4-variant sum + 4 ctors + message + Debug + Display + Eq) + CbgrTier (4-variant sum) + get/set_execution_tier global accessor. 41 unit + 19 property + 18 integration + 9 regression tests cover module-root types + the umbrella re-export contract (every submodule symbol resolves via mount core.mem.{Name}). All pins closed — the §3.1 field-shift trio un-gated in earlier waves and the §3.4 umbrella has_capability collision un-@ignore'd 2026-07-05 (public-mount re-export traversal resolves the binding authoritatively): 87/87/0.

The dedicated-suite-pending modules are tracked in core-tests/INVENTORY.md; new modules graduate to once all four test files land and the audit deferrals all close on both tiers.

Defect status (2026-07-06 --aot) — 123 → 48 failures; 6 modules green both tiers

Six fundamental classes landed (53e13bf29 + 72d2a9578; the second re-lands the interp handler batch dropped by a concurrent branch reset). Post-fix per-module --aot (was → now): size_class 50/38 → 88/0, capability 93/4 → 97/0, fat_ref 40/2 → 42/0, cap_audit 39/1 → 40/0, epoch 43/0, diagnostics 29/0, hazard 44/3 → 46/1, header 58/9 → 65/2, cap_audit_ring 27/9 → 34/2, allocator 40/23 → 50/13, arena 51/9 → 53/7, segment 38/7 → 39/6, heap 45/7 → 46/6, thin_ref 38/1 → 38/1, mod 77/10 → 77/10. Interp gate re-validated on the same binary: 861/861/0.

ClassOne line
CONST-ARRAY-RETTYPE-1Composite-typed consts archived return_type=Unit → AOT GetE indexed the zeroed object header (SIZE_CLASSES[0]=0). Declared TypeRef now rides into the descriptor.
DEREF-INTERIOR-1Deref of a RefListElement result double-dereferenced (record element's zero header word as an address — ring commit EXC 0x18); interior marks pass through Deref and survive Mov.
CLONE-AOT-ALIAS-1AOT Clone pointer-aliased heap objects ([Slot{…}; 256] = 256 aliases of one slot); now checked_malloc+memcpy with statically-tracked sizes, cleared on register reuse.
HEAP-INTORAW-1 (AOT twin)Transparent-Heap identity for into_raw/from_raw + statically-true is_valid on both dispatch forms.
SIGNATURE-PREPASS-1Stage-1 method stubs carry declared return types — the for-in classifier now sees List.drain → Drain<T> regardless of module order; reclaim compiles to the protocol loop on BOTH tiers.
PRECOMPILE-TARGET-SWEEP-1Stdlib walkers skip target/ — harness residue was compiled as stdlib (355 of 580 field-intern fallbacks) and thrashed the precompile hash.

Residual 48, attributed on pure HEAD (d1c721f36..14007a8a1 window, minimal repros filed): the archive f-string-of-int .message() memmove crash under --aot (every error-variant message test across segment/arena/heap/allocator/mod), Shared.strong_count AOT, ring recent() slot-0 skip, and 4 singletons — the concurrent text/float-AOT wave's frontier; coordination data handed over.

Defect status (2026-07-05 --interp) — frontier CLOSED

All 15 modules GREEN: 861 passed / 0 failed / 0 ignored at --test-threads 4 (parallel runs no longer crash). The three residual defects from the 2026-06-19 table below plus two regressions that had accumulated since are closed by five fundamental verum_vbc fixes (commit 799cff9b2); the last three @ignore pins are un-gated (c3e4236bb).

Closed defectClassRoot cause → fix
hazard whole-module SIGSEGV (killed every parallel run)TYPE-NAME-INFERENCE-1 + PROTOCOL-ITER-1 + CALLSYNC-R0-CLOBBER-1SCREAMING_CASE static-mut receivers classified as TYPE namespaces (miss-fallback returned the static's NAME as a type) and Path-only instance-receiver resolution left self.retired.drain(…) untyped → for-in lowered a Drain<T> through native IterNew, which mapped every non-builtin type_id to ITER_TYPE_LIST and read the record as a List header (value-dependent SIGSEGV). IterNew/IterNext now dispatch the Iterator protocol for unknown records via resolved <Type>.next; the protocol call surfaced a latent kernel bug — call_function_sync clobbered the caller's r0 (self) via return_reg=Reg(0) — fixed with save/restore.
cap_audit_ring 27/9 (commit wrote .event at slot 4 of a 2-field record; count() stuck at 0)CAP-AUDIT-SLOT-LAYOUT-1 + ATOMIC-CAS-ZEROINIT-1The Cast inference arm resolved only single-ident Path targets, so as *mut CapAuditSlot carried no type and the deref-field write fell to the global intern (proven via VERUM_TRACE_FIELDSHIFT). Independently, size-8 atomic CAS compares NaN-boxed patterns while a fresh static-mut cell is raw zero — NEXT_SEQ's inlined fetch_add lost every increment. Cast/Deref arms now resolve every type shape (incl. *mut/*const carriers); the CAS accepts the raw-zero never-stored pattern for expected == 0.
allocator 61/2 (Heap.into_raw field-OOB; Shared.strong_count not found)HEAP-INTORAW-1 / SHARED-STRONGCOUNT-1The Tier-0 runtime wrapper representations (Heap<T> = CBGR data pointer; Shared<T> = [refcount, value] object) do not match the source-level records, so the compiled bodies misread memory. Shape-guarded intercepts on both dispatch paths (wrapper_runtime.rs static-Call twin + CallM arms); clone bumps the refcount on all three clone arms, binding-drop decrements it, *shared derefs to the inner value.
heap get_heap_stats NullPointer whenever core.mem.segment was mountedXMOD-CALL-ID-BAND-1Archive bytecode kept ctx-GLOBAL ids for cross-module calls while local calls were remapped to [0, N) — overlapping id spaces made the id-keyed external-name lookup structurally ambiguous (heap's LOCAL get_heap id collided with the recorded cross-module id of atomic_fetch_add_int). Cross-module ids now re-home into the reserved band [0x2000_0000, 0x4000_0000) at precompile — disjoint spaces by construction.
3 @ignore pins (mod §3.4 umbrella has_capability, diagnostics §B can_read 5-way collision, arena single-field-variant Eq)dispatch-collision classClosed by the interim dispatch waves (public-mount re-export traversal; receiver-type-aware CallM; single-field unboxing); each verified green by direct probe before un-gating.

Cross-tier: the --aot sweep is in flight; results land in this table and the frontmatter when complete. Attribution note: the time/ and async/ suite failures visible on current main reproduce identically on pure HEAD without this batch (clean-baseline build d1c721f36) — they belong to the concurrent intrinsics wave and are tracked separately.

Defect status (2026-06-19 --interp) — historical

The five mem-codegen-fixes codegen fixes (table below) were consolidated and re-validated this cycle on branch mem-conformance-work (isolated worktree, dedicated CARGO_TARGET_DIR, archive regenerated), PLUS a new static-mut method-receiver fix (commit on this branch). 12 of 15 modules are now fully green: header 67/0, thin_ref 39/0, fat_ref 42/0, capability 97/0, size_class 88/0, arena 59/0, diagnostics 28/0, epoch 43/0, cap_audit 40/0, mod 86/0, heap 52/0. The 4 partial modules (allocator 61/2, cap_audit_ring 27/9, hazard 40/7, segment 44/1) are now THREE distinct codegen defects (see the "Still open" table below), not one family.

Resolved (branch mem-codegen-fixes):

WasNowRoot cause → fix
epoch 40/343/0@thread_local record static mut (THREAD_EPOCH_CACHE) is not cell-backed (its &mut self method read garbage); @thread_local scalar static-mut IS. → cached_epoch/invalidate_epoch_cache rerouted to thread-local scalar shadows, mirroring GLOBAL_EPOCH_COUNTER.
cap_audit_ring SIGSEGV27/9extract_type_name_from_ast had no Array arm → static mut R:[T;N] got a garbage type-name → R[i].field resolved against the global field interner (wrong slot; slot.state read garbage → SIGSEGV). → added the Array arm + extract_element_type strips [T]/[T;N].
allocator 56/761/2(a) array-const name-collision allocator.SIZE_CLASSES:[Int;11] vs size_class.SIZE_CLASSES:[Int;73]compile_pending_constants looked the pending const up by bare name so both shared one FunctionId and dedup dropped one (loser's qualified archive descriptor vanished) → now looks up by source-module-qualified name. (b) Heap/Shared wrapper-method dispatchlet b = Heap.new(rec) was typed as the inner T, so b.is_valid() dispatched to T → type the binding as the wrapper Heap<T>/Shared<T> + auto-deref field access to the inner T.
mod 84/286/0HEADER_SIZE umbrella re-export tie-break was pure-alphabetical, so the type-associated core.mem.MemSegment.HEADER_SIZE beat the module core.mem.header.HEADER_SIZE. → prefer a module parent (lower_snake) over a type parent (CapitalCase).

Closed this cycle: cap_audit 38/2 → 40/0. The two failures were a test bug, not field contamination: the tests read event.epoch_at_event, which is not a field of CapEvent (the field is timestamp_ns). The bare name resolved leniently to a wrong slot (returning a contaminated value) instead of erroring, so the field-independence asserts compared garbage. Pointed at the real field. (The lenient unknown-field resolution that returns a wrong slot instead of a hard error is itself a language defect — tracked.)

Closed this cycle (#2): heap 51/1 → 52/0 via the static-mut method-receiver fix above (get_heap_statsget_heap()CURRENT_HEAP.as_mut()).

Still open (now three DISTINCT defects, not one family):

Module(s)DefectRoot causeFix locus
cap_audit_ring (9)InvalidOpcode 29 (pc 254)static-mut array element address-of &mut CAP_AUDIT_RING[idx] in the commit/recent archive bodies emits mis-sized operands → PC desync. (Standalone repro: &mut ARR[idx] on a static mut [Rec; N] → null deref / RefListElement on non-List backing.)VBC codegen: static-mut array element address-of + archive regen.
segment (1)field access OOB type_id=0 in lock_segmentsresult.1 of atomic_compare_exchange_u32 (a tuple return) mis-dispatches to LocalHeap.release_segment instead of projecting tuple field 1.tuple-projection field-access codegen.
hazard (7)NullPointerAt 0x62 in LocalHeap.free_localrecord-static-mut field / list walk (THREAD_HAZARD_RECORD.retired, GLOBAL_HAZARD_DOMAIN) reaches a null interior — the method-receiver fix cleared the .scan_hazards() dispatch but the record-field reads underneath still null-deref.record-static-mut field read backing.
allocator (2)Shared.strong_count not found on Object; Heap.into_raw field-OOBShared.new(99) infers no inner type from an unsuffixed int literal (binding falls back to Object, method-table lookup fails between Shared/Weak candidates); into_raw by-value self.literal→Int default; by-value-self dispatch.

Root-cause correction (2026-06-19): the earlier hypothesis that hazard/heap/segment shared a "record static mut write-persistence / cell-backing" defect was disproved by minimal repros — heap-object lifetime across frames is fine (static mut Maybe<Rec> whole read/write and cross-frame reads pass). heap was actually the method-receiver mis-dispatch (now fixed); hazard/segment are the two distinct codegen issues above.

Round-17 expansion (2026-05-28) — core.mem.mod/ first-pass + foundation-layer property law sweeps

Three commits landed:

1. mem/mod/ 4-file conformance suite (commit 8efa39d08) — first coverage of the core/mem/mod.vr umbrella manifest. 41 active unit + 19 property + 18 integration + 9 regression tests + audit.md covering the 3 module-root types (UseAfterFreeError, RevocationError, CbgrTier) and the umbrella re-export contract.

2. mem/epoch/ + mem/hazard/ property sweep (commit 84a329253) — epoch property 85→250 LOC (4→13 laws); hazard property 38→235 LOC (3→17 laws); hazard integration 41→200 LOC (2→11 tests). Closes foundation-layer algebraic-law gaps for EpochCache 3-field isolation, needs_reclaim threshold sweep, estimated_retired_bytes 5×5 Cartesian product law, MAX_THREADS / RETIRED_THRESHOLD power-of-two pins, and footprint analysis composed with HEADER_SIZE / SEGMENT_SIZE / DEFAULT_ARENA_CAPACITY.

3. mem/heap/ + mem/arena/ property sweep (commit d533408b7) — heap property 53→300 LOC (3→16 laws); arena property 40→230 LOC (4→18 laws). HeapStats 8-field isolation + balance algebra (alloc_count == dealloc_count + live_count no-leak invariant + bytes balance + monotonicity); ArenaConfig .default()/.fixed()/.custom() constructor invariants + ArenaError 4-variant disjointness + payload-conjunctive Eq laws.

NEW defects surfaced

§1. Cross-module instance-method-body field-access shift

UseAfterFreeError.message() body reads self.<field> at the WRONG offsets when invoked on instances constructed in test code, because the method body's compilation context is core/mem/mod.vr and the precompiled-archive's field layout for UseAfterFreeError isn't fully threaded into compile_field_access at the method-body codegen site.

Demonstration (interpolated output under --interp 2026-05-28):

let e: UseAfterFreeError = UseAfterFreeError {
expected_gen: 5, actual_gen: 6,
expected_epoch: 1, actual_epoch: 2,
type_name: "Shared<Int>",
};
print(e.message());
// Output: "use-after-free detected for 1: expected gen=5 epoch=5,
// actual gen=6 epoch=6"

Decoded shift:

FieldLogical slot.message() reads slot
expected_gen00 ✓
actual_gen11 ✓
expected_epoch20 ❌ (reads expected_gen)
actual_epoch31 ❌ (reads actual_gen)
type_name42 ❌ (reads expected_epoch)

Same root cause class as the existing use_after_free_error_field_shift_2026-05-27 defect (see below) but surfaced at the instance-method-body codegen site rather than the cross-module static-method return path. Pinned @ignore'd in core-tests/mem/mod/unit_test.vr §1-§2.

§2. Umbrella-mount dispatch collision: has_capability

When has_capability is mounted via the umbrella (mount core.mem.{has_capability} routing through mod.vr's public mount .capability.{has_capability} re-export), a 2-arg call has_capability(flags, cap) is dispatched to the SAME-NAME 2-arg method AllocationHeader.has_capability(&self, cap) defined at core/mem/header.vr:636.

let flags: UInt16 = CAP_OWNED;
assert(has_capability(flags, CAP_READ));
// Runtime: NullPointerAt { op: "opcode 0x78",
// site: "AllocationHeader.load_capabilities",
// pc: 0 }

The first UInt16 argument (CAP_OWNED) is re-interpreted as a &AllocationHeader pointer (= null), faulting on the first self.load_capabilities() call.

Direct submodule mount works: mount core.mem.capability.{has_capability} resolves to the free function correctly (29 GREEN tests in core-tests/mem/capability/). The defect is specific to umbrella-mount dispatch, not bare-name dispatch.

Fundamental fix surface:

  1. The dispatcher's bare-name lookup must distinguish free-fn-arity-N from impl-block-method-arity-(N-1)-plus-receiver dispatch.
  2. Or — umbrella-re-exported free fns must carry their canonical source-module identity in their function-id key.

Pinned @ignore'd in core-tests/mem/mod/unit_test.vr §8 + regression_test.vr §H.

Round-14 expansion (2026-05-27) — +26 new integration tests

The latest expansion landed 26 new integration tests across 4 mem submodules under --interp:

SubmoduleNew testsSections covered
capability+9§7 composition with GEN_* lifecycle; §8 capability lattice ordering (top/bottom/idempotence/associativity); §9 has_capability bit-mask invariants (monotone-under-or, zero-mask, has_all_capabilities-universal-self)
size_class+6§9 aligned_size semantic (passthrough for align ≤ MAX_ALIGN_SIZE, overhead for oversized); §10 round-trip law size_to_bin · bin_to_size + monotone-over-doubling; §11 blocks-per-page lower bound
header+6§6 9-flag power-of-two layout + 6 pairwise-distinctness; §7 GEN_UNALLOCATED < GEN_INITIAL < GEN_MAX < UInt32.MAX chained inequality + headroom > 2^31; §8 compound flag operations (OR/XOR)
heap+5§4 HeapStats zero-state invariants (8-field baseline + live-count + bytes-outstanding + page-and-cache activity); §5 DIRECT_LOOKUP_SIZE + PAGE_HEADER_SIZE drift pins

All 26 tests pass under --interp (~28-30s each). Test budget for full round-14 sweep: ~13 minutes wall-clock.

Known open defect — cross-module record-return field-access shift

The UseAfterFreeError.new(...) (5-arg static constructor) returns a record whose field reads at the test site shift by +2 indices. The constructor body writes fields at correct offsets; the test-side field READS land on wrong slots because compile_field_access falls through resolve_field_index's type-aware lookups and lands on the global intern_field_name(field_name) fallback.

Affected sites: every cross-module Type.new(...) (5+ args) call where the test expects to read distinct field values back. Workaround pinned in core-tests/mem/thin_ref/unit_test.vr §5: construct via direct record literal at the test site, NOT via the cross-module .new(...) constructor.

Attempted fix: a defensive self.types-by-name fallback in resolve_field_index (commit ab8e707f4) regressed 3 previously-GREEN record-literal tests; reverted in commit 585728904. The correct fundamental fix must preserve the 4-way cache consistency (type_name_to_id, self.types, type_field_layouts, type_field_type_names) holistically — likely at the archive-load path (import_archive_type_with_protocol_remap in crates/verum_vbc/src/codegen/mod.rs:15771-15820) rather than at downstream consumers.

Pinned by memory entry use_after_free_error_field_shift_2026-05-27.md, audit core-tests/mem/thin_ref/audit.md §6-§7, and the corresponding @ignore pins in unit_test.vr §6 (3 tests in thin_ref + 1 in diagnostics). Same defect class as [[btree_pattern_match_ref_generic_class]] and [[enactment_field_access_oob_2026-05-24]].

Cross-tier validation status

The Tier column reflects validated status under verum test --interp (Tier 0 VBC interpreter) as of the latest mem-suite sweep. Tier 2 (verum test --aot, LLVM AOT) verification of the full suite was, until 2026-06-01, blocked at the runner level: verum test --aot runs with [test].parallel = true by default, and the runner contained two independent parallelism bugs that crashed the whole run with an in-process compiler SIGSEGV during generate_native (see defect-class catalogue §23 — AOT-PARALLEL-1 (verum repo, docs/architecture/defect-class-catalogue.md)):

  1. Colliding artifact paths. Per-test build artifacts (the merged target/test/test_<stem>.merged.vr, the output binary, the derived .o/.ll, and the shared verum_runtime_stubs.c/.o) were keyed on the test file's file_stem, which repeats across every module (all unit_test.vr → stem "unit_test"). Parallel workers clobbered each other's files → corrupt source → malformed IR → SIGSEGV. Fixed by unique_merged_stem (folds the source path + test fn into every artifact name).
  2. LLVM backend not thread-safe. Even with unique artifacts, LLVM's per-process pass registry / subtarget caches / cl::opt globals are not safe to drive from multiple threads at once. Fixed by a process-global llvm_backend_lock() around the optimisation + object emission window.

(The earlier "WinSock recv arity" hypothesis was disproved — a single --aot test compiles and passes; the blocker was the parallel runner, not a stdlib symbol.) With both fixed, verum test --aot completes.

First post-fix --aot baseline (mem capability + cap_audit + cap_audit_ring, 173 tests, 0 compiler SIGSEGV): 125 pass / 48 fail. Every failure is the same per-test defect — AOT-ITER-1 (catalogue §18): tests whose bodies iterate for x in …iter() (the exhaustive property laws and integration scenarios) crash at runtime ("process terminated by signal") on the raw &unsafe T iterator backing-pointer deref. Pure-data tests (constants, ADT construct/match, field round-trips) pass --aot cleanly. So the Tier column stays --interp per-module until AOT-ITER-1 §18 closes — that single codegen fix un-crashes the for-loop tests suite-wide and lets the mem modules promote to --interp ✓ / --aot ✓. (Record-variant construction, catalogue §-pending, is a smaller secondary surface.)

File-by-file API surface

FileWhat's in it
allocator.vrAllocator protocol, cbgr_alloc/cbgr_dealloc/cbgr_realloc, Layout, AllocError
header.vrAllocationHeader (32-byte CBGR metadata), MemValidationError / ValidationError alias, FLAG_* bits
thin_ref.vrThinRef<T> (16 bytes)
fat_ref.vrFatRef<T> (32 bytes)
hazard.vrHazardGuard<T> — concurrent-safe deref protection
epoch.vrEpochManager — generation wraparound safety
capability.vrCapability bits — read/write/execute/delegate/revoke/borrowed/mutable/no-escape; pack_epoch_caps / unpack_*
arena.vrGenerationalArena<T> — O(1) mass invalidation
segment.vrSegment — 32 MiB virtual regions, mimalloc-style
size_class.vr73-bin size class table (Mimalloc-style); size_to_bin / bin_to_size / aligned_size
heap.vrLocalHeap — thread-local allocation
diagnostics.vrRead-only MemHeaderView observer surface; live_allocations
cap_audit.vrCapEvent capability-transition event type
cap_audit_ring.vrLock-free SPMC ring buffer for CapEvent; record_revoke / record_attenuate / record_ref_* / record_gen_bump
mem_raw.vr (re-exported via core.intrinsics.runtime.mem_raw.*)memcpy, memmove, memset, memcmp, strlen, strcmp

References — three tiers

ThinRef<T> — 16 bytes

type ThinRef<T> is {
ptr: *unsafe T,
generation: UInt32,
epoch_caps: UInt32, // high 16 bits: epoch; low 16: capability flags
};

Used for &T when T: Sized. The generation and epoch_caps are fixed at reference creation; the CBGR check compares them against the allocation's AllocationHeader on every deref.

FatRef<T> — 32 bytes

type FatRef<T> is {
ptr: *unsafe T,
generation: UInt32,
epoch_caps: UInt32, // epoch in high 16 bits, capabilities in low 16
metadata: UInt64, // slice length, dyn-protocol vtable pointer, etc.
offset: UInt32, // non-zero for interior references
reserved: UInt32, // padding + room for future fields
};

Used when T is unsized — slices ([T]) and protocol objects (dyn P) — and for interior references that need an offset into a larger allocation.

AllocationHeader — 32 bytes, cache-aligned

type AllocationHeader is {
generation: UInt32, // incremented on free
epoch: UInt32, // wraparound-safety
flags: UInt32, // drop impl, pinned, capabilities
layout_size: UInt32, // for realloc / sanity
_padding: UInt64, // align to 32 bytes
layout: Layout, // size, align
};

Prepended to every CBGR-tracked allocation. The header lives in the same cache line as (or adjacent to) the object, so the CBGR check is typically a hot L1 hit.

CBGR check sequence (conceptual)

fn deref<T>(r: ThinRef<T>) -> &T {
let hdr = header_of(r.ptr);
if hdr.generation != r.generation {
handle_use_after_free(&r, &hdr);
}
if (r.epoch_caps >> 16) != hdr.epoch {
handle_epoch_mismatch(&r, &hdr);
}
unsafe { &*r.ptr }
}

Measured: ~0.93 ns on the production_targets bench (x86_64 release build), well under the ≤ 15 ns design target.


Heap<T> — unique owned allocation

Heap.new(value) -> Heap<T> // panics on OOM
Heap.new_default() -> Heap<T> // T: Default
Heap.new_zeroed() -> Heap<T>
Heap.try_new(value) -> Result<Heap<T>, AllocError>
Heap.from_raw(ptr) -> Heap<T> // unsafe

Introspection

h.as_ref() -> &T h.as_mut() -> &mut T
h.into_inner() -> T h.into_raw() -> &unsafe T (leaks)
h.leak() -> &mut T // leaks; returns static-lifetime mut ref
h.generation() -> UInt32
h.epoch() -> UInt16
h.capabilities() -> UInt16
h.is_valid() -> Bool
h.is_allocated() / h.is_freed() -> Bool
h.header_generation() / h.header_epoch() / h.header_size()

Implements

Deref, DerefMut, Drop, Clone (deep-copy if T: Clone), Debug, Eq, Ord, Hash, Default (if T: Default).


Shared<T> — atomically ref-counted

Shared.new(value) -> Shared<T>
s.clone() -> Shared<T> // bumps refcount
s.weak() -> Weak<T> // does not bump strong count
Shared.strong_count(&s) -> Int
Shared.weak_count(&s) -> Int
Shared.try_unwrap(s) -> Result<T, Shared<T>> // succeeds if strong_count == 1
Shared.get_mut(&mut s) -> Maybe<&mut T> // Some if unique

Weak<T>.upgrade() -> Maybe<Shared<T>> — returns Some if the target is still live. Used to break reference cycles.


Allocator protocol

type Allocator is protocol {
fn alloc(&self, layout: Layout) -> Result<*mut Byte, AllocError>;
fn dealloc(&self, ptr: *mut Byte, layout: Layout);
fn realloc(&self, ptr: *mut Byte, old: Layout, new: Layout)
-> Result<*mut Byte, AllocError>;
}

Layout

type Layout is {
size_: Int, // bytes
align_: Int, // bytes (power of 2)
};

implement Layout {
fn new<T>() -> Layout // T.size, T.alignment
fn from_size_align(size: Int, align: Int) -> Layout // panics on bad input
fn try_from_size_align(size: Int, align: Int)
-> Result<Layout, AllocError> // fallible
fn from_size(size: Int) -> Layout // natural alignment
fn size(&self) -> Int
fn align(&self) -> Int
fn repeat(&self, n: Int) -> Layout // [T; N] layout
fn try_repeat(&self, n: Int) -> Result<Layout, AllocError>
fn extend(&self, other: Layout) -> Layout // sequential layout
fn try_extend(&self, other: Layout) -> Result<Layout, AllocError>
}

from_size_align panics on invalid alignment (non-positive or non-power-of-2) or negative size — use try_from_size_align at any trust boundary (FFI, deserialised input).

AllocError

type AllocError is
| OutOfMemory { requested: Int }
| InvalidSize { size: Int }
| InvalidAlignment { alignment: Int }
| MmapFailed { code: Int }
| MunmapFailed { code: Int }
| PageExhausted
| InvalidPointer
| CapacityOverflow { requested: Int }
| UnsupportedOs { op: Text }
;

e.message() -> Text // human-readable

Implements Display (routes via .message()), Debug, and Eq (per-variant; payload-bearing variants compare payloads).

Default allocator — cbgr_alloc

unsafe fn cbgr_alloc(layout: Layout) -> *mut Byte
unsafe fn cbgr_alloc_zeroed(layout: Layout) -> *mut Byte
unsafe fn cbgr_dealloc(ptr: *mut Byte, layout: Layout)
unsafe fn cbgr_realloc(ptr: *mut Byte, old: Layout, new: Layout) -> *mut Byte

Context-scoped allocator

set_context_allocator(alloc: &dyn Allocator)
ctx_alloc(layout: Layout) -> Result<*mut Byte, AllocError> using [Allocator]
ctx_dealloc(ptr, layout) using [Allocator]

Using an arena or slab allocator for a task tree:

let arena = GenerationalArena.new(capacity: 1 << 20);
provide Allocator = arena in {
build_parse_tree(source).await
};
// Dropping the scope drops all arena memory in O(1).

Alignment

fn align_up(x: Int, align: Int) -> Int
fn align_down(x: Int, align: Int) -> Int
fn is_aligned(x: Int, align: Int) -> Bool

Hazard pointers

type HazardGuard is { ... };
acquire_hazard(ptr: &unsafe Byte) -> HazardGuard
guard.release() // explicit drop also works
force_reclaim_all() // scan + reclaim retired nodes
cleanup_thread_hazards() // called on thread exit

Used internally to keep reads safe against a concurrent free. A reader installs its target in a hazard slot before the CBGR check; a freer scans all hazard slots before returning memory to the pool.

Observability — HazardStats

type HazardStats is {
protected_count: Int, // currently protected pointers across all threads
retired_count: Int, // retired nodes awaiting reclamation
thread_count: Int, // registered threads
};

hazard_stats() -> HazardStats

implement HazardStats {
fn needs_reclaim(&self) -> Bool {
self.retired_count >= RETIRED_THRESHOLD
}
fn estimated_retired_bytes(&self, avg_size: Int) -> Int {
self.retired_count * avg_size
}
}

Constants

const HAZARD_POINTERS_PER_THREAD: Int = 8; // per-thread slot count
const RETIRED_THRESHOLD: Int = 64; // amortises scan cost
const MAX_THREADS: Int = 256; // global cap

Epoch manager

type EpochManager is { ... };

EpochManager.global() -> &EpochManager
mgr.current() -> UInt32
mgr.advance() // bump epoch (typically timer-driven)
mgr.register_thread()
mgr.retire(callback: fn())

Epochs are the safety net for 32-bit generation wraparound: each thread carries an epoch that advances periodically (~1 kHz), and a reference with a stale epoch fails the check even if the generation field collided.


Capabilities

type Capability is UInt16; // bitflags

const CAP_READ: UInt16 = 0x0001;
const CAP_WRITE: UInt16 = 0x0002;
const CAP_ADMIN: UInt16 = 0x0004;
const CAP_DELEGATE: UInt16 = 0x0008;
const CAP_REVOKE: UInt16 = 0x0010;
// Application-defined: bits 5..15

Embedded in the low 16 bits of the epoch_caps field of references. Database with [Read] compiles to a reference with only CAP_READ set; attempts to call a write method hit a compile-time check against the method's required capability set.


GenerationalArena<T>

type GenerationalArena<T> is { ... };

GenerationalArena.new(capacity) -> GenerationalArena<T>
a.insert(value) -> ArenaHandle<T>
a.get(handle) -> Maybe<&T>
a.get_mut(handle) -> Maybe<&mut T>
a.remove(handle) -> Maybe<T>
a.clear() // O(1) mass invalidation via epoch bump
a.len() / a.is_empty() / a.capacity()

Arenas are the idiomatic choice for:

  • AST trees (parser lifetimes)
  • Game engine objects (frame-scoped)
  • Request-scoped data (web-server tasks)

Configuration — ArenaConfig

type ArenaConfig is {
initial_capacity: Int, // bytes
max_capacity: Int, // bytes; 0 = no limit
growth_factor: Int, // percentage; 200 = double on growth
};

ArenaConfig.default() // 64 KiB / 256 MiB / 2×
ArenaConfig.fixed(capacity: Int) // capacity / capacity / no-grow (100%)
ArenaConfig.custom(initial, max, growth)

Errors — ArenaError

type ArenaError is
| OutOfMemory { requested: Int, available: Int }
| ExceedsMaxCapacity { requested: Int, max: Int }
| InvalidAlignment { alignment: Int }
| AlreadyDestroyed
;

e.message() -> Text // human-readable formatter

Implements Display (routes via .message()) and Debug (renders the variant + payload braces).

Constants

const DEFAULT_ARENA_CAPACITY: Int = 65_536; // 64 KiB
const MAX_ARENA_CAPACITY: Int = 268_435_456; // 256 MiB
const DEFAULT_GROWTH_FACTOR: Int = 200; // 200% = double
const ARENA_ALIGNMENT: Int = 8; // 64-bit word
const ARENA_GEN_INITIAL: Int = 1; // matches CBGR GEN_INITIAL

Segment allocator (internal)

// Partitioning constants
const SEGMENT_SIZE: Int = 32 * 1024 * 1024; // 32 MiB
const SLICE_SIZE: Int = 64 * 1024; // 64 KiB
const SLICES_PER_SEGMENT: Int = 512; // 32 MiB ÷ 64 KiB
const SEGMENT_ALIGN: Int = SEGMENT_SIZE;
const SMALL_PAGE_SIZE: Int = SLICE_SIZE;
const MEDIUM_PAGE_SIZE: Int = 8 * SLICE_SIZE; // 512 KiB
const LARGE_PAGE_THRESHOLD: Int = MEDIUM_PAGE_SIZE;

// Slice state bytes
const SLICE_FREE: UInt8 = 0;
const SLICE_USED: UInt8 = 1;
const SLICE_SPAN_START: UInt8 = 2;
const SLICE_SPAN_CONTINUE: UInt8 = 3;

// Segment kinds
const SEGMENT_NORMAL: UInt8 = 0;
const SEGMENT_HUGE: UInt8 = 1;

type Segment is MemSegment; // alias
type SegmentError is
| MmapFailed { code: Int }
| MunmapFailed { code: Int }
| OutOfMemory
| UnsupportedOs { op: Text }
;

fn segment_alloc(thread_id: UInt64) -> Result<&mut MemSegment, SegmentError>
fn segment_free(seg: &mut MemSegment)
fn segment_abandon(seg: &mut MemSegment)
fn ptr_to_segment(ptr: &unsafe Byte) -> &MemSegment

Allocations are grouped into 73 size classes spaced at ~12.5% intervals (see size_class.vr). Small objects come from thread-local segments via segment_alloc; medium / large allocations are bookkept separately.


LocalHeap

type LocalHeap is { ... };

// Lifecycle
init_thread_heap() -> Result<(), HeapError>
shutdown_thread_heap()
get_heap() -> &mut LocalHeap // lazy-init on first call

// Allocation (Tier-0: through CBGR `cbgr_alloc` rather than directly)
heap_alloc(size: Int) -> Result<(&unsafe Byte, UInt32, UInt16), HeapError>
heap_alloc_zeroed(size: Int) -> Result<(&unsafe Byte, UInt32, UInt16), HeapError>
heap_free(ptr: &unsafe Byte) -> Result<(), HeapError>
heap_free_validated(ptr: &unsafe Byte, gen: UInt32, caps: UInt16)
-> Result<(), HeapError>

// Observability
get_heap_stats() -> HeapStats

heap_alloc returns (ptr, generation, capabilities) — the generation is the CBGR header.generation at allocation time, and the capabilities are the bitflags the allocator deemed safe for the slot. Callers typically don't invoke this directly: the public Heap<T> / Shared<T> constructors are the user-facing API.

HeapStats

type HeapStats is {
alloc_count: UInt64,
dealloc_count: UInt64,
bytes_allocated: UInt64,
bytes_freed: UInt64,
live_count: UInt64,
live_bytes: UInt64,
pages_in_use: UInt32,
segments_owned: UInt32,
};

HeapStats.new() -> HeapStats // every field zero — bootstrap initialiser

Errors — HeapError

type HeapError is
| OutOfMemory
| PageExhausted
| InvalidPointer
| InvalidSize { size: Int }
| InvalidAlignment { alignment: Int }
| SegmentError { inner: SegmentError } // From<SegmentError>
| UseAfterFree
;

e.message() -> Text // human-readable
HeapError.from(seg_err: SegmentError) -> HeapError // From impl

Implements Display (routes via .message()), Debug, and Eq (per-variant; payload-bearing variants compare payloads).

Constants

const DIRECT_LOOKUP_SIZE: Int = 129; // wsize 0..128 (lock-free fast path)
const PAGE_HEADER_SIZE: Int = 128; // cache-line aligned

const PAGE_FLAG_IN_FULL_QUEUE: UInt16 = 0x0001;
const PAGE_FLAG_HAS_ALIGNED: UInt16 = 0x0002;
const PAGE_FLAG_ZERO_INIT: UInt16 = 0x0004;

Thread-local heap. Lock-free fast path; spills into the global heap for cross-thread frees.


CBGR error types

The reference-validation surface returns two error sum types:

UseAfterFreeError — 5-field record

type UseAfterFreeError is {
expected_gen: UInt32,
actual_gen: UInt32,
expected_epoch: UInt16,
actual_epoch: UInt16,
type_name: Text,
};

implement UseAfterFreeError {
fn new(eg: UInt32, ag: UInt32, ee: UInt16, ae: UInt16,
tn: Text) -> UseAfterFreeError
fn null_pointer(type_name: Text) -> UseAfterFreeError
// sets both gens to GEN_UNALLOCATED — `.message()` routes
// through the "null pointer" branch.
fn capability_violation(capability: Text, type_name: Text)
-> UseAfterFreeError
fn message(&self) -> Text // null-pointer / use-after-free branches
}

Implements Display (routes via .message()), Debug, and Eq (field-by-field compare).

RevocationError — 4-variant sum

type RevocationError is
| NullPointer { type_name: Text }
| CapabilityViolation { type_name: Text }
| AlreadyRevoked { type_name: Text }
| Internal { type_name: Text, reason: Text }
;

implement RevocationError {
fn null_pointer(type_name: Text) -> RevocationError
fn capability_violation(type_name: Text) -> RevocationError
fn already_revoked(type_name: Text) -> RevocationError
fn internal_error(type_name: Text, reason: Text) -> RevocationError
}

Capability audit ring

The CBGR system records every capability-state transition as a CapEvent and commits it into a lock-free single-producer / multi- consumer ring (cap_audit_ring.vr). Observers — panic post-mortem handlers, runtime monitors, future debugger UIs — read recent events via recent(n).

CapEventKind — 6-variant tag

type CapEventKind is
| Revoke // capability revoked (e.g., write→read)
| Attenuate // capability narrowed (subset retained)
| RefIncr // reference count increment
| RefDecr // reference count decrement
| GenBump // generation field bumped (free path)
| EpochAdvance // wraparound-safety epoch bump
;

CapEvent — 8-field record

type CapEvent is {
seq: UInt64, // ring-assigned commit sequence; 0 = un-committed
kind: CapEventKind,
target_ptr: UInt64, // address of the affected allocation
generation_before: UInt32,
generation_after: UInt32,
capabilities_before: UInt16,
capabilities_after: UInt16,
epoch_at_event: UInt32,
};

CapEvent.new(kind, target_ptr, gen_before, gen_after, caps_before,
caps_after, epoch) -> CapEvent // returns seq=0
event.bumped_generation() -> Bool // true for Revoke + GenBump

bumped_generation() is kind-driven, not diff-driven: it returns true iff the kind is Revoke or GenBump, regardless of whether the before/after generation values happen to differ. This intent-based semantics matches the doc-comment contract and pinned in the audit suite.

Ring API

const CAP_AUDIT_RING_CAPACITY: Int = 256; // power of 2 for efficient mod

is_enabled() -> Bool
enable() // idempotent
disable()
count() -> UInt64 // total commits since enable
recent(n: Int) -> List<CapEvent> // bounded by min(n, ring fill)

record_revoke(target_ptr: UInt64, gen_before, gen_after, caps_before, caps_after, epoch)
record_attenuate(target_ptr, gen_before, gen_after, caps_before, caps_after, epoch)
record_ref_incr(target_ptr, ..., epoch)
record_ref_decr(target_ptr, ..., epoch)
record_gen_bump(target_ptr, ..., epoch)
record_epoch_advance(target_ptr, ..., epoch)

When the ring is disabled, every record_* writer is a short-circuit no-op — count() does not advance, no allocation, no atomic. This keeps audit-disabled production builds zero-overhead.


Read-only diagnostics

core.mem.diagnostics is the introspection surface — no mutating operations. Used by panic post-mortem handlers, runtime monitors, and future debugger integrations.

MemHeaderView — snapshot of an allocation header

type MemHeaderView is {
generation: UInt32,
epoch: UInt32,
caps: UInt16,
size: UInt32,
align: UInt32,
type_id: UInt32,
flags: UInt32,
ref_count: UInt32,
};

MemHeaderView.from_header(h: &AllocationHeader) -> MemHeaderView

CallFrame — stack-trace entry

type CallFrame is {
function: Text,
file: Text,
line: UInt32,
column: UInt32,
instruction_pointer: UInt64,
};

Functions

live_allocations() -> List<MemHeaderView>
live_allocation_count() -> UInt64
current_call_stack(skip: UInt32) -> List<CallFrame>

The producer-side wiring (writing MemHeaderView snapshots from the allocator on each cbgr_alloc, populating the call-stack from VBC debug info) is owned by the CBGR allocator and the interpreter debugger — those are tested separately.


Raw memory operations

unsafe fn memcpy(dst: *mut Byte, src: *const Byte, n: Int)
unsafe fn memmove(dst: *mut Byte, src: *const Byte, n: Int) // overlap-safe
unsafe fn memset(dst: *mut Byte, byte: Byte, n: Int)
unsafe fn memcmp(a: *const Byte, b: *const Byte, n: Int) -> Int
unsafe fn strlen(ptr: *const Byte) -> Int // NUL-terminated
unsafe fn strcmp(a: *const Byte, b: *const Byte) -> Int

These bypass CBGR. Use only in allocator implementations, FFI boundaries, or when you can prove safety by other means.


Constants

const GEN_INITIAL: UInt32 = 1;
const GEN_MAX: UInt32 = 0xFFFF_FFFE;
const GEN_UNALLOCATED: UInt32 = 0;

const EPOCH_INITIAL: UInt32 = 1;
const EPOCH_INTERVAL_MS: Int = 1; // advance 1000×/s

const SSO_CAPACITY: Int = 23; // Text inline capacity
const PAGE_SIZE: Int = 4096; // architecture-dependent

Errors

type UseAfterFreeError is {
ptr: *unsafe Byte,
gen_expected: UInt32,
gen_actual: UInt32,
epoch_expected: UInt32,
epoch_actual: UInt32,
};

type RevocationError is { ptr: *unsafe Byte, revoker: Text };
type AllocError is OutOfMemory | InvalidLayout | Refused;

On a CBGR violation, the runtime:

  1. Constructs a UseAfterFreeError with full diagnostic context.
  2. Invokes the installed panic handler (default: abort with stack trace).

CBGR execution tiers

type ExecutionMode is Interpreter | Aot;

fn current_mode() -> ExecutionMode
fn is_interpreted() -> Bool

Execution mode affects how the CBGR check is produced, not whether it runs:

  • Interpreter: software check every deref, via the VBC Deref / DerefMut opcodes — the safe-by-default path, because the interpreter validates every reference regardless of the reference's static CBGR tier.
  • AOT: each CBGR tier lowers to a distinct code sequence in LLVM IR. Tier-0 references emit the load-compare-branch pattern; tier-1 references proven safe by escape analysis are elided to a direct load (0 ns); tier-2 &unsafe T references compile to a direct load with no check.

There is no JIT tier in between; a Verum program runs either in the interpreter or as AOT-compiled native code.


Cross-references