Closures

Stack- and heap-allocated capturing lambdas, escape rules, and loop-safe callback patterns.

Stack vs heap

Non-escaping capturing closures store their environment in a stack alloca at the creation site. The closure body receives ptr %env as its first argument.

When a capturing closure is coerced to a plain fn(...) for a synchronous callback (e.g. iter_filter predicate), the compiler emits a per-site wrapper that loads the stack env from an invoke slot before each call.

As of v2.2, closures that escape — returned from a function, passed as a fn(...) argument, or stored beyond the synchronous invoke window — promote their env to the heap (malloc + memcpy). The wrapper loads the env from a persistent global; heap env is freed when the owning let binding goes out of scope.

Escape rules

Direct call vs fn pointer

fn make_adder(n){
    return (x) => x + n          // heap — escapes via return
}
let add5 = make_adder(5)
print(add5(10))                 // 15

iter_filter(v, (x) => x > 0)    // stack + invoke slot (sync)

Loop-safe pattern

import "stdlib/iter/mod.ny"

fn main() {
    let v = Vec_i32_from_range(1, 6)
    let mut i = 0
    let threshold = 2
    while i < 1 {
        let pred = (x) => if x > threshold { 1 } else { 0 }
        let out = iter_filter(v, pred)
        print(Vec_i32_len(out))
        i = i + 1
    }
}

iter_filter calls pred synchronously before returning, so the stack env remains valid for the invoke window.

Ownership