← Back to AILP Home

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:

  1. Sees the binding pattern (RHS shape matters — the recognizer peels gc / pin wrappers and prefix-matches the allocator call name).
  2. Pushes a DropEntry{kind, varName, alloca, typeName} onto the current scope's drop stack.
  3. 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;
};

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;
};

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;
};

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;
};

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 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