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.
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; move-only elements are listed, each handed over with <-, or written in a loop. A count of zero still evaluates the value once, and destroys it if its type needs destruction.
rux 0.4.0 refuses a move into an array literal that has a type annotation: with a move-only
k, let b: Key[1] = [<-k]; reports value 'k' is used after it was moved. The same literal without the annotation, let b = [<-k];, is accepted.Value semantics
An array is a value, like a struct or a tuple. Assigning it, passing it by value and returning it copy every element, and the two arrays share nothing afterwards:
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. Arrays have no ordering.
rux 0.4.0 compares an array wider than 16 bytes incorrectly — only its first eight bytes take part — and accepts
<, <=, > and >= on arrays, comparing their bytes as one number. Compare such arrays element by element.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 =, <- or a compound assignment when the array is writable, borrowed, and have its address taken with @.
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:
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. 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)
Flexible tails
T[], with no count, is a flexible tail. It is allowed in exactly one place — as the last field of a struct — and describes elements that follow the struct in memory, in storage allocated larger than the struct itself:
struct Packet {
length: uint32;
bytes: uint8[];
}
The tail contributes its alignment to the struct but no storage, so sizeof(Packet) is 4. It has no length and no members of its own — the struct records the count, here in length. Reach the elements by taking the tail's address, a *var (uint8[]), converting it to an element pointer, and viewing as many elements as were allocated:
import Memory::{ Alloc, Free };
func Main() -> int {
let count: uint = 3;
let packet = Alloc(sizeof(Packet) + count) as *var Packet;
if packet == null {
return 1;
}
defer Free(packet);
packet.length = 3;
let bytes = (@packet.bytes as *var uint8)[..count];
bytes[0] = 7;
return 0;
}
A flexible tail anywhere else — a field that is not last, a union member, a local, a parameter, a return type, an operand of sizeof — is an error:
error: flexible array type is only allowed as the final field of a struct
rux 0.4.0 accepts
packet.bytes[i], checks i against a length of zero, and so stops the program with Panic: index out of range for every index. Index the view built from the tail's address, as above.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 and of a struct with a flexible tail
- Learn: Array, Array repeat, Array nested