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>.
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]; // 1Or 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.
A literal takes its type from its destination
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(); // 3An 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(); // 0The two brackets do different jobs
$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 readThe methods and the brackets are the same code
push is $this[] = $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& $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 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 is doubling, with a floor
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 every borrow into the array
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.
slot appends without building a value first
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.
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.
The removal methods are not interchangeable
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(); // 4An array cannot extend itself
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.
An element is a destination like any other
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.
Letting C write into one
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.
One thing that compiles and is wrong
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.
- Iteration for
foreach, and for the copy it does not make you pay for. arr::mergeandarr::room, which do not belong on the type.- Ownership and moving for what happens when
Towns something.