Arrays
An array<T> is a growable, contiguous buffer of exactly one type. It is not a hash map and it is not heterogeneous. The brackets are just how you write one. When the length is part of the type instead, that is a fixed_array<T, N>. When it must grow without moving live slots, that is a stable_array<T>.
array<int32> $numbers = [1, 2, 3];
$numbers[] = 4;
echo $numbers->count(); // 4
echo $numbers[0]; // 1Because arrays are objects, the things you know from a standard library live on the array itself: count(), push(), pop(). The catch is how the buffer is grown and when a borrow into it dies.
Four ways to make one
The constructor, with the element type written out:
array<int32> $numbers = array<int32>();
$numbers[] = 7;
echo $numbers->count(); // 1A declaration with no initializer, which default-constructs and allocates nothing:
array<int32> $numbers;
$numbers[] = 7;
echo $numbers->count(); // 1A literal, which takes its type from where it is going:
array<float64> $ratios = [0.5, 1.5];
echo $ratios[1]; // 1.500000const works on that spelling too. The literal fills a mutable array and moves it into the name, so $c is frozen from the moment it exists:
const $c = [1, 2];
const array<int32> $typed = [1, 2];
echo $c[0]; // 1
echo $typed[0]; // 1A const array<T> field on a struct is the same fill: write the literal at the call. Structs is that shape.
Or arr::room, when you already know how many elements are coming:
array<int32> $numbers = arr::room<int32>(64);
echo $numbers->count(); // 0
echo $numbers->capacity(); // 64Note what is not on that list: there is no array<int32>(5). A single number could mean "five slots" or "one element", and I would rather not guess. arr::room says which one you meant.
Literal type
Write the type and the elements are checked against it:
array<int32> $mixed = [1, 2, "three"];
// error: Invalid type conversion: cannot assign 'string' to 'int32'Leave the type out and the first element decides:
$names = ["Mario", "Ray", "Ronon"]; // array<string>
$numbers = [1, 2, 3]; // array<int32>
echo $names[2]; // Ronon
echo $numbers->count(); // 3The destination also types each element, so a leading-dot constructor works the same way it does at any other destination:
enum Slot
{
case empty;
case filled(int32 $n);
}
array<Slot> $row = [.empty, .filled(3)];
echo $row->count(); // 2An empty literal has nothing to go on, so it needs a destination that says:
$empty = [];
// error: an empty array literal has nothing to go onarray<int32> $empty = []; // fine
echo $empty->count(); // 0Two brackets
$a[$i] names an existing element. $a[] names the slot one past the end, so assigning to it appends:
array<int32> $numbers = [1, 2];
$numbers[] = 3; // append
$numbers[0] = 99; // overwrite
echo $numbers[0]; // 99
echo $numbers->count(); // 3Both are ordinary declared operators, told apart by arity alone, and you can declare the same pair for your own type. Operators has the mechanics.
Since $a[] names a slot that does not hold anything yet, reading it is refused rather than returning garbage:
array<int32> $numbers = [1, 2];
echo $numbers[];
// error: names the slot after the last one, so there is nothing there to readMethods and brackets
push is $this[] = mv $value and at is &$this[$index]. That's not a coincidence to remember, it is literally how they are written, so the two spellings can never drift apart:
array<int32> $numbers = array<int32>();
$numbers->push(1); // same as $numbers[] = 1
int32 $n = 4;
$numbers[] = mv $n; // hands the local over, no copy
int32& $slot = $numbers[0]; // a T& destination takes the address, same as at(0) or &$numbers[0]
$slot = 5;
echo $numbers[0]; // 5The full surface, off stdlib/core/array.eco:
const function count() : usize
const function capacity() : usize
const function empty() : bool
function reserve(usize $count) : void
function fit(usize $count) : void
function shrink() : void
const function room() : usize
const function data() : ptr<const T>
function data() : ptr<T>
function spare() : ptr<T>
function commit(usize $n) : void
function push(T $value) : void
function slot() : T&
function extend(const array<T>& $other) : void
function extend(usize $start, usize $count) : void
function take_from(inout array<T>& $other) : void
function pop() : T
function remove(usize $index) : T
function pluck(usize $index) : T
function truncate(usize $count) : void
function clear() : void
const function get(usize $index) : T
function at(usize $index) : T&
const function at(usize $index) : const T&
function sub() : slice<T>
function sub(usize $from, usize $count) : slice<T>
const function clone() : array<T>Every index is a usize, and every one of them is bounds-checked with assert. echoc build drops those checks, which is the same bargain C's assert makes. See Errors and panics.
Growth
reserve doubles the capacity, and doubling from zero is zero, so the first allocation gets a floor instead. The floor depends on the element size, because the smaller the element the more of them fit in the smallest block an allocator hands out at all:
array<usize> $words;
array<uint8> $bytes;
$words[] = 1;
$bytes[] = 1;
echo $words->capacity(); // 4
echo $bytes->capacity(); // 8A request above the floor is honoured exactly, which is what keeps reserve's promise that calling it up front is the one allocation for the whole loop:
array<usize> $big;
$big->reserve(1000);
echo $big->capacity(); // 1000fit skips both the doubling and the floor, for when you want a small buffer sized to the byte:
array<usize> $tight;
$tight->fit(3);
echo $tight->capacity(); // 3Growth invalidates borrows
This is the rule that bites, and nothing enforces it. There is no borrow checker. When an array grows it may move its buffer, and every slice<T> and every &$a[$i] you were holding points at the old one:
int32& $first = $a->at(0);
$a[] = 999; // may reallocate
$first = 1; // the borrow may now point at freed memoryReserve up front and the problem goes away, because there is no reallocation left to trip over. Pointers and references covers what a borrow is and how long it is good for. A stable_array<T> is the other way out: growth takes another block and leaves the seats you already have alone.
slot
Sometimes you want to write into the new element rather than construct one and copy it in. slot() hands back a borrow of the fresh slot, and &$a[] is the same thing spelled with brackets:
struct Point
{
int32 $x;
int32 $y;
}
array<Point> $points = array<Point>();
Point& $slot = &$points[];
$slot->x = 1;
$points->slot()->y = 2;
echo $points->count(); // 2
echo $points[0]->x; // 1
echo $points[1]->y; // 2Neither form reads the slot, which is why both are legal where echo $a[] is not. They are for a T that is just bytes. An owning element is $a[] = $v or push: that seats a value into a fresh slot. Writing through &$a[] would run a destructor over leftover storage.
An array owns its elements
If T owns something, the array owns it too, and it ends when the array does:
{
array<string> $words = [];
$words[] = '';
$words[0]->append('hello');
echo $words[0]; // hello
} // the buffer and the string are both freed right hereCopying an array copies the elements. For a string that means one more reference to the same buffer, not a duplicated allocation:
array<int32> $a = [1, 2, 3];
array<int32> $b = $a;
$b[0] = 99;
echo $a[0]; // 1
echo $b[0]; // 99$b = $a and $a->clone() are the same copy, one inferred and one said out loud. If you want to hand the buffer over instead of duplicating it, that's a move:
array<int32> $a = [1, 2, 3];
array<int32> $b = mv $a;
echo $b->count(); // 3Ownership and moving is the full story.
Removal
pop() hands the element over rather than copying it. The array stops owning it at the same moment the caller starts:
array<string> $words = [];
$words[] = '';
$words[0]->append('popped');
string $out = $words->pop();
echo $out; // popped
echo $words->count(); // 0remove($i) keeps the order and shifts everything after it down. pluck($i) does not keep the order: it relocates the last element into the hole, which is why it is the cheap one:
array<string> $letters = [];
$letters[] = 'x';
$letters[] = 'y';
$letters[] = 'z';
echo $letters->pluck(0); // x
echo $letters[0]; // z
echo $letters->count(); // 2truncate($n) drops everything past $n, destroying what it drops. clear() is truncate(0): the elements go, the buffer stays, and the array is immediately usable again. That's the reason to clear rather than reassign.
array<int32> $numbers = [1, 2, 3, 4];
$numbers->truncate(2);
echo $numbers->count(); // 2
$numbers->clear();
echo $numbers->count(); // 0
echo $numbers->capacity(); // 4Self-extend
extend borrows its source and writes into its receiver, and those two accesses cannot name one array:
array<int32> $a = [1, 2];
$a->extend($a);
// error: This names the same storage as another argument of the same callSay which range you meant instead, and it is unambiguous:
array<int32> $a = [1, 2, 3];
$a->extend(0, $a->count()); // double it
echo $a->count(); // 6
echo $a[3]; // 1For two different arrays, extend is fine, and arr::merge is the version that builds a third:
array<int32> $a = [1, 2, 3];
array<int32> $b = [4, 5];
array<int32> $merged = arr::merge($a, $b);
echo $merged->count(); // 5
echo $merged->capacity(); // 5
echo $a->count(); // 3merge reserves the total up front, so the whole result is one allocation, and both inputs are const borrows so neither is consumed.
take_from
extend copies every element. When the source is a batch you are done with, you want the elements, not a second copy of them. take_from moves them across and leaves the source empty:
array<int32> $all = [1, 2];
array<int32> $got = [3, 4, 5];
$all->take_from($got);
echo $all->count(); // 5
echo $got->count(); // 0
echo $all[4]; // 5A T that cannot be copied still travels, because nothing is duplicated. Nested owners move with the element: an array<string> field is the same handle, not a second buffer.
One element is still $a[] = mv $x or push(mv $x). $a[] = mv $s on a foreach &$s is refused: $s is a borrow of a slot. Ownership is that catch. Drain the array instead.
$a->take_from($a) is the same conflict extend names, with both sides inout.
Element writes
Writing into an element destroys what was there and stores the new value, exactly as writing into a field does. $at is not evaluated twice:
array<string> $rows = array<string>();
$rows->push('first');
usize $at = 0;
$rows[$at] = 'replaced';
echo $rows[0]; // replacedA literal written into an element is checked against the element type, exactly as one written into a declaration is. A value that does not survive the trip is refused rather than truncated:
array<int32> $ints = [];
$ints[] = 2.5;
// error: The floating point number literal '2.5' cannot be implicitly converted to an integer type
// due to non zero decimal values.One that converts exactly still converts, and says nothing, because nothing was lost:
array<int64> $wide = [1, 2.0];
echo $wide[0]; // 1
echo $wide[1]; // 2The same goes for a literal inside the brackets, since [1, 2.5] is those appends written another way. Note that the element type comes from the first element, so [2.5, 1] is an array<float64> holding 2.5 and 1.0. That ordering rule is deliberate.
C buffers
Strings already had this: reserve, spare(), commit(). Arrays were missing it, which is how you ended up allocating a sidecar, copying sample by sample, and freeing by hand. The sequence is the same one, counted in elements rather than bytes.
array<float32> $frames = arr::room<float32>(4);
ptr<float32> $p = $frames->spare();
$p:$[0] = 1.0f;
$p:$[1] = 2.0f;
$frames->commit(2);
echo $frames->count(); // 2
echo $frames[0]; // 1.000000
echo $frames->room(); // 2arr::room is the allocation. spare() is the first free slot. room() is how many elements that pointer may accept without growing. commit($n) makes those slots live. Growing the buffer after spare() invalidates the pointer, the same rule reserve has always had.
data() is the live prefix, for C that already has a length. Empty is null.
This is only honest for a T that is bytes. C cannot construct an Echo string, and spare / commit on an array<string> die rather than invent a constructor. slot() is the Echo-side fill for that T.
A fixed_array<T, N> is already a C buffer: every slot is live, so there is no spare. $quad->data() is the first element and N is the count.
Pass the pointer and the count yourself. A live slice<T> is two public words, $data and $len, if the window is already elements.
C interop has the rest of the boundary.
Sorting
$a->sort() is stable, $a->sort_unstable() is the faster in-place one. Integers, floats and strings just work. A struct of yours opts in by naming itself, struct Gate : contract::comparable<Gate>, and an operator <. There is no Self. by: and key: pick a different order. Sorting is the chapter.
array<int32> $n = [3, 1, 2];
$n->sort();
echo $n[0]; // 1A silent bug
It is real, it is silent, and it is on the list.
An array literal inside a field-wise constructor loses its elements. Bag([7, 9]) and then reading it back is a use-after-destruction at every optimization level, with no diagnostic. Build the array first and pass it in.
Next
- C interop for handing
data()andspare()to a C function. - Slices for handing out a window onto an array without copying it.
- Sorting for
sort,sort_unstable,by:andkey:. - Iteration for
foreach, and for the copy it does not make you pay for. arr::mergeandarr::room, which do not belong on the type.- Stable arrays when you need to grow without moving live slots.
- Ownership and moving for what happens when
Towns something.