← Back to AILP Home

Wild

wild opts out of the borrow checker. The value lives on the unmanaged heap (npk_alloc), and the user is responsible for pairing the allocation with the matching release.

func:scratch = int32() {
    wild int32:buf = 0i32;
    defer free(buf);            // ARIA-014 requires this
    // ... use buf via FFI ...
    pass 0i32;
};

wild exists for FFI buffers and interop with C-style APIs. Do not reach for it as an escape hatch from the borrow checker — that is what gc is for.

What "exempt from the borrow checker" means

The exemption is narrower than it sounds: it covers raw pointer arithmetic and FFI access, not in-Nitpick use of the binding. As of v0.27.6, taking an explicit $$i / $$m borrow of a wild binding goes through the same checker as a stack or gc binding, because v0.27.1's region propagation gives the wild slot a real region tag and v0.27.2's borrow inheritance threads it through $$i / $$m declarations:

wild int8->:buf = alloc(64);
{
    $$i int8->:b = buf;        // OK — Nitpick-visible immutable borrow.
}                              // borrow drops at end of inner scope.
free(buf);                     // OK — no live borrow.

The checker still rejects two overlapping mutable borrows on the same wild buffer (ARIA-023, bug250), and still rejects free while a borrow is alive (ARIA-019). FFI-style passthrough — handing the raw int8@ to an Nitpick function that takes $$i/$$m and releasing after the call returns — works end-to-end (bug251):

func:fill = int32($$m int8->:p) { pass 0; };

wild int8->:buf = alloc(128);
fixed int32:rc = raw fill(buf);  // borrow lives only across the call.
free(buf);                       // OK.

What wild does opt out of is checking on raw pointer reads / writes through the unmanaged pointer (buf[i] = x;, <-some_ptr_field). Inside FFI you are running C-side rules; the language trusts you, and therefore so does the optimiser. As of v0.32.5, pointer arithmetic itself is still gated: import use "unsafe.npk".*; and use @ptr_add<T> / @ptr_sub<T> only on wild or wildx pointers. See pointers.md for the full ARIA-065/066 policy.

The allocation tracking lives in borrow_checker.cpp:1875 and the borrow-region inheritance that makes the above work lives in borrow_checker.cpp:1671.

Diagnostics that still apply

Even though aliasing is unchecked, two leak/use-after-free guards remain:

See diagnostics.md for examples and fix recipes.

Since v0.29.3, importing stdlib/drop.npk opts the file into RAII: wild T:x = T{ ... } struct bindings auto-emit npk_free(x) at scope end, and ARIA-014 no longer fires on them. The full opt-in surface and the per-region recognizer rules are in the guide/drop/ cookbook.

wildx — manual heap, executable (W^X)

wildx is wild with PROT_EXEC (write-XOR-execute discipline). It exists to support runtime code generation: allocate, write machine bytes with the W mapping, mprotect to PROT_EXEC, then call as a function pointer.

Surface lifecycle (v0.27.5+)

The W^X state machine is exposed through three pointer-keyed builtins:

wildx int8->:page = wildx_alloc(64);   // UNINITIALIZED -> WRITABLE
// ... write machine bytes into `page` ...
fixed int32:rc1 = wildx_seal(page);    // WRITABLE -> EXECUTABLE
// ... call into the page (e.g. via an extern jit_call helper) ...
fixed int32:rc2 = wildx_free(page);    // EXECUTABLE -> FREED (munmap)

Each pointer is registered in a process-wide std::unordered_map<void*, WildXGuard> keyed by the writable address. The guards own the FNV-1a code hash, ASLR jitter base, and quota accounting; surface code never sees the struct directly.

The runtime enforces:

The borrow checker registers wildx_free as a deallocator, so the same wild-leak (ARIA-014) and double-free (ARIA-022) diagnostics that protect plain wild apply to the new builtins (regressions: bug247 double-free, bug248 leak). Compile-time rejection is preferred over runtime rejection; the runtime rc=-1 path exists as a defense-in-depth backstop for code that smuggles pointers through function boundaries the borrow checker cannot see across.

Legacy form

The pre-v0.27.5 shape wildx int8->:b = alloc(N); free(b); still parses and compiles, but it routes through plain npk_alloc (no W^X mapping). New code should prefer the wildx_alloc / wildx_seal / wildx_free triple to actually reach npk_alloc_exec.

Going further: JIT cookbook

For the full story on producing, sealing, calling, and tearing down executable pages — plus the stdlib/jit.npk helper (Type:Jit.compile_add_i32 / call_i32_i32 / free), the safety model around JIT'd bytes, and the FAQ on patch-in-place / ARM64 / ASLR — see the JIT cookbook. This page covers wildx as an allocator; the JIT cookbook covers wildx as a code-generation surface.

Interop with gc

The two heaps are disjoint: the GC marker never follows a wild pointer (verified end-to-end by bug230 against the runtime accessor npk_gc_is_heap_pointer_i32), and a wild slot does not root anything the GC can see (bug231 confirms a gc binding survives 5000 wild alloc/free cycles around explicit safepoints).

If you genuinely need a wild slot to root a gc reference, the escape hatch is npk_shadow_stack_add_root. Full discussion in interop.md.

ABI gotcha

When exposing C functions that return a region predicate, never use a 1-byte return type (_Bool, uint8_t) and read it as int32 from Nitpick. The x86-64 SysV ABI leaves the upper EAX bits undefined for sub-word returns, and the Nitpick extern call will see garbage. Always widen at the C side — see the v0.26.5 fix that introduced npk_gc_is_heap_pointer_i32 (full int32_t return) alongside the legacy _Bool-returning variant.

Validation