Memory diagnostics
This chapter lists the diagnostic codes the compiler emits for
region/lifetime/leak issues, with the smallest reproducer for each and
the canonical fix. For borrow-checker diagnostics that are not
region-specific (ARIA-022, ARIA-023, ARIA-026), see
borrow/diagnostics.md.
ARIA-014 — wild allocation without matching defer free
Every wild allocation must be paired with a release — the simplest
form is defer free(...) in the same scope.
func:bad = int32() {
wild int32:buf = 0i32;
pass 0i32; // [ARIA-014] no defer free for buf
};
Fix: add defer free(buf); immediately after the allocation, or
explicitly call the matching release before every exit path.
func:good = int32() {
wild int32:buf = 0i32;
defer free(buf);
pass 0i32;
};
Alternative (v0.29.3+): import drop.npk and use a struct
binding to opt the binding into RAII auto-free, which silences
ARIA-014 for that shape. See the guide/drop/
cookbook.
ARIA-015 — use of released wild variable
Using a wild value after it has been freed, moved, or otherwise
drop-tracked is rejected.
wild int32:buf = 0i32;
free(buf);
pass buf; // [ARIA-015] buf is released
Fix: rebuild the value (allocate again) or restructure the code so the release happens after the last use.
ARIA-028 — stack escape (return/pass of borrow into a local)
You tried to return, pass, or fail a borrow whose host is a
binding local to the current function (or to an inner block) — the
host's stack frame is destroyed at the function (or block) boundary,
so the reference would dangle.
func:bad = $$m int32() {
stack int32:tmp = 7i32;
$$m int32:r = tmp;
pass r; // [ARIA-028] tmp's frame ends here
};
The same diagnostic also fires when the host is a gc binding: the
underlying object survives on the heap, but the named binding is
local and the borrow path goes out of scope.
Fix: return by value, take ownership in the caller and pass a borrow in, or accept a borrow parameter and return that.
Note (v0.26.6): earlier compiler versions emitted these errors as
ARIA-017; the docs citedARIA-027. Both are unified underARIA-028 STACK_ESCAPEfrom v0.26.6 on. The full text now mentions the host's stack frame and includes an inline fix hint.
Reserved / deferred codes
ARIA-029 and ARIA-031 were reserved here through v0.26.x and
landed in v0.27.2 / v0.27.3 respectively — see the dedicated sections
above. ARIA-032 (handle outlives arena) landed in v0.27.9.
No region codes are currently in the "reserved" state.
ARIA-029 — GC reference from wild (v0.27.2)
A gc binding (or gc field) is being initialised from a borrow
whose origin is a wild allocation. The type checker emits a hint
suggesting the pin operator #x, since the underlying shape would
otherwise leak a wild-region reference into a GC root.
wild int32:w = 5i32;
gc int32@:p = @w; // [ARIA-029] consider `gc int32@:p = #w;`
Fix: pin the wild value (#w), or restructure so the GC binding
owns its value rather than referencing through wild storage.
The borrow-checker rejection in checkVarDecl is defense-in-depth;
the type checker rejects this shape first in surface code.
ARIA-031 — stack reference into a GC field (v0.27.3)
Assigning the address of a stack binding into a field reached
through a gc path would let the GC observe a soon-to-be-dangling
stack pointer.
gc Node:n = Node{ next: @local_stack_node }; // [ARIA-031]
Fix: allocate the referenced node in the gc region, or pin it
(#local_stack_node) so the borrow checker enforces address
stability — and accept that the stack lifetime still bounds the
reference's validity.
ARIA-032 — handle outlives its arena (v0.27.9)
Within a function body, you destroyed an arena and then derefed (or
freed) a Handle<T> bound to that arena.
int64:a = raw HandleArena.create();
Handle<int32>:h = raw HandleArena.alloc(a, 4i64);
raw HandleArena.destroy(a);
int64:p = raw HandleArena.deref(h); // [ARIA-032]
Fix: move the deref/free before the HandleArena.destroy(a)
call, or drop it entirely (a bulk-destroy obviates per-slot frees).
The rule has grown across v0.28.x:
- v0.27.9 — intra-function only: destroy + later deref/free in the same function body.
- v0.28.3 — cross-function Phase 1: a callee that calls
HandleArena.destroy(p)on a parameterpis auto-discovered; the call site firesARIA-032if the matching argument is re-used after the call. - v0.28.4 / v0.28.4.1 — cross-function Phase 2: returning a
handle bound to a local arena (bare or stashed in a struct
field) fires
ARIA-032at thepass/returnsite. - v0.28.5 — FFI passthrough: passing a
Handle<T>directly to anexternfunction emits a warning with the@cast<int64>(h)fix suggestion. The cast is also the way to silence the warning intentionally; once you cross the C boundary the static rule cannot follow you and the runtime generation check is your only safety net. - v0.30.1 — transitive destroy fixpoint: any callee reachable through a chain of bare-identifier calls that eventually destroys a parameter lifts the destroy to the caller. Indirect destroys ship as a warning for one cycle (IPC-DEC-004); intra-function stays an error.
- v0.30.2 — transitive escape: a callee whose return path
builds a handle bound to one of its arena parameters
records that fact in its summary. Caller sites that pass a
local arena to such a callee warn at the binding /
passsite with "inferred from a callee that escapes its arena parameter". - v0.30.3 — cross-module summaries:
use "x.npk".*;now brings function summaries with it, so the v0.28.x and v0.30.1/2 rules fire across module boundaries. Imported externs are no longer a static blind spot; thebug277local-redeclaration workaround is retired (the legacy pattern still works — local re-decl shadows the imported summary). - v0.30.4 —
#[destroys_arena(<param>)]attribute on externs (and self-hosted functions). Seehandles/ffi.md. - v0.30.6 — precision pass: reassigning a destroyed arena
name (
a = raw HandleArena.create();after a priordestroy(a)) clears the destroyed marker; and destroys nested inside anifbranch that terminates the function (exit,pass,fail,return) no longer leak into the post-ifflow.
For cases the static rule still does not catch (handles stashed
in GC objects, handles round-tripped through opaque C state,
indirect aliasing through arrays), the runtime generation check
still applies: deref returns 0i64. See
handles/lifetimes.md for the full
rule, handles/cross_module.md
for the v0.30.x cross-module story, and
handles/diagnostics.md for the
canonical examples.
Runtime bounds-check sentinel (v0.31.13.2+)
When a dynamic array index fails a runtime bounds check, the
compiler-emitted guard dispatches the user's failsafe(tbb32:err)
with a dedicated tbb32 sentinel value of 45 (named
kFailsafeOobErrCode in ir_generator.cpp). Earlier compilers
used the generic 99 fall-through code, which was indistinguishable
from a generic unhandled main-return.
You can branch on the cause in your failsafe handler:
func:failsafe = int32(tbb32:err) {
if (err == 45i32) {
// OOB-specific recovery / logging
exit 45;
}
exit 99;
};
If your failsafe does not branch on err, its existing behaviour
is preserved — the bounds-check still dispatches into it; only the
err value is more specific. The post-failsafe fallback exit code is
also 45 on the OOB path (it would only be reached if the failsafe
itself returns rather than calling exit).
The sentinel value is element-type-agnostic (fires the same for
int32/int64/struct arrays) and fires for both over- and
under-bound indices. See fixtures bug565–bug572 in the
compiler repo for the full lock.
ARIA-065 — pointer arithmetic without unsafe.npk (v0.32.5)
@ptr_add<T>(ptr, offset) and @ptr_sub<T>(ptr, offset) are compiler
intrinsics behind a narrow stdlib gate. Without a wildcard import of
unsafe.npk, the call is rejected before IR generation:
func:bad = int32(wild int32->:p) {
int32->:q = @ptr_add<int32>(p, 1i64); // [ARIA-065]
pass <-q;
};
Fix: import the gate in the module that performs raw pointer arithmetic:
use "unsafe.npk".*;
The gate enables only pointer arithmetic. It does not weaken borrow checking, region checking, bounds checking, or other diagnostics.
ARIA-066 — pointer arithmetic on stack or gc (v0.32.5)
Even with unsafe.npk, pointer arithmetic is allowed only on wild and
wildx pointers. Managed or frame-local pointers are rejected:
use "unsafe.npk".*;
func:bad_stack = int32() {
stack int32:v = 1i32;
stack int32->:p = @v;
int32->:q = @ptr_add<int32>(p, 0i64); // [ARIA-066 ARIA-PTRARITH-STACK]
pass <-q;
};
gc sources report ARIA-066 ARIA-PTRARITH-GC for the same reason: moving a
collector-managed address by byte/element offset would bypass the GC's object
model and write barriers.
Fix: keep pointer arithmetic inside explicit wild/wildx buffers or move
the arithmetic into a C shim whose ownership contract is documented at the FFI
boundary.
ARIA-067 — address-of NULL (v0.32.5)
@NULL is rejected because NULL has no storage location:
int32->:p = @NULL; // [ARIA-067 ARIA-NULL-ADDRESS]
Fix: take the address of a typed binding, or initialize a pointer variable
to NULL directly if you need the null sentinel.
Runtime NULL-dereference sentinel (v0.32.5+)
Dereferencing NULL through <- p, *p, or p->field dispatches the user's
failsafe(tbb32:err) with sentinel value 46
(kFailsafeNullDerefErrCode in ir_generator.cpp), then exits 46 if the
failsafe returns.
func:failsafe = int32(tbb32:err) {
if (err == 46i32) {
exit 46;
}
exit 99;
};
Sentinel 45 remains reserved for runtime array-bounds failures. Sentinel 46 is reserved for NULL pointer dereference paths.
See also
borrow/diagnostics.md— non-region borrow codes (ARIA-022,ARIA-023,ARIA-026,ARIA-028, ...).stack.md,gc.md,wild.md— the region chapters with longer-form examples.pointers.md— pointer operators, region policy,unsafe.npk, and C ABI table.../handles/README.md— the handle cookbook (ARIA-032, runtime UAF behaviour).