Cella Documentation 0.1.0-dev Home Source

Arrays

An array is a fixed number of values of one type, stored one after another. Its type is array[T, N]: the element type T and the length N, which is part of the type and known when the program compiles. Arrays live wherever the variable holding them lives, on the stack or inside a record, and never allocate.

Array literals

An array literal lists its elements between brackets. Its type comes from the elements:

main: fun{	val primes = [2, 3, 5, 7]	val scores = [1.5, 2, 4.25]	printf("%d %f\n", primes[3], scores[1])}
Output
7 2.000000

primes is an array[i32, 4]. The elements of scores are an integer and two floats, so all three become f64 and the array is an array[f64, 3]. Integer literals that don't fit in an i32 make the elements a wider type, as they would in arithmetic.

Elements that can't share one type are an error:

main: fun{	val mixed = [1, "two"]The elements of this array have incompatible types: 'i32', 'str'}

Writing the type

A type on the declaration gives the elements their type, and the literal must have exactly that many elements:

main: fun{	val bytes: array[u8, 4] = [1, 2, 3, 4]	val short: array[u8, 4] = [1, 2]Cannot convert type 'array[u8, 2]' to 'array[u8, 4]'	val large: array[u8, 2] = [1, 300]'300' doesn't fit in 'u8'}

An empty literal [] has no elements to take a type from, so it needs one from the declaration:

main: fun{	val none: array[i32, 0] = []	val unknown = []Cannot infer the type of '[]'}

Indexing

a[i] is the element at index i, counting from 0. The index is a usize. Unsigned integers narrower than usize convert to it, but signed ones don't, so an i32 index needs usize(i):

main: fun{	var counts = [0, 0, 0]	val slot: u8 = 2	val offset: i32 = 1	counts[slot] = 30	counts[usize(offset)] = 20	counts[0] += 10	printf("%d %d %d\n", counts[0], counts[1], counts[2])}
Output
10 20 30

Writing to an element changes the array, so the array must be a var. A val array can't be changed through any of its elements:

main: fun{	val counts = [0, 0, 0]	counts[0] = 5Cannot reassign values}

Bounds checks

An index that is a constant is checked when the program compiles:

main: fun{	val primes = [2, 3, 5, 7]	printf("%d\n", primes[4])Index 4 is out of range for 'array[i32, 4]'}

Any other index is checked when the program runs. An index past the end stops the program with a panic instead of reading memory outside the array:

pick: fun(index: usize) -> i32{	val primes = [2, 3, 5, 7]	ret primes[index]} main: fun{	printf("%d\n", pick(3))	printf("%d\n", pick(4))}
Output
7
Panic at main.ce:11:6: index out of bounds

The check runs in every build, release builds included. A project can turn it off with the BoundsChecks setting; Checks and panics covers panics and that setting.

Length

a.length is the number of elements, as a usize. Since the length is part of the type, it's the same for every value of that type:

main: fun{	val primes = [2, 3, 5, 7]	var sum = 0	for i in 0..primes.length		sum += primes[i] 	printf("%zu primes, sum %d\n", primes.length, sum)}
Output
4 primes, sum 17

Sizes from constants

The length in array[T, N] is a constant usize. It can be a literal, a named constant or any constant expression, including sizeOf. Here the arrays are fields of records, which group named values like a C struct (Records covers them):

slotCount: const usize = 4 Pair: rec{	a: i32	b: i64} Inventory: rec{	slots: array[i32, slotCount]	spare: array[i32, slotCount * 2]	raw: array[u8, sizeOf(Pair)]} main: fun{	val inventory = Inventory()	printf("%zu %zu %zu\n", inventory.slots.length, inventory.spare.length, inventory.raw.length)}
Output
4 8 16

A variable can't be a length, even one that never changes. Constants of other integer types need a conversion:

count: const i32 = 3 main: fun{	var length = 3	var a: array[i32, length]'length' is not a constant	var b: array[i32, count]Cannot convert type 'i32' to 'usize'	var c: array[i32, usize(count)]}

An array's size in memory is its length times its element's size, with no header or padding between elements:

main: fun{	printf("%zu %zu %zu\n", sizeOf(array[i32, 4]), sizeOf(array[u8, 3]), sizeOf(array[i32, 0]))}
Output
16 3 0

Zeroed arrays

An array declared without a value starts with every element zeroed, when its element type allows it:

main: fun{	var totals: array[f64, 3]	totals[1] = 2.5	printf("%.1f %.1f %.1f\n", totals[0], totals[1], totals[2])}
Output
0.0 2.5 0.0

Numbers, bool, char and pointers start zeroed, and so do records whose fields all can, as long as they declare no constructor and no destructor. A record that declares a constructor, a new: op member, can't (see Records). An array of those starts uninitialized, and each element must be set before it's read:

Point: rec{	x: i32	y: i32 	new: op(x: i32, y: i32)	{		self.x = x		self.y = y	}} main: fun{	var points: array[Point, 2]	points[0] = Point(1, 2)	printf("%d\n", points[1].x)Cannot use uninitialized values}

A loop that sets every element counts, as long as its bounds are constants:

main: fun{	var points: array[Point, 3]	for i in 0..3		points[i] = Point(i32(i), i32(i) * 10) 	printf("%d %d\n", points[2].x, points[2].y)}
Output
2 20

The array builder

array[T, N](make) builds an array by calling make once for each index, in order from 0, and storing what it returns. make takes the index as a usize and returns a T. It's often a lambda (see Function values and closures), and it can use the variables around it:

main: fun{	val base = 100	val values = array[i32, 4](fun(i) => base + i32(i) * 10)	printf("%d %d %d %d\n", values[0], values[1], values[2], values[3])}
Output
100 110 120 130

Any function that takes a usize and returns the element type works, named functions included. The builder works for every element type, including records that can't start zeroed:

Tile: rec{	id: i32 	new: op(id: i32)	{		printf("make %d\n", id)		self.id = id	}} makeTile: fun(index: usize) -> Tile => Tile(i32(index) + 1) main: fun{	val tiles = array[Tile, 3](makeTile)	printf("last %d\n", tiles[2].id)}
Output
make 1
make 2
make 3
last 3

The function must have the right shape:

main: fun{	val a = array[i32, 3](fun(i, j) => 0)'fun[usize -> i32]' takes one parameter	val b = array[i32, 3](5)No constructor for 'array[i32, 3]' accepts argument of type 'integer literal'}

Nested arrays

An array's elements can be arrays. array[array[i32, 3], 2] is two rows of three, and grid[r][c] indexes the row, then the element in it:

main: fun{	var grid = [[1, 2, 3], [4, 5, 6]]	grid[1][2] = 60	val table = array[array[i32, 3], 3](fun(r) => array[i32, 3](fun(c) => i32(r * c)))	printf("%d %d %zu %zu\n", grid[1][2], table[2][2], grid.length, grid[0].length)}
Output
60 4 2 3

The rows of a nested literal share one element type, so they must all have the same length. Their elements are typed together, so a float in one row makes every row f64:

main: fun{	val ragged = [[1, 2], [3]]The elements of this array have incompatible types: 'array[i32, 2]', 'array[i32, 1]'}

Copying and moving arrays

An array copies when its element type copies. Assigning it, passing it to a parameter without a mode, or returning it makes a new array with the same elements, and changing one leaves the other alone:

doubled: fun(values: array[i32, 3]) -> array[i32, 3]{	var result = values	for i in 0..3		result[i] *= 2 	ret result} main: fun{	val original = [1, 2, 3]	val twice = doubled(original)	printf("%d %d\n", original[2], twice[2])}
Output
3 6

A destructor is a drop: op member that runs when a value is dropped (Ownership and destructors covers them). An array of values with a destructor can't be copied, since each element has one owner. It moves instead, and its elements are dropped when the array is, last element first:

Name: own rec{	id: i32 	drop: op	{		printf("drop %d\n", self.id)	}} keep: fun(names: array[Name, 3]){	printf("kept %d names\n", i32(names.length))} main: fun{	val names = [Name(1), Name(2), Name(3)]	keep(names)	printf("back in main\n")}
Output
kept 3 names
drop 3
drop 2
drop 1
back in main

keep takes the array by value, so the array moves into it and is dropped when keep returns. A function that only reads the array takes it with imm.

A single element can be moved out, as in keepOne(names[1]), when the index is a constant. The moved element is dropped by its new owner, and the rest of the array is still dropped when it goes out of scope:

keepOne: fun(name: Name){	printf("kept %d\n", name.id)} main: fun{	val names = [Name(1), Name(2), Name(3)]	keepOne(names[1])	printf("back in main\n")}
Output
kept 2
drop 2
back in main
drop 3
drop 1

The other elements can still be used, but the array as a whole can't, since part of it is gone. An index computed when the program runs can't be used to move an element out:

keepOne: fun(name: Name){} keepAll: fun(names: array[Name, 3]){} pick: fun(index: usize){	val names = [Name(1), Name(2), Name(3)]	keepOne(names[index])Cannot move array elements at computed indices} main: fun{	val names = [Name(1), Name(2), Name(3)]	keepOne(names[1])	keepAll(names)Cannot use partly moved valuesMoved on line 29}

Ownership and destructors covers moves and drops in general, and Parameter modes covers imm and mut parameters, which let a function read or change an array without copying it.

Looping over arrays

for x in a runs its body once for each element, from the first to the last. How the loop variable holds the element depends on the word before it:

LoopThe variableNeeds
for x in athe element by value: a copy, or the element moved out of the arraya can be moved, for elements that don't copy
for imm x in aa read-only borrow of the elementnothing
for mut x in aa mutable borrow of the elementa can be changed

A copy is enough for plain data. mut changes the elements in place:

Point: rec{	x: i32	y: i32} main: fun{	var values = [1, 2, 3]	for mut v in values		v *= 10 	var total = 0	for v in values		total += v 	var points = [Point(1, 2), Point(3, 4)]	for mut p in points		p.y += p.x 	printf("%d %d %d\n", total, points[0].y, points[1].y)}
Output
60 3 7

Elements with a destructor can't be copied, so for n in names would move them out of the array. To look at them, the loop takes them with imm:

main: fun{	val names = [Name(4), Name(9), Name(2)]	var best = 0	for imm n in names	{		if n.id > best			best = n.id	} 	printf("best %d\n", best)}
Output
best 9
drop 2
drop 9
drop 4

Moving elements out

for x in a over elements that can't be copied moves each element out of the array into the loop variable. An element that the body doesn't move on is dropped at the end of its pass. If the loop ends early with break, the elements it didn't reach are dropped when the loop ends:

keep: fun(n: Name){	printf("keep %d\n", n.id)} main: fun{	val names = [Name(1), Name(2), Name(3), Name(4)]	for n in names	{		if n.id == 3			break 		if n.id == 1			keep(n)	} 	printf("after the loop\n")}
Output
keep 1
drop 1
drop 2
drop 3
drop 4
after the loop

After the loop the array has been moved and can't be used. An array the function doesn't own, such as an imm parameter, can't be moved out of, so a plain for x in a over it borrows each element instead, as for imm x in a would.

Loops over temporaries

The array can be any expression, such as a call. A temporary array lives until the loop ends, so imm elements stay valid for the whole loop:

names: fun -> array[Name, 2] => [Name(7), Name(8)] main: fun{	for imm n in names()		printf("name %d\n", n.id) 	printf("done\n")}
Output
name 7
name 8
drop 8
drop 7
done

What the loop rejects

For an array, the word goes before the variable, never before the array:

main: fun{	var counts = [1, 2, 3]	for c in mut countsCannot use 'mut' on 'array[i32, 3]' loop sources	{	}}

for mut changes the array, so the array must be a var:

main: fun{	val fixed = [4, 5, 6]	for mut c in fixedCannot mutably borrow values		c += 1}

Records can define their own for loops with loop operators; see Operators.