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
- Return —
return (x) => x + nalways heap-promotes. - Sync callback — inline
iter_filter(v, (x) => …)or alet pred = …passed immediately in the same block uses stack + invoke slot. - Fn pointer param — stack closure variables passed to external
fn(...)params set the invoke slot at the call site; returned closures use heap globals.
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)fn make_adder(n: i32){
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
}
}import "stdlib/iter/mod.ny"
fn main() -> void {
let v = Vec_i32_from_range(1, 6)
let mut i: i32 = 0
let threshold: i32 = 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
- Move-captured owned values follow existing borrow-check rules (no active borrows when creating a closure).
- Captures of
Send/Synctypes only — references cannot be captured. - Heap-promoted env blocks are freed at scope end when held in a
letbinding withheap_ownedmetadata.