fixed

tape
import fixed from "fixed";

@tape {
    fixed.gen_fixed_vec(u8, 256);          // FixedVec_u8_256
    fixed.gen_ring(i64, 16);               // Ring_i64_16
    fixed.gen_fixed_map(u32, i64, 64);     // FixedMap_u32_i64_64
}

var line: FixedVec_u8_256 = fixed_vec_u8_256_new();
if (!line.push(65)) { /* full */ }

var events: Ring_i64_16 = ring_i64_16_new();
events.push_back(7);
let next: ?i64 = events.pop_front();      // 7

var ports: FixedMap_u32_i64_64 = fixed_map_u32_i64_64_new();
ports.set(80 as u32, 3);
let fd: ?i64 = ports.get(80 as u32);      // 3

fixed has containers whose elements live inside the container value itself: a FixedVec_u8_256 is 256 bytes and a length, nothing more. They never allocate, so they work in every profile, t0 included: in a kernel, a network stack, a freestanding program, or wherever you want a container that lives in a global, on the stack or in an arena block.

The capacity is part of the type, chosen when you generate it (gen_fixed_vec(T, N) gives FixedVec_T_N). When a container is full, adding returns false and changes nothing; taking from an empty one returns null. An index outside 0..len() stops the program with a trap, as an array’s does.

Elements are plain values, copied by assignment: numbers, bool, enums, pointers, views ([]const u8, and string in t0, which is a view there), and structs of those. Assigning a container copies all of it. A t1 string (owned) is refused when you generate: these containers never free what they hold. For growable, owning containers use collections.

The module is @profile(t0) and pure. It’s all generators: call them in a @tape block, and the types land in your module.

FixedVec

FixedVec_T_N holds up to N elements in order.

FunctionDoes
fixed_vec_T_N_new() -> FixedVec_T_NAn empty vector
v.len(), v.cap()Elements held, and N
v.is_empty(), v.is_full()
v.push(item) -> boolAppends; false when full
v.pop() -> ?TRemoves and returns the last; null when empty
v.get(i) -> TA copy of element i
v.get_ref(i) -> *const T, v.get_ptr(i) -> *TElement i in place
v.set(i, item)Replaces element i
v.insert(i, item) -> boolInserts before element i (i == len appends), moving the rest up; false when full
v.remove(i) -> TRemoves element i, moving the rest down
v.swap_remove(i) -> TRemoves element i, the last element taking its place (constant time)
v.truncate(n), v.clear()Keeps the first n, or none
v.slice() -> []const T, v.slice_mut() -> []TThe elements in place
v.iter()Copies of the elements, first to last

Ring

Ring_T_N is a queue of up to N elements you can push to and pop from at both ends, in a circular buffer (any N, not only powers of two).

FunctionDoes
ring_T_N_new() -> Ring_T_NAn empty ring
r.len(), r.cap(), r.is_empty(), r.is_full()
r.push_back(item) -> bool, r.push_front(item) -> boolAdds at that end; false when full
r.pop_front() -> ?T, r.pop_back() -> ?TRemoves from that end; null when empty
r.peek_front() -> ?*T, r.peek_back() -> ?*TThe element at that end, in place
r.get(i) -> TElement i, counted from the front
r.clear()Empties it
r.iter()Copies, front to back
tape
@tape { fixed.gen_ring(u8, 64); }

var rx: Ring_u8_64 = ring_u8_64_new();
fn on_byte(b: u8) {
    if (!rx.push_back(b)) { /* overrun: drop the byte */ }
}

FixedMap

FixedMap_K_V_N maps up to N keys to values, in N slots (N must be a power of two). Keys are integers, bool, enums, pointers or text ([]const u8, and t0’s string): text is compared by content and hashed with FNV-1a, and stored as the view, so the bytes must outlive the map.

FunctionDoes
fixed_map_K_V_N_new() -> FixedMap_K_V_NAn empty map
m.len(), m.cap(), m.is_empty(), m.is_full()
m.set(key, value) -> boolSets key; false when it’s new and the map is full
m.get(key) -> ?VA copy of the value; null when absent
m.get_ref(key) -> ?*const V, m.get_ptr(key) -> ?*VThe value in place
m.has(key) -> boolWhether key is there
m.remove(key) -> boolRemoves it; false when absent
m.clear()Empties it
m.iter()Copies of the entries (.key, .value), in slot order

A map that is nearly full gets slower (a lookup walks past more slots): give it room, twice the keys you expect is a good size. For other keys, gen_fixed_map_by(K, V, N, hash_fn, eq_fn) takes your hash fn(*const K) -> u64 and equality fn(*const K, *const K) -> bool; keys that are equal must hash the same.

Removing a key moves the entries after it, so a pointer from get_ptr is valid only until the next remove.

FixedPool

FixedPool_T_N is collections’ Pool in N inline slots: values reached by handles (FixedHandle_T_N, an index and a generation), so a removed value’s old handles find nothing.

tape
pub struct Enemy { x: i64; hp: i64; }

@tape { fixed.gen_fixed_pool(Enemy, 64); }    // FixedPool_Enemy_64, FixedHandle_Enemy_64

var enemies: FixedPool_Enemy_64 = fixed_pool_Enemy_64_new();
let h: ?FixedHandle_Enemy_64 = enemies.insert(Enemy { x: 0, hp: 3 });
if (h == null) { /* all 64 slots taken */ }
let orc: FixedHandle_Enemy_64 = h!;
enemies.remove(orc);
let gone: ?Enemy = enemies.get(orc);          // null: the handle is stale
FunctionDoes
fixed_pool_T_N_new() -> FixedPool_T_NAn empty pool
p.len(), p.cap(), p.is_empty(), p.is_full()
p.insert(item) -> ?FixedHandle_T_NStores the value, returns its handle; null when full
p.has(h) -> boolWhether h still names a value
p.get(h) -> ?TA copy of the value; null when h is stale
p.get_ref(h) -> ?*const T, p.get_ptr(h) -> ?*TThe value in place; null when stale
p.set(h, item) -> boolReplaces the value; false when h is stale
p.remove(h) -> ?TRemoves and returns the value; null when h is stale
p.clear()Removes every value; every handle becomes stale
p.iter()Copies of the live entries (.handle, .value), in slot order
p.refs(), p.refs_mut()The same with .value a pointer in place
fixed_handle_T_N_null()A handle that is never valid
h.is_null(), h.eq(other), h == otherWhether h is the null handle; whether two handles are the same (!= too)

The rules are Pool’s: freed slots are reused (last freed first) with a new generation, a slot whose generation would wrap is retired, iteration skips free slots. The slots never move, so a get_ptr pointer stays valid until that value is removed.

FixedSparseSet

FixedSparseSet_T_N is collections’ SparseSet for ids 0 to N - 1: values keyed by id, packed together. Pair it with a FixedPool of the same N and key it by the handle’s index.

FunctionDoes
fixed_sparse_set_T_N_new() -> FixedSparseSet_T_NAn empty set
s.len(), s.cap(), s.is_empty()
s.set(id: u32, value) -> boolSets id; false when id >= N
s.has(id), s.get(id) -> ?TWhether it’s there; a copy of its value
s.get_ref(id), s.get_ptr(id)The value in place
s.remove(id) -> ?TRemoves it; the last entry moves into its place
s.clear()Empties it
s.ids(), s.values(), s.values_mut()The packed ids and values, in place
s.iter()Copies of the entries (.id, .value)
tape
@tape {
    fixed.gen_fixed_pool(Enemy, 64);
    fixed.gen_fixed_sparse_set(i64, 64);        // FixedSparseSet_i64_64
}

var burning: FixedSparseSet_i64_64 = fixed_sparse_set_i64_64_new();
burning.set(orc.index, 5);                    // 5 ticks of fire for this enemy

List

List_T is a doubly linked list of nodes you own: it allocates nothing. A node type has a field link: fixed.Link, which the list uses to chain the nodes. A node is in at most one list at a time (per Link field), and must not move while it’s in one.

tape
pub struct Task {
    id: i64;
    link: fixed.Link;
}

@tape { fixed.gen_list(Task); }

var ready: List_Task = list_Task_new();
var a: Task = Task { id: 1, link: fixed.Link { prev: null, next: null } };
var b: Task = Task { id: 2, link: fixed.Link { prev: null, next: null } };
ready.push_back(&a);
ready.push_back(&b);
ready.remove(&a);                       // unlinked in constant time
let first: ?*Task = ready.first();      // &b
for (t in ready.iter()) { /* t: *Task */ }
FunctionDoes
list_T_new() -> List_TAn empty list
l.len(), l.is_empty()
l.push_back(n), l.push_front(n)Links node n in at that end
l.insert_after(at, n), l.insert_before(at, n)Links n in next to at, a node of the list
l.remove(n)Unlinks n; traps if n is plainly not in the list
l.pop_front() -> ?*T, l.pop_back() -> ?*TUnlinks and returns the node at that end
l.first() -> ?*T, l.last() -> ?*TThe node at that end
l.next(n) -> ?*T, l.prev(n) -> ?*TThe node after or before n
l.clear()Unlinks every node
l.iter()The nodes in place, first to last; removing the node just returned is fine

Generators

GeneratorMakes
gen_fixed_vec(T, N)FixedVec_T_N, fixed_vec_T_N_new
gen_ring(T, N)Ring_T_N, ring_T_N_new
gen_fixed_map(K, V, N)FixedMap_K_V_N, fixed_map_K_V_N_new (N a power of two)
gen_fixed_map_by(K, V, N, hash_fn, eq_fn)the same with your functions
gen_fixed_pool(T, N)FixedPool_T_N, FixedHandle_T_N, fixed_pool_T_N_new, fixed_handle_T_N_null
gen_fixed_sparse_set(T, N)FixedSparseSet_T_N, fixed_sparse_set_T_N_new
gen_list(T)List_T, list_T_new

Three helpers are public for other generators (collections builds its maps’ key handling on them):

HelperDoes
is_key(K) -> boolWhether K has a built-in hash and equality (integers, bool, enums, pointers, text)
emit_key_ops(K, LK, pre, text_hash)Emits pre_hash(key) and pre_eq(stored, key) for such a key, text hashed with your fn([]const u8) -> u64
emit_key_ops_by(K, pre, hash_fn, eq_fn)The same two through your hash and equality

Names follow the $T naming: a []const u8 key is cslice_u8 (FixedMap_cslice_u8_i64_16), and so is a string key in t0, where string is that view.

Last modified: