← Back to AILP Home

Closures & Lambdas

Function Pointer Variables

Lambda expressions are assigned to typed variables using function pointer syntax:

(int32)(int32):identity = int32(int32:x) { pass x; };

(int32)(int32, int32):add = int32(int32:a, int32:b) {
    int32:sum = a + b;
    pass sum;
};

The variable type is (ReturnType)(ParamTypes) — return type first, then parameter types.

Passing as Arguments

Higher-order functions accept function pointers as parameters:

func:apply = int32((int32)(int32):f, int32:x) {
    int32:result = f(x) ? -1i32;
    pass result;
};

(int32)(int32):dbl = int32(int32:x) { int32:v = x + x; pass v; };
int32:r = apply(dbl, 21i32) ? -1i32;

Note: Lambdas must be assigned to a named variable first, then passed by name. There are no anonymous inline lambdas at call sites.

Capturing Outer Variables (v0.31.14.x+)

Lambdas capture outer-scope locals automatically. Module-level fixed constants and use-imported names stay direct references (no env slot). Wild / wildx / coro-frame captures are deferred to v0.32.x.

func:main = int32() {
    int32:seed   = 42i32;
    string:label = "ok";
    int64:handle = 0xCAFEBABEi64;

    (int32)():report = int32() {
        // `seed`, `label`, and `handle` are all captured.
        // They live in an env struct on the GC heap; the
        // lambda value is a fat pointer `{fn_ptr, env_ptr}`.
        if (label != "ok")          { fail 2i32; }
        if (handle != 0xCAFEBABEi64){ fail 3i32; }
        pass seed;
    };

    int32:r = report() ? -1i32;
    exit r;   // 42
};

Capture-mode rule (CLOSURE-DEC-003)

For each captured outer variable the analyzer picks ONE mode:

  1. BY_REF — the lambda body mutates the variable, or takes its address with @.
  2. BY_VALUE — primitive scalar, or a small struct whose getTypeAllocSize() ≤ 16 bytes, and the type has no Drop, and the type has no internal pointers. string (NpkString fat pointer, exactly 16 bytes) and int64 handles qualify.
  3. BY_BORROW — otherwise (large struct, Drop-bearing type, internal-pointer-bearing type). The captured borrow extends the source $i window across every call site of the lambda.

Drop-bearing types are never BY_VALUE — that would silently clone the underlying resource.

ARIA-060 — INVALID_CLOSURE_CAPTURE

A capture is rejected at compile time when it is both mutated and has its address taken inside the lambda body (CLOSURE-DEC-009b, the $m-alias-live case):

func:main = int32() {
    int32:n = 0i32;
    (int32)():f = int32() {
        n = n + 1i32;     // mutation
        int32:p = @n;     // address taken — ARIA-060
        pass p;
    };
    int32:r = f() ? -1i32;
    exit r;
};
error[ARIA-060]: INVALID_CLOSURE_CAPTURE: capture of 'n' is both
                 mutated and address-taken inside the lambda body

Fixes:

Stack-escape captures (a lambda holding a $$i/$$m borrow escapes its enclosing frame) keep being caught by the existing return-borrow checker — they re-use the same ARIA-060 identity but the message is emitted by the borrow checker, not the closure analyzer.

What is not yet supported

Related