Arrays and lists
A fixed-length array whose count is in its header, and the growable type built on it.
Arrays
An array is a heap object with its length in the header and its elements inline, which is the same shape a string literal has.
// Arrays, `for` loops, and generic functions over them.
//
// An array is a heap object with its length in the header and its elements
// inline -- the same shape a string literal has. Indexing bounds-checks and
// panics on failure, like `.?` on a null optional: a failure the type system
// permits but the program must not perform.
const array = @import("std/array");
const str = @import("std/str");
// A type parameter is written when it has to be named, and inferred when not.
fn first[T](a: []T) T { return a[0]; }
fn sum(xs: []i64) i64 {
var total = 0;
for (xs) |x| { total = total + x; }
return total;
}
// `for (xs) |x, i|` binds the index as well as the element.
fn show(xs: []i64) str {
var out = "";
for (xs) |x, i| {
if (i > 0) { out = str.concat(out, ", "); }
out = str.concat(out, str.from_int(x));
}
return out;
}
fn main() i64 {
const xs = []i64{ 3, 1, 4, 1, 5 };
print(show(xs));
print(sum(xs));
print(first(xs));
print(first([]str{ "a", "b" }));
// Every one of these returns a new array: the length lives in the header,
// so there is no capacity to grow into. `std/list` is the type that has
// one -- see examples/list.ws.
print(show(array.push(xs, 9)));
print(show(array.slice(xs, 1, 4)));
print(show(array.concat(xs, []i64{ 9, 2 })));
// `a[i]` is a place as well as a value.
var ys = []i64{ 1, 2, 3 };
ys[1] += 10;
print(show(ys));
return 0;
}wsharp run examples/arrays.ws3, 1, 4, 1, 5
14
3
a
3, 1, 4, 1, 5, 9
1, 4, 1
3, 1, 4, 1, 5, 9, 2
1, 12, 3Three things to notice.
for binds an index if you ask for one. for (xs) |x| walks the elements and
for (xs) |x, i| walks them with their positions.
a[i] is a place as well as a value, so ys[1] += 10 writes back. An index
outside the array panics, exactly as .? on a null optional does: a failure the
type system permits but the program must not perform.
Every std/array operation returns a new array. The length lives in the
header and there is no capacity beside it, so array.push allocates a whole new
array every call. That is fine for building something once and wrong for a loop,
which is what the next section is about.
Any integer type indexes. An index is an i64 at the machine level and the
widening is emitted where it is used, so a u8 loop counter needs no conversion.
That is not a hole in the rule that numeric conversions are written: an index is
not a value the program keeps, it is an argument to one operation whose type is
fixed. A u64 past i64’s range arrives negative and is caught anyway, because
the bounds check is one unsigned compare.
g[i][j] = v works too. The base of a place is evaluated once into a hidden local,
so a compound assignment reads the place and writes it back without evaluating the
base twice.
Lists
list.List[T] is the second object that length needs: a backing array whose
header length is the capacity, and a count of how much of it is in use. Pushing
writes into the spare tail, only a full list reallocates, and it doubles when it
does, so a run of pushes is amortised constant time.
// A growable array.
//
// wsharp run examples/list.ws
//
// `[]T` is fixed-length: its count lives in the object header, and there is no
// capacity beside it, so `array.push` allocates a whole new array every call.
// `list.List[T]` is the second object that length needs -- a backing array
// whose header length is the *capacity*, and a count of how much of it is in
// use. Pushing writes into the spare tail; only a full list reallocates, and
// it doubles when it does, so a run of pushes is amortised constant time.
const list = @import("std/list");
const str = @import("std/str");
/// Collatz: how many steps `n` takes to reach 1, and the path it took.
fn path(n: i64) list.List[i64] {
var steps: list.List[i64] = list.new();
var v = n;
list.push(steps, v);
while (v != 1) {
if (v % 2 == 0) { v = v / 2; } else { v = 3 * v + 1; }
list.push(steps, v);
}
return steps;
}
fn show(xs: list.List[i64]) str {
var out = "";
// A list is walked directly: `for` over anything but an array calls `iter`
// and `next` from the module that declares the type, so `std/list` says
// how a list is iterated without the type checker knowing it exists.
for (xs) |v, i| {
if (i > 0) { out = str.concat(out, " -> "); }
out = str.concat(out, str.from_int(v));
}
return out;
}
fn main() i64 {
const p = path(7);
print(show(p));
print(list.len(p));
// The list grew from nothing, so its capacity is the first power of two
// past its length -- the spare tail is what makes the next push free.
print(list.capacity(p));
// Lists of references work the same way; a growth copies them through the
// write and load barriers, which is why `std/list` is written in W#.
var words: list.List[str] = list.new();
list.extend(words, str.split("the quick brown fox", " "));
list.push(words, "jumps");
print(str.join(list.to_array(words), ","));
// `pop` and `remove` hand a value back; both panic rather than returning
// `?T`, for the reason `a[i]` does.
print(list.pop(words));
print(list.remove(words, 0));
print(list.len(words));
return 0;
}wsharp run examples/list.ws7 -> 22 -> 11 -> 34 -> 17 -> 52 -> 26 -> 13 -> 40 -> 20 -> 10 -> 5 -> 16 -> 8 -> 4 -> 2 -> 1
17
32
the,quick,brown,fox,jumps
jumps
the
3for over a list works, and nothing in the type checker knows what a list is.
for over anything that is not an array calls iter and next from the module
that declares its type, so std/list says how a list is iterated by writing two
functions, and any type you write can do the same.
pop and remove hand a value back and panic on an empty list rather than
returning ?T, for the same reason a[i] does: asking for element zero of an
empty list is a bug, not a case.
std/list is written in W# rather than in Rust, and the reason is the rule that
draws the line for the whole standard library: a builtin may read and write
bytes, and anything that moves a reference from one object into another is
written in W#, where the write barrier, the load barrier and the stack maps all
apply by construction. A list growth copies references, so it is W#.
See the standard library for everything std/list and
std/array provide.