Reussir Language Reference

Array Operations

Array Operations

Reussir arrays combine a functional value interface with dense, contiguous storage. Their shape is part of the type, so the compiler knows every extent while lowering loops. An array value is one reference-counted heap object; its payload contains all elements contiguously, rather than a tree of separately allocated rows. This layout lets a uniquely owned chain of functional updates be optimized as in-place mutation.

Array types and shapes

Write the element type before a semicolon and one extent for each dimension:

[f64; 8] // eight floating-point values
[i32; 4, 4] // four rows by four columns
[u8; 2, 3, 4] // a rank-three array
[f64; 8] // eight floating-point values
[i32; 4, 4] // four rows by four columns
[u8; 2, 3, 4] // a rank-three array

Extents are positive integer literals, and their product may be at most . The current implementation accepts plain scalar elements: booleans, characters, integers, and supported floating-point types. Records, strings, nullable values, cells, closures, and nested arrays are not yet valid elements.

The values are stored in row-major order, so the final index varies fastest. For shape [T; D0, D1, D2][T; D0, D1, D2], the element at (i0, i1, i2)(i0, i1, i2) has linear offset (i0 * D1 + i1) * D2 + i2(i0 * D1 + i1) * D2 + i2. There are no per-row pointers or dynamic shape fields in the payload; the dimensions come from the static type.

Passing an array moves or retains its single managed reference, rather than copying its payload. A payload copy is needed only when an update must preserve another live version. The array’s shape and element type never change.

Import the intrinsic family before using its short name:

import core::intrinsic::array;
import core::intrinsic::array;

The full path, such as core::intrinsic::array::getcore::intrinsic::array::get, remains available. An import alias such as import core::intrinsic::array as arr;import core::intrinsic::array as arr; works as well.

Constructing arrays

Fill every element with one value

splatsplat needs the complete result type explicitly:

fn zeros() -> [f64; 8] {
array::splat<[f64; 8]>(0.0)
}
fn identity_seed() -> [i64; 4, 4] {
array::splat<[i64; 4, 4]>(0)
}
fn zeros() -> [f64; 8] {
array::splat<[f64; 8]>(0.0)
}
fn identity_seed() -> [i64; 4, 4] {
array::splat<[i64; 4, 4]>(0)
}

The supplied value is placed at every index.

Compute values from their indices

tabulatetabulate calls a closure once per element in row-major order. The closure receives one i64i64 index for every dimension:

fn iota() -> [f64; 8] {
array::tabulate<[f64; 8]>(|i| i as f64)
}
fn multiplication_table() -> [i64; 4, 4] {
array::tabulate<[i64; 4, 4]>(|row, column|
(row + 1) * (column + 1))
}
fn iota() -> [f64; 8] {
array::tabulate<[f64; 8]>(|i| i as f64)
}
fn multiplication_table() -> [i64; 4, 4] {
array::tabulate<[i64; 4, 4]>(|row, column|
(row + 1) * (column + 1))
}

For a rank-two type, a one-argument closure is a type error: the rank fixes the kernel’s arity.

Reading and updating

getget takes one index per dimension:

fn center(matrix: [f64; 3, 3]) -> f64 {
array::get(matrix, 1, 1)
}
fn center(matrix: [f64; 3, 3]) -> f64 {
array::get(matrix, 1, 1)
}

setset takes the same indices followed by a replacement value and returns the updated array:

fn set_center(matrix: [f64; 3, 3], value: f64) -> [f64; 3, 3] {
array::set(matrix, 1, 1, value)
}
fn set_center(matrix: [f64; 3, 3], value: f64) -> [f64; 3, 3] {
array::set(matrix, 1, 1, value)
}

Both operations check every dynamic index. An out-of-range or negative index panics with array index out of boundsarray index out of bounds rather than reading outside the allocation. Constant in-range indices allow the optimizer to remove the checks. splatsplat, tabulatetabulate, and foldfold generate loops whose limits are the static extents, so their own traversal does not require dynamic bounds checks.

getget borrows the array storage for the load. setset consumes one owned array reference, requests a unique mutable view, writes through that view, and returns the updated array. If the consumed reference is unique, the view names the existing payload. If another live reference exists, the uniqueness operation copies the entire payload first, leaving the other version unchanged.

Reducing in row-major order

foldfold visits every element and threads an accumulator through the traversal:

fn sum(matrix: [f64; 4, 4]) -> f64 {
array::fold(matrix, 0.0, |total, value| total + value)
}
fn sum(matrix: [f64; 4, 4]) -> f64 {
array::fold(matrix, 0.0, |total, value| total + value)
}

The kernel type is (Accumulator, Element) -> Accumulator(Accumulator, Element) -> Accumulator. Traversal is row-major for every rank. A named closure may be passed in place of a lambda:

fn reduce(
combine: (f64, f64) -> f64,
values: [f64; 8],
) -> f64 {
array::fold(values, 0.0, combine)
}
fn reduce(
combine: (f64, f64) -> f64,
values: [f64; 8],
) -> f64 {
array::fold(values, 0.0, combine)
}

foldfold borrows its array, consumes the initial accumulator, and threads the owned accumulator returned by the kernel into the next iteration. The accumulator type is inferred independently of the element type; for example, an [i32; 64, 64][i32; 64, 64] may be folded into an i64i64. The kernel closure is consumed like an ordinary function argument.

Linear use unlocks in-place updates

Array updates have persistent semantics. Whether they allocate a new buffer is determined by ownership at runtime and then simplified by compiler analysis. Threading a single version through the computation is the fast path:

fn three_marks() -> [i64; 8] {
let values = array::splat<[i64; 8]>(0);
let values = array::set(values, 1, 10);
let values = array::set(values, 3, 20);
array::set(values, 5, 30)
}
fn three_marks() -> [i64; 8] {
let values = array::splat<[i64; 8]>(0);
let values = array::set(values, 1, 10);
let values = array::set(values, 3, 20);
array::set(values, 5, 30)
}

Every old binding is dead after its setset, and splatsplat produced a fresh array, so the same allocation can be updated three times. This is a linear use pattern, not a restriction on the type and not a special linear array type.

When an old version remains live, Reussir preserves it with copy-on-write:

fn old_plus_new() -> i64 {
let original = array::splat<[i64; 4]>(1);
let changed = array::set(original, 0, 99);
array::get(original, 0) + array::get(changed, 0)
}
fn old_plus_new() -> i64 {
let original = array::splat<[i64; 4]>(1);
let changed = array::set(original, 0, 99);
array::get(original, 0) + array::get(changed, 0)
}

The result is 100100: original[0]original[0] remains 11, while changed[0]changed[0] is 9999. The second array therefore needs distinct storage. Keep multiple versions when the algorithm needs persistence; otherwise, pass the returned array directly to the next update.

A complete transformation

Ordinary tail recursion can express an element-wise update without a special map intrinsic:

import core::intrinsic::array;
fn add_one_from(
values: [f32; 1024],
index: i64,
) -> [f32; 1024] {
if index == 1024 {
values
} else {
let value = array::get(values, index);
add_one_from(
array::set(values, index, value + 1.0),
index + 1,
)
}
}
fn add_one(values: [f32; 1024]) -> [f32; 1024] {
add_one_from(values, 0)
}
import core::intrinsic::array;
fn add_one_from(
values: [f32; 1024],
index: i64,
) -> [f32; 1024] {
if index == 1024 {
values
} else {
let value = array::get(values, index);
add_one_from(
array::set(values, index, value + 1.0),
index + 1,
)
}
}
fn add_one(values: [f32; 1024]) -> [f32; 1024] {
add_one_from(values, 0)
}

The recursive call is in tail position, and the updated array is passed directly into it: after add_one_from(...)add_one_from(...) returns, the caller has no pending addition, constructor, or other work to perform.

Reussir’s uniqueness-carrying analysis examines that recursion before it is turned into a loop. array::setarray::set always returns one uniquely owned array: either it reuses a unique input or it copies a shared input once. The recursive call therefore receives a unique array even when the original function argument was shared. The compiler creates a specialized recursive path that carries this fact through the remaining calls and removes their repeated uniqueness checks.

At -Oaggressive-Oaggressive, later optimization converts the tail recursion into a unit-stride loop. The bounds checks become provably true for indices 0..10230..1023, and LLVM sees a conventional contiguous load/add/store loop. This exact pattern is covered by Reussir’s vectorization tests; the chosen vector width and whether vectorization is profitable remain target-dependent.

Intrinsic summary

Intrinsic Signature Behavior
splatsplat splat<[T; D...]>(T) → [T; D...]splat<[T; D...]>(T) → [T; D...] Construct an array by repeating one value.
tabulatetabulate tabulate<[T; D...]>((i64, ...) → T) → [T; D...]tabulate<[T; D...]>((i64, ...) → T) → [T; D...] Construct an array by calling kernelkernel with one index per dimension.
getget get([T; D...], i64, ...) → Tget([T; D...], i64, ...) → T Bounds-check and read one element through a borrowed view.
setset set([T; D...], i64, ..., T) → [T; D...]set([T; D...], i64, ..., T) → [T; D...] Bounds-check and return an updated array, mutating in place when unique.
foldfold fold([T; D...], A, (A, T) → A) → Afold([T; D...], A, (A, T) → A) → A Reduce all elements in row-major order.

Here D...D... denotes the nonempty extent list and one i64i64 argument per extent; it is notation, not literal Reussir syntax. Only splatsplat and tabulatetabulate take an explicit array type argument. getget, setset, and foldfold infer the shape from their first argument and reject explicit type arguments.