collections

tape
import collections from "collections";

var words: collections.Vec_string = collections.vec_string_new();
words.push("apple");
words.push("pear");

var counts: collections.Map_string_i64 = collections.map_string_i64_new();
counts.set("apple", 3);
let n: i64 = counts.get("apple")!;          // 3
let missing: ?i64 = counts.get("kiwi");     // null

collections has the containers programs need: a growable vector, a hash map, a hash set, a double-ended queue, a binary heap and a bit set.

tape has no generics: a container type is made for one element type by a generator, at compile time (@tape generators). The common ones are already made, so you use them directly:

Ready to useHolds
collections.Vec_i64, Vec_u8, Vec_f64, Vec_stringvectors
collections.Map_string_string, Map_string_i64, Map_i64_i64maps
collections.Set_string, Set_i64sets

For any other type, call the generator once in your module; the types land in your module, so you use them without the collections. prefix:

tape
import collections from "collections";

pub struct Person { name: string; age: i64; }

@tape {
    collections.gen_vec(Person);              // Vec_Person, vec_Person_new()
    collections.gen_map(string, Person);      // Map_string_Person
}

pub fn main() -> i32 {
    var people: Vec_Person = vec_Person_new();
    people.push(Person { name: "Ada", age: 36 });
    return 0;
}

An element type you generate for must be pub. Your own Vec_i64 from gen_vec(i64) and collections.Vec_i64 are two different types: use the ready-made ones when they fit.

collections is a pure module: it allocates, but has no other effect.

Ownership

A container owns copies of what you put in it, and frees them:

  • push, set, insert, add store a copy (a temporary, like "item-{i}" or a struct literal, is moved in rather than copied);
  • get, front, peek and iteration give you a copy you own;
  • pop and remove give you the element and take it out;
  • replacing or removing an element frees the old one;
  • assigning a container copies it, elements and all; it is freed when its variable goes out of scope.

So elements can be strings, structs with strings, tagged unions, or other containers, and nothing leaks (see Ownership). v.free() frees a container early; using it afterwards is fine, it’s just empty.

get_ref, get_ptr, slice and slice_mut let you work on elements in place without copying: get_ref and slice to read them, get_ptr and slice_mut to change them. The pointer or slice is valid until the container next grows, shrinks or removes something.

The methods that only read (len, get, get_ref, has, iter, slice, …) work on a container held in a let, or passed as a parameter; the ones that change it (push, set, get_ptr, slice_mut, clear, …) need a var:

tape
let v: collections.Vec_i64 = load();
let n: i64 = v.len();      // fine: len only reads
v.push(1);                 // error: '.push()' changes 'v', a 'let' binding

Indexes run from 0 to len() - 1; one outside that stops the program with a trap (Vec: index out of range). Absence is an optional: pop on an empty vector and get of a missing key return null.

Vec

Vec_T is a growable array.

FunctionDoes
vec_T_new() -> Vec_TAn empty vector (allocates on the first push)
vec_T_with_cap(cap: i64) -> Vec_TAn empty vector with room for cap elements
v.len() -> i64, v.is_empty() -> bool, v.cap() -> i64Size and capacity
v.push(item: T)Appends a copy
v.push_take(item: *T)Appends *item without copying it; *item is left zeroed (an empty string or container)
v.extend(items: []const T)Appends a copy of each
v.pop() -> ?TRemoves and returns the last element (moved out, not copied); null when empty
v.get(i: i64) -> TA copy of element i
v.get_ref(i: i64) -> *const TElement i in place, read-only
v.get_ptr(i: i64) -> *TElement i in place, to change it
v.set(i: i64, item: T)Replaces element i
v.set_take(i: i64, item: *T)Replaces element i with *item, not copied; *item left zeroed
v.insert(i: i64, item: T)Inserts before element i (i == len appends)
v.insert_take(i: i64, item: *T)Inserts *item before element i, not copied; *item left zeroed
v.remove(i: i64) -> TRemoves and returns element i (moved out, not copied); the rest move down
v.truncate(n: i64)Keeps the first n elements
v.reserve(n: i64)Makes room for n more without growing again
v.clear()Removes everything, keeps the memory
v.free()Removes everything and frees the memory
v.slice() -> []const TThe elements in place, read-only: for bytes, fs.write_file and loops
v.slice_mut() -> []TThe elements in place, to change: for sort
v.iter()Iterates over copies, first to last

push, set and insert store a copy of what you give them. When you are done with a value (a block you built, a string you made), the _take forms move it in instead: v.push_take(&b) stores b without copying its strings and vectors, and leaves b zeroed (an empty value that owns nothing). pop and remove hand you the element itself.

tape
var v: collections.Vec_i64 = collections.vec_i64_new();
for (i in 0..5) { v.push(i * i); }       // 0 1 4 9 16
v.insert(0, -1);                         // -1 0 1 4 9 16
let last: i64 = v.pop()!;                // 16
var sum: i64 = 0;
for (x in v.iter()) { sum = sum + x; }   // 13

Map

Map_K_V is a hash map from keys K to values V.

FunctionDoes
map_K_V_new() -> Map_K_VAn empty map
m.len() -> i64, m.is_empty() -> boolNumber of entries
m.set(key: K, value: V)Sets key to a copy of value, replacing an old value
m.get(key) -> ?VA copy of the value; null when key is absent
m.get_ref(key) -> ?*const VThe value in place, read-only; null when absent
m.get_ptr(key) -> ?*VThe value in place, to change it; null when absent
m.get_ptr_or_set(key, value) -> *VThe value in place, key set to value first when absent (tell which by m.len() growing): one lookup where has, set and get_ptr take three
m.has(key) -> boolWhether key is there
m.remove(key) -> boolRemoves the entry; false when absent
m.clear(), m.free()Removes every entry (and frees the memory)
m.iter()Iterates over entries, each with .key and .value

Keys can be integers, bool, enums, pointers, string or []const u8. Text keys are compared by content. A string-keyed map looks up with any text: a literal, a string, or a slice of one, without allocating. A []const u8 key is stored as the slice, not a copy, so the text it points to must outlive the map.

tape
var counts: collections.Map_string_i64 = collections.map_string_i64_new();
for (w in bytes.split("a b a c a", " ")) {
    let c: ?i64 = counts.get(w);
    if (c == null) { counts.set("{w}", 1); } else { counts.set("{w}", c! + 1); }
}
// counts.get("a")! == 3

for (e in counts.iter()) {
    io.println("{e.key}: {e.value}");
}

The iteration order isn’t sorted or insertion order, but it is the same every time the same operations run, in the VM and natively.

Floats aren’t keys (NaN isn’t equal to itself). For a struct key, or any key with your own idea of equality, use gen_map_by with a hash function and an equality function; keys that are equal must hash the same:

tape
pub struct Point { x: i64; y: i64; }
fn point_hash(p: *const Point) -> u64 { return hash.hash_i64(p.x * 31 + p.y); }
fn point_eq(a: *const Point, b: *const Point) -> bool { return a.x == b.x && a.y == b.y; }

@tape { collections.gen_map_by(Point, string, point_hash, point_eq); }

var names: Map_Point_string = map_Point_string_new();
names.set(Point { x: 1, y: 2 }, "home");

Set

Set_T holds each value once: the keys of a map without the values. Same key types as Map; gen_set_by for others.

FunctionDoes
set_T_new() -> Set_TAn empty set
s.len() -> i64, s.is_empty() -> boolNumber of elements
s.add(key: T) -> boolAdds a copy; false if it was already there
s.has(key) -> boolWhether key is there
s.remove(key) -> boolRemoves it; false when absent
s.clear(), s.free()Removes everything (and frees the memory)
s.iter()Iterates over copies of the elements

Deque

Deque_T is a queue you can push to and pop from at both ends, in constant time. Generate it with collections.gen_deque(T).

FunctionDoes
deque_T_new() -> Deque_TAn empty deque
d.len() -> i64, d.is_empty() -> boolNumber of elements
d.push_back(item: T), d.push_front(item: T)Adds a copy at that end
d.pop_back() -> ?T, d.pop_front() -> ?TRemoves and returns from that end; null when empty
d.front() -> ?T, d.back() -> ?TA copy of the element at that end
d.get(i: i64) -> T, d.set(i: i64, item: T)Element i, counted from the front
d.clear(), d.free()Removes everything (and frees the memory)
d.iter()Iterates over copies, front to back
tape
@tape { collections.gen_deque(i64); }

var q: Deque_i64 = deque_i64_new();
q.push_back(1);
q.push_back(2);
let first: i64 = q.pop_front()!;   // 1

Heap

Heap_T_by_less is a priority queue: pop always gives the least element by the less function you pass to collections.gen_heap(T, less), and the type is named after both. With “a < b” you get the smallest first; with “a > b”, the largest.

FunctionDoes
heap_T_by_less_new() -> Heap_T_by_lessAn empty heap
h.len() -> i64, h.is_empty() -> boolNumber of elements
h.push(item: T)Adds a copy
h.pop() -> ?TRemoves and returns the least element; null when empty
h.peek() -> ?TA copy of the least element
h.clear(), h.free()Removes everything (and frees the memory)
tape
pub struct Job { pri: i64; name: string; }
fn more_urgent(a: *const Job, b: *const Job) -> bool { return a.pri > b.pri; }

@tape { collections.gen_heap(Job, more_urgent); }

var jobs: Heap_Job_by_more_urgent = heap_Job_by_more_urgent_new();
jobs.push(Job { pri: 1, name: "tidy" });
jobs.push(Job { pri: 9, name: "ship" });
let next: Job = jobs.pop()!;       // "ship"

Elements that compare equal come out in no particular order. Each order is its own type, so one module can have several heaps of one element type (Heap_Job_by_more_urgent, Heap_Job_by_oldest).

BitSet

A fixed number of bits, all clear at first.

FunctionDoes
collections.bitset(n: i64) -> BitSetn bits, all clear
b.len() -> i64The number of bits
b.set(i), b.clear(i), b.toggle(i)Changes bit i
b.test(i) -> boolWhether bit i is set
b.count() -> i64How many bits are set
b.set_all(), b.clear_all()Every bit
b.free()Frees the bits; the set is then empty
tape
var seen: collections.BitSet = collections.bitset(1000);
seen.set(42);
if (seen.test(42)) { /* ... */ }

A BitSet is copied by assignment and freed at scope end, like the other containers.

Generators

GeneratorMakes
gen_vec(T)Vec_T, vec_T_new, vec_T_with_cap
gen_map(K, V)Map_K_V, map_K_V_new (built-in key types)
gen_map_by(K, V, hash_fn, eq_fn)the same with your fn(*const K) -> u64 and fn(*const K, *const K) -> bool
gen_set(T), gen_set_by(T, hash_fn, eq_fn)Set_T, set_T_new
gen_deque(T)Deque_T, deque_T_new
gen_heap(T, less)Heap_T_by_less, heap_T_by_less_new, ordered by your fn(*const T, *const T) -> bool

Type names follow the $T naming: Map_cslice_u8_i64 for []const u8 keys, Vec_Vec_string for a vector of vectors. Each made type also gets _copy and _drop functions (Vec_Person_copy), which the compiler calls when you assign or drop one; you don’t call them yourself.

Last modified: