Arrays
An array T[N] is a fixed number N of values of one type T, stored side by side. The count is part of the type, so int32[3] and int32[4] are different types, and an array never grows or shrinks. An array is its elements: it holds them inline, with no pointer, header or allocation, and it lives wherever its binding lives — in a local, inside a struct, inside another array.
array-type = type "[" constant-expression "]"
array-literal = "[" [ expression { "," expression } [ "," ] ] "]"
repeat-literal = "[" expression ";" constant-expression "]"
let primes: int32[4] = [2, 3, 5, 7];
let rgb: uint8[3] = [255, 128, 0];
let zeros: float64[8] = [0.0; 8];
The type
N must be a non-negative integer known at compile time: a literal or a constant. A run-time value is rejected:
const Limit: uint = 100;
var flags: bool[Limit] = [false; Limit];
error: array length must be a non-negative compile-time integer
A range where the length goes, as in int[0..4], gets the same error with the help line write 'T[..]' for a slice — T[..] is the slice type, a view of some number of elements. A sizeof of a type is a compile-time integer too, so uint8[sizeof(Header)] is an array type.
An array type always has a length. T[], with nothing between the brackets, is refused wherever it is written:
error: an array type needs a length between '[' and ']'
help: use 'Vector<T>' for a growable array or 'T[..]' for a view
The element type can be any type, including another array. Each [N] wraps everything written before it, so a nested array type reads inside out: int32[4][3] is three rows of four int32 values. Indexing reads outside in: grid[row] is an int32[4], and grid[row][column] is one int32.
let grid: int32[4][3] = [
[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]
];
Array literals
[a, b, c] lists the elements in order. Its type is T[N], where N is the number of elements and T comes from the context — an annotation, a parameter, a field — or, without one, from the first element:
let inferred = [2, 3, 5, 7]; // int[4]
let reals = [1.5, 2.5]; // float64[2]
let bytes: uint8[3] = [1, 2, 255]; // the context types every element
Every element must convert to one element type, and the count must match the type exactly:
error: array element 2 has type 'bool8', but element 1 established element type 'int'
error: cannot assign 'int[3]' to 'int32[4]'
Repeated literals
[value; count] builds an array of count copies of one value. The value is evaluated exactly once, and the result is copied into every element, so [Roll(); 4] calls Roll once and its four elements are always equal. count follows the same rule as N: a non-negative compile-time integer.
let zeros: int32[16] = [0; 16];
let rolls = [Roll(); 4]; // one call, four copies
var board: int32[3][3] = [[0; 3]; 3]; // nests: three copies of a row
error: array repeat count must be a non-negative compile-time integer
The element type must be copyable, which means plain data with nothing to destroy; move-only elements are listed, each handed over with move, or written in a loop. A count of zero still evaluates the value once.
A move into an array literal works with a type annotation or without one: with a move-only k, both let b: Key[1] = [move k]; and let b = [move k]; take k, and a later use of it is value 'k' is used after it was moved.
Value semantics
An array is a value, like a struct or a tuple. An array of copyable elements is copied when it is assigned, passed by value or returned — every element — and the two arrays share nothing afterwards. An array of move-only elements is move-only itself, and a named one is transferred with move:
var scores: int32[3] = [10, 20, 30];
var copy = scores;
copy[0] = 99; // scores is still [10, 20, 30]
The binding decides mutability for the whole array: through a let binding no element can change, while a var binding allows both element writes and whole-array assignment.
error: cannot modify immutable variable 'primes'
help: declare 'primes' with 'var' to make it mutable
To hand an array to a function without copying it, take a reference to it, &T[N] or &var T[N], or pass a slice, which also accepts every length.
func Bump(values: &var int32[4]) {
values[0] += 100;
}
Two arrays of the same type are equal when every element is: == and != compare element by element from the first, with the element type's own equality, whatever the length, and an array literal on either side is built as the other operand's type, as in a == [1, 2, 3, 4]. Arrays of different lengths or element types are not comparable. Arrays have no ordering, so <, <=, > and >= on them are refused; compare the elements in the order you mean:
error: operator '<' is not defined for array 'int32[2]'
Length
.length is the element count N, a uint. It is fixed by the type, so it never changes and costs nothing to read:
let buffer: float64[8] = [0.0; 8];
let count = buffer.length; // 8
.length is an array's only member. An array has no .data: its first element's address is @a[0], and a[..].data is the data pointer of a view of it.
Indexing
a[i] is the element at position i, counting from zero, so valid indexes run from 0 to a.length - 1. The index may have any integer type. The result is a place: it can be read, written with = (a[i] = move value for a move-only element) or a compound assignment when the array is writable, borrowed, and have its address taken with @. A write destroys the element the place held before installing the new one.
var row: int32[3] = [10, 20, 30];
let first = row[0];
row[2] = 99;
row[1] += 5;
Every index is checked, on every target and in every build profile:
- A constant index into an array is checked when the program is compiled, and so are constant slice bounds past its end, as in
primes[1..9](slice end 9 is out of range for an array of 4 elements):error: index 7 is out of range for an array of 4 elements help: valid indexes are 0 through 3 - An index computed at run time is checked as the program runs. One that does not name an element — a negative index included — stops the program instead of reading the memory beyond the array, and the message names the function, file, line and column of the subscript:
func At(values: int32[4], i: uint) -> int32 { return values[i]; } func Main() -> int { let primes: int32[4] = [2, 3, 5, 7]; return At(primes, 4) as int; }Panic: index out of range at At (Src/Main.rux:2:18)
See Panics. A release build removes a check it can prove always passes.
The index must be an integer or a range; anything else is index for type 'int[2]' must be an integer or range, but has type 'float64'. Indexing with a range produces a slice — see below.
Iteration
for walks an array's elements in order, binding each one to the loop variable. To know the position as well, walk the indexes with a range:
var sum: int32 = 0;
for prime in primes {
sum += prime;
}
for i in 0..primes.length {
PrintLine("primes[{}] = {}", i, primes[i]);
}
A nested array yields whole rows, each an ordinary array. The loop variable may be a pattern that takes each element apart, such as for (key, value) in pairs over an array of tuples. See Loops.
Arrays as slices
An array is contiguous elements with a known count, so it can always be viewed as a slice — a pointer to the first element and a length, with nothing copied.
- Implicitly, read-only. A
T[N]converts to a read-onlyT[..]wherever one is expected: an argument, a binding, a return. One function taking a slice therefore serves arrays of every length. - Explicitly, with a range.
a[..]views the whole array, anda[i..j],a[i..],a[..j]anda[..=j]view part of it. The view is writable,var T[..], when the array place is — avarlocal, or an array reached through&var— and read-only otherwise.
func Total(values: int32[..]) -> int32 {
var sum: int32 = 0;
for value in values {
sum += value;
}
return sum;
}
func Fill(values: var int32[..], value: int32) {
for i in 0..values.length {
values[i] = value;
}
}
func Main() -> int {
var storage: int32[5] = [1, 2, 3, 4, 5];
let whole = Total(storage); // the array, viewed read-only
let tail = Total(storage[2..]); // 3 + 4 + 5
Fill(storage[..2], 0); // a writable view of the first two
return 0;
}
A writable parameter never takes the array itself, which only converts to a read-only view; hand it storage[..]:
error: no matching overload for 'Fill' with argument types (int32[5], int)
Elements after a header
A struct has no field without a length, so data whose count is known only at run time is kept in a growable Vector<T> or viewed through a slice. Elements that follow a header in one allocation — a layout foreign formats use — are reached through a raw pointer to the bytes after the header, which a pointer slice turns into a checked view. Every step through the raw pointer is in an unsafe block:
import Memory::{ Alloc, Free };
struct Header {
length: uint32;
}
func Main() -> int {
let count: uint = 3;
let block = Alloc(sizeof(Header) + count);
if block == null {
return 1;
}
// Safety: block was allocated above and is freed once, when Main returns.
defer unsafe { Free(block); }
// Safety: block holds a Header followed by count bytes.
unsafe {
let header = block as *var Header;
header.length = 3;
let bytes = ((block as *var uint8) + sizeof(Header))[..count];
bytes[0] = 7;
}
return 0;
}
See Pointers and Pointer slicing.
See also
- Slices — the
T[..]view an array is passed as - Ranges — the bounds used to slice an array and to walk its indexes
- Tuples — a fixed sequence of values of different types
- Layout — the size and alignment of an array
- Learn: Array, Array repeat, Array nested