Regions
Drop covers five resource regions as of v0.31.5.x. Each region
has its own per-binding-shape recognizer in the IR generator;
each flips on the same use "drop.npk".*; import. This page
walks the per-region behaviour and the RAII-vs-explicit
decision.
The five supported binding shapes
| Region | Binding shape | Auto-emit | Slice |
|---|---|---|---|
wild struct |
wild T:x = T{ ... }; |
npk_free(x) |
v0.29.3 |
wildx |
wildx T->:p = wildx_alloc(N); |
npk_wildx_free(p) |
v0.29.4 |
HandleArena |
int64:a = HandleArena.create(); |
npk_handle_arena_destroy(a) |
v0.29.5 |
JitFn |
wildx int8->:f = Jit.compile_add_i32(); |
npk_wildx_free(f) |
v0.29.6 |
Handle<T> |
Handle<T>:h = HandleArena.alloc(a, N); |
npk_handle_free(h) |
v0.31.5 |
In every case the compiler:
- Sees the binding pattern (RHS shape matters — the recognizer
peels
gc/pinwrappers and prefix-matches the allocator call name). - Pushes a
DropEntry{kind, varName, alloca, typeName}onto the current scope's drop stack. - At every exit path for that scope, walks the stack in reverse declaration order and emits the matching free.
Borrow-checker integration (where applicable) mirrors the
codegen recognizer so ARIA-014 (leak) does not fire on
RAII-managed bindings.
wild T:x = T{ ... }; (v0.29.3)
use "drop.npk".*;
struct:Holder = { value: int64, };
func:demo = NIL() {
wild Holder:h = Holder{ value: 42i64 };
// h is auto-freed at scope end via npk_free(h).
pass;
};
- The recognizer keys on
wild T:x = T{...}where T is a struct (not a pointer-typedwild). wild T->:p = alloc(...)(raw pointer-typed wild) is not on the surface this cycle — that's the deferred v0.29.3b slice. Manualnpk_free(p)is still required there.
wildx T->:p = wildx_alloc(N); (v0.29.4)
use "drop.npk".*;
func:demo = NIL() {
wildx int8->:page = wildx_alloc(64i64);
// Write bytes, optionally seal, then return.
// Auto-emits npk_wildx_free(page) at scope end.
pass;
};
- Works whether or not
wildx_sealis called. The runtime state-machine accepts a free on either WRITABLE or SEALED pages. - A binding initialised by
Jit.compile_*flows through the v0.29.6 JitFn recognizer instead (separate flag — see below).
int64:a = HandleArena.create(); (v0.29.5)
use "drop.npk".*;
use "handle.npk".*;
func:demo = NIL() {
int64:arena = HandleArena.create();
Handle<int64>:h = HandleArena.alloc(arena, 32i64);
// Auto-emits npk_handle_free(h) at scope end (v0.31.5.x).
// Auto-emits npk_handle_arena_destroy(arena) at scope end.
pass;
};
- Destroying the arena bumps every slot's generation, so
outstanding
Handle<T>values become stale automatically. Per-handle auto-free is therefore not strictly required for correctness, but is now emitted (v0.31.5.x) to give every binding a symmetric scope-end teardown and to enable earlier slot reuse when the arena outlives the binding. ARIA-032(handle outlives arena) continues to fire on escape attempts from a RAII-managed arena.
Handle<T>:h = HandleArena.alloc(a, N); (v0.31.5.x)
use "drop.npk".*;
use "handle.npk".*;
func:demo = NIL() {
int64:a = raw HandleArena.create();
Handle<int64>:h = raw HandleArena.alloc(a, 8i64);
// Auto-emits npk_handle_free(h) at scope end.
raw HandleArena.destroy(a);
pass;
};
- The recognizer matches the IRGen-mangled callee names
HandleArena_alloc/npk_handle_allocand peelsraw/dropwrappers on the initializer. pass hmoves the handle out of the originating frame; the pre-existingmoved_var_namefilter inemitDropsForScopeskips the auto-free, so cross-function ownership transfers work the same as forwild/wildxbindings (HANDLE-DEC-006).- Writing both an explicit
HandleArena.free(h)and letting RAII fire is a hard error in v0.31.6.4 (NODROP-DEC-013) — the borrow checker emits the unifiedARIA-022and the diagnostic hint recommendsnodropas the per-binding opt-out. (The v0.31.5.3ARIA-051warning has been retired and folded intoARIA-022now thatnodropprovides a precise opt-out.) To keep a single binding on the explicit-free contract without removingdrop.npkfrom the module, usenodropat the declaration site:Handle<T>:h = nodrop raw HandleArena.alloc(a, N);. - See
guide/handles/lifetimes.mdfor theHandle<T>outlives rule's interaction with RAII.
wildx int8->:f = Jit.compile_add_i32(); (v0.29.6)
use "drop.npk".*;
use "jit.npk".*;
func:demo = int32() {
wildx int8->:f = Jit.compile_add_i32();
int32:r = Jit.call_i32_i32(f, 2i32, 3i32);
// Auto-emits npk_wildx_free(f) at scope end.
pass r;
};
- The flag is independent from the generic
wildxflag (DROP-DEC-007 — a program may enable JIT auto-free without enabling blanketwildxRAII, even thoughdrop.npkcurrently flips both at once). - The recognizer prefix-matches
Jit_compile_*so future signatures (compile_mul_i32, …) inherit RAII without code changes.
The RAII-vs-explicit decision
| Situation | Choose |
|---|---|
| Function-local binding, single owner, simple lifetime | Drop |
Binding escapes via pass v to caller |
Drop + bare-identifier move (DROP-DEC-004) |
Binding stored in a gc struct, transferred by value |
explicit free at the new owner's end |
| Long-lived process resource (started once at boot) | explicit free in a teardown helper, or never |
| Conditional release based on runtime state | explicit free + don't import drop.npk for that binding's type |
| Need to free before scope end (e.g. release pressure mid-function) | explicit free — but be aware v0.29.x will then double-free at scope end (diagnostic coming in successor cycle) |
The honest rule: if you are not sure, start with Drop. As of
v0.31.6.x, mixing an explicit HandleArena.free(h) with the
auto-free is detected and rejected as a hard error
(ARIA-022 — NODROP-DEC-013, promoted from the v0.31.5.3
ARIA-051 warning); for wild bindings the parallel check
(ARIA-050) ships as an error since the underlying allocator
has no generation safety net. In both cases the preferred
opt-out is the new nodrop keyword at the declaration site,
which works uniformly across all five region shapes:
wild T:x = nodrop T{ /* ... */ };
wild T->:p = nodrop alloc(sizeof(T));
wildx int8->:p = nodrop wildx_alloc(N);
int64:a = nodrop HandleArena.create();
Handle<T>:h = nodrop raw HandleArena.alloc(a, N);
wildx int8->:f = nodrop Jit.compile_add_i32();
nodrop peels at the outermost wrapper position only
(NODROP-DEC-011); inner uses such as identity(nodrop f())
do not opt the binding out. See
pitfalls.md for the precise rule.
Borrow-checker visibility
Drops are emitted by the IR generator. The borrow checker has
its own parallel recognizers (mirroring the codegen gates) so
that bindings under RAII do not trip ARIA-014 for "missing
free". The two recognizers must agree — every slice that adds
a new region pattern to codegen also extends the borrow
checker.
ARIA-022 (double-free) and ARIA-015 (use-after-free) on
the underlying pointer continue to fire as before. Drop does
not weaken them.
Validation
bug_tests_v0293— wild struct RAII (bug285–bug288).bug_tests_v0294— wildx RAII (bug289–bug293).bug_tests_v0295— HandleArena RAII (bug294–bug296).bug_tests_v0296— JitFn RAII (bug297–bug300).- All four region recognizers exercised by
bug_tests_v0297early-exit fixtures (bug301–bug310).