Skip to content

Closures and function values

Functions and closures are typed values in Z. Arrow closures use a familiar TypeScript-shaped syntax:

const offset = 2;
const add = (value: i32): i32 => value + offset;

A block-bodied closure uses return to complete that closure invocation:

let total = 0;
const accumulate = (value: i32): i32 => {
total = total + value;
return total;
};

Here return targets accumulate, not the function in which the closure was created. select remains reserved for local value-producing control-flow blocks inside that closure.

Parameter names make signatures readable but do not create a structural object type:

type Operation = (left: i32, right: i32) => i32;
function apply(
in operation: Operation,
left: i32,
right: i32
): i32 {
return operation(left, right);
}

A compatible named function or closure can initialize Operation:

function add(left: i32, right: i32): i32 {
return left + right;
}
const direct: Operation = add;
const doubled: (value: i32) => i32 = (value) => value * 2;

Parameter capabilities are part of the function type. A callback accepting inout value: T is a different contract from one accepting in value: T.

An ordinary closure borrows captured local state. It remains valid only where the destination proves the capture cannot outlive that state:

const item = store.at(0);
const observed = inspect(() => item.value);
store.append(next); // valid after inspect and the borrow end

This gives synchronous higher-order APIs concise call sites while preserving the same escape and invalidation checks as explicit views.

Borrowing state is not taking a snapshot:

let total = 0;
const add = (value: i32): void => {
total = total + value;
};
add(20);
add(22); // total is 42
const read = (): i32 => total;
total = 100;
const observed = read(); // 100, not the value when read was created

These synchronous block and scalar-reading expression forms execute through both compiler paths with stack environments and no heap allocation. Expression closures may also mutate borrowed state by calling an inout helper. Borrowed String, Array, cleanup-bearing value structs, and ARC handles stay owned by their original scope. For example:

const source = "first second";
let text = source.copyBytes(0, 5);
const length = (): usize => text.byteLength;
text = source.copyBytes(6, 12);
const observed = length(); // 6; no captured String snapshot or heap environment

move gives the callback its own captured value instead; mutations to that owned state persist across calls and do not reassign the parent’s Copy scalar. This applies to expression bodies as well as block bodies. Synchronous block callbacks can return owned values and declare their own throws E, independently of the enclosing function:

const describe = (count: i32): String => {
if (count == 0) return "No notes";
return `Notes: ${count}`;
};
const title = describe(42);

Each invocation cleans up its own locals and by-value parameters, except ownership transferred out of those bindings. Replacing an owner through a mutable borrowed capture releases its former value; returning from the callback does not destroy the captured binding. Borrowed capture consumption and escape still reject. Async borrowed environments and other remaining storage boundaries are tracked in implementation status.

Both compilers inspect the receiving Z helper, not just the in spelling:

function inspect(in operation: () => i32): i32 {
return operation();
}

Direct invocation and forwarding through proven synchronous named helpers are accepted. A helper that returns, stores, aliases, or captures its callback cannot receive an inline stack-backed environment. When retention is intended, pass an owned closure:

function preserve(in operation: () => i32): () => i32 {
return operation;
}
const count = 42;
const saved = preserve(move (): i32 => count);

The helper borrows the owned callable and returns a retained copy. Recursive, dynamic, raw, or deferred forwarding is not inferred nonescaping in this tier. This is proof about a known implementation, not a structural function-type property or a runtime escape check. Received Objective-C blocks with ARC-backed adapter environments remain retainable under their native ownership contract.

Use a move closure when the callable must escape its defining scope:

function makeCounter(start: i32): () => i32 {
let current = start;
const counter: () => i32 = move () => {
current = current + 1;
return current;
};
return counter;
}

move describes an owned capture environment. Capturing a unique owned value transfers it, capturing an ARC identity retains shared identity, and capturing a cleanup-free copy value stores its ordinary value snapshot.

Owned closure environments use ARC. Copying a closure value shares the same environment and therefore the same captured mutable state:

const counter = makeCounter(40);
const sameCounter = counter;
const first = counter(); // 41
const second = sameCounter(); // 42

This is shared closure identity, not two independent deep clones.

Error effects remain part of the callable type:

struct ResponseError {
status: i32;
}
type ResponseValidator = (status: i32) => i32 throws ResponseError;
function validate(
in operation: ResponseValidator,
status: i32
): i32 throws ResponseError {
return try operation(status);
}

A contextually typed closure may use throw without restating the effect on the literal. Calling the stored function still requires try or attempt; storage does not erase failure.

Synchronous and asynchronous callable types are distinct even when their ordinary parameters and results match. Executor contracts such as on thread.main and on thread.any are also part of callable identity; they constrain where construction, capture, storage, and invocation are legal rather than scheduling work implicitly.

An ARC class stored strongly by its own closure would form a cycle. Capture a Weak<T> when the closure should not keep that identity alive:

const owner = weak controller;
const handler: () => i32 = move (): i32 => {
const upgraded = attempt owner.upgrade();
return match (upgraded) {
success(current) => current.id;
failure(expired) => -1;
};
};

upgrade() produces a new strong owner or throws WeakExpired. Automatic ORC cycle collection remains future work, so deliberate weak edges matter today.

Runnable examples: function values, owned closures, and throwing callables.

Next: Generics and traits.