Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

In-place Updates

In-place updates do not produce observable side effects, but they provide a way to update an array efficiently — the cost is proportional to the size of the value(s) being written, not the size of the full array.

The a with [i] = v construct (and its derived forms) performs an in-place update. The compiler verifies that the original array a is not used on any execution path following the update, and that no alias of a is used either. Most language constructs produce fresh arrays with no aliases; slicing is the main exception — a slice aliases its source.

A function parameter may be marked consuming by prefixing its type with *. A return type may be marked alias-free the same way. For example:

def modify(a: *[]i32, i: i32, x: i32) *[]i32 =
  a with [i] = a[i] + x

* is a property of a signature only — it may appear on function parameter and return types, and nowhere else. It is not part of a value’s type: a let binding, a type ascription, or a plain (non- function) def may not carry *. Uniqueness is never inferred, so a * return does not make an unannotated parameter consuming — annotate the parameter to consume it. A function that declares a * return must actually produce an alias-free value: freshly allocated, or a parameter it consumed (a with update on a consumed array qualifies); returning an observing parameter or a global is an error.

Because a consuming function’s effect cannot be tracked when the function is used as a value, a consuming function may not be passed as a value — a parameter, lambda parameter, or argument whose type is a function mentioning * is rejected — and functions may not be returned from if or match.

A parameter that is not consuming is called observing. The * in a: *[]i32 means modify takes ownership of a; no caller may reference a (or any alias of it) after the call. This is what permits the with expression to update in place. After a call modify(a, i, x), neither a nor any alias of a may be used on any subsequent execution path.

If a * appears anywhere inside a tuple parameter type, the whole parameter is considered consuming:

def consumes_both(p: (*[]i32, []i32)) i32 = ???

This is usually not desirable. Prefer separate parameters:

def consumes_first_arg(a: *[]i32, b: []i32) i32 = ???

For bulk in-place updates with multiple values, use the scatter function from the prelude.

Alias Analysis

The rules used to determine aliasing are intuitive in the intra- procedural case. Aliases are associated with entire arrays. Aliases of a record or tuple are tracked for each element, not for the record or tuple itself. Most constructs produce fresh arrays with no aliases; the main exceptions are if, loop, function calls, and variable references.

After a binding let a = b, which simply assigns a new name to an existing variable, a aliases b. Similarly for record projections and pattern bindings.

The result of an if aliases the union of the aliases of its branches.

The result of a loop aliases the initial values as well as any aliases that the merge parameters may assume at the end of an iteration, computed to a fixed point.

The aliases of a value returned from a function depend on whether the return value is declared alias-free (with *). If it is, the value has no aliases. Otherwise, it aliases all arguments passed for non-consumed parameters.

Globals

A top-level constant is a shared, immutable value: it carries no ownership, so it is never consumable. A function body may observe a global freely — index it, pass it as an argument, fold over it — but cannot consume it, and a function that returns a global as a non-unique result exposes it only for observation. Consequently a global can never satisfy a consuming parameter or an alias-free (*) return.

def table: [4]i32 = [1, 2, 3, 4]

def ok(i: i32) i32 = table[i]              -- observe: fine
def get(i: i32) [4]i32 = table            -- return for observation: fine

def bad(i: i32) i32 = consume(table)      -- error: cannot consume a global
def worse(i: i32) *[4]i32 = table         -- error: not alias-free

In-place Updates and Higher-order Functions

Consumption interacts inflexibly with higher-order functions: the language cannot control how many times a function argument is applied, or to what, so it is not safe to pass a function that consumes its argument. Two conservative rules govern this interaction:

  • In the expression let p = e1 in …, if any in-place update takes place inside e1, the value bound by p must not be or contain a function.
  • A function that consumes one of its arguments may not be passed as a higher-order argument to another function.