Pointers and FFI ABI
This chapter is the v0.32.x pointer-surface closeout. It gathers the
operators audited across the cycle, the region rules that decide when a
pointer may be used, and the C ABI shapes to use at extern boundaries.
Pointers are deliberately boring at the backend level: LLVM 20 uses opaque
ptr, and Nitpick keeps the safety policy in the parser, type checker,
borrow checker, and region metadata rather than inventing a larger family of
runtime pointer values.
Operator quick reference
| Surface | Meaning | Notes |
|---|---|---|
T-> |
pointer to T in type position |
Example: int32->:p |
. |
field access on a struct value | No implicit pointer dereference |
p->field |
pointer-member access | Equivalent to deref + field projection |
<- p |
whole-value dereference | Copies primitives; follows the normal value semantics for aggregate values |
@x |
address of a storage-backed binding | Illegal on NULL because NULL has no storage location |
#x |
pin/address-stability operator | Region-specific; see pinning |
@ptr_add<T>(p, n) |
pointer arithmetic | Requires use "unsafe.npk".*; and a wild/wildx pointer |
@ptr_sub<T>(p, n) |
pointer subtraction | Same gate and region rule as @ptr_add |
The older types/pointer chapter remains the compact syntax reference. This memory chapter is the policy/ABI reference.
Region policy
Pointer types are not region-qualified by themselves. The source binding or parameter carries the region metadata:
stack int32:s = 1i32;
gc int32:g = 2i32;
wild int32:w = 3i32;
int32->:sp = @s; // pointer value whose source binding is stack
int32->:gp = @g; // source binding is gc
int32->:wp = @w; // source binding is wild
That distinction matters for unsafe operations:
stackand default locals live only for the current frame.gcobjects are managed by the collector and safepoints.wildandwildxare raw arena/manual-memory regions intended for C-style interop and runtime code generation.
The compiler stores this region metadata on symbols, not in the pointer type.
That keeps int32-> as the surface type while still letting diagnostics say
"this pointer came from a stack binding" or "this pointer came from a GC
binding".
Address-of and NULL
@x requires x to be a storage-backed binding. NULL is a sentinel value,
not an object, so @NULL is rejected statically:
int32->:bad = @NULL; // ARIA-067 ARIA-NULL-ADDRESS
Dereferencing a null pointer is a runtime failure path, not undefined
behaviour. As of v0.32.5, generated dereference code checks for NULL and
uses failsafe sentinel 46:
func:failsafe = int32(tbb32:err) {
if (err == 46i32) { exit 46; }
exit 99;
};
func:main = int32() {
int32:v = <-NULL; // dispatches failsafe(46), then exits 46 if it returns
exit v;
};
The same sentinel covers pointer-member access such as p->field when p is
NULL. Bounds-check failures use sentinel 45, so recovery code can
distinguish out-of-bounds from null dereference.
Pointer arithmetic gate
Raw pointer arithmetic is intentionally opt-in and region-limited. A module
must wildcard-import stdlib/unsafe.npk:
use "unsafe.npk".*;
func:at_same = int32(wild int32->:p) {
int32->:q = @ptr_add<int32>(p, 0i64);
pass <-q;
};
The gate enables only these two compiler intrinsics:
@ptr_add<T>(ptr, offset)@ptr_sub<T>(ptr, offset)
It does not disable borrow checking, region checking, bounds checking, or any other safety diagnostic.
Region rules:
| Source region | Pointer arithmetic |
|---|---|
wild |
allowed behind use "unsafe.npk".*; |
wildx |
allowed behind use "unsafe.npk".*; |
stack / default local |
rejected: ARIA-066 ARIA-PTRARITH-STACK |
gc |
rejected: ARIA-066 ARIA-PTRARITH-GC |
| no gate | rejected: ARIA-065 ARIA-PTRARITH-NOIMPORT |
The public wrappers in unsafe.npk are unsafe_ptr_add::<T> and
unsafe_ptr_sub::<T>. They exist for code that wants a named API, but the same
region policy still applies.
C ABI table
At an extern boundary, pointer-shaped Nitpick values lower to machine
pointers on x86-64 / LLVM opaque-pointer backends. Prefer explicit shim
wrappers whenever the C API has ownership, callback, or out-parameter rules
that the Nitpick type checker cannot see.
| Nitpick surface | C-side shape | Guidance |
|---|---|---|
int32-> |
int32_t * |
Plain pointer to scalar storage. |
T-> where T is a struct |
struct T * or ABI-equivalent C struct pointer |
Keep the C struct layout in sync with the Nitpick field order and widths. |
?* / ?-> |
void * / opaque pointer |
Best for malloc, handles, context pointers, and erased buffers. |
wild ?* return |
void * allocated by C |
Pair with a matching free/destroy function and keep ownership explicit. |
T->-> |
T ** |
Use only for already-validated source shapes; nested-pointer parser polish remains a carry-forward item. For public shims, prefer an opaque ?*/int64 handle until that audit closes. |
Nitpick function value (Ret)(Args...) |
closure pair {fn_ptr, env_ptr} internally |
Not a raw C callback pointer. Use a C shim/trampoline or pass an opaque handle. |
| C callback pointer | Ret (*)(Args...) |
Model as int64/?* at the Nitpick boundary and call it from C-side shim code. |
Opaque handle pattern
For C libraries with contexts, resources, or double pointers, the most stable ABI is still an opaque integral or erased pointer handle:
extern func:create_context = int64();
extern func:destroy_context = void(int64:ctx);
extern func:read_context = int32(int64:ctx, wild ?*:out_buf, int64:len);
The compiler cannot prove C-side lifetime or aliasing through that handle, so pair it with narrow wrapper functions and document ownership at the shim boundary.
String and large-value reminders
stringparameters lower asconst char *;stringreturns must use the runtimeNitpickString { char *data; int64_t length; }shape.- Large integer/aggregate values may use
sret/byvalABI lowering. If the C side is not generated from the same header, write a small shim with plain pointer parameters. flt32currently travels through the C ABI asdouble; write shims withdoubleparameters forflt32Nitpick declarations.
K semantics note
The K model for v0.32.x keeps pointer arithmetic intentionally small:
- raw
PTR(n)values move by integer offset; - store-backed local/pinned pointers preserve the exact location for offset
0; - nonzero arithmetic on store-backed locations is not modeled because the K core has no contiguous-object layout.
That is enough to lock value flow for the supported runtime tests without pretending the formal model understands arbitrary object layout.
Related chapters
- Wild —
wild/wildxallocation and deallocation rules. - Pinning — address stability and FFI-safe pinned hosts.
- Interop — GC ↔
wildpartition and shadow-stack escape hatch. - Diagnostics — ARIA-065/066/067 and failsafe sentinels.
- Extern — basic C FFI syntax.
- Advanced FFI — string, LBIM, handle, and buffering gotchas.