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.

Moving into an annotated literal.
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.

Comparing wide arrays.
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-only T[..] 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, and a[i..j], a[i..], a[..j] and a[..=j] view part of it. The view is writable, var T[..], when the array place is — a var local, 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
Indexing a tail directly.
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