← Back to AILP Home

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:

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:

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


K semantics note

The K model for v0.32.x keeps pointer arithmetic intentionally small:

That is enough to lock value flow for the supported runtime tests without pretending the formal model understands arbitrary object layout.


Related chapters