fixed
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); // 3fixed 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.
| Function | Does |
|---|---|
fixed_vec_T_N_new() -> FixedVec_T_N | An empty vector |
v.len(), v.cap() | Elements held, and N |
v.is_empty(), v.is_full() | |
v.push(item) -> bool | Appends; false when full |
v.pop() -> ?T | Removes and returns the last; null when empty |
v.get(i) -> T | A copy of element i |
v.get_ref(i) -> *const T, v.get_ptr(i) -> *T | Element i in place |
v.set(i, item) | Replaces element i |
v.insert(i, item) -> bool | Inserts before element i (i == len appends), moving the rest up; false when full |
v.remove(i) -> T | Removes element i, moving the rest down |
v.swap_remove(i) -> T | Removes 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() -> []T | The 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).
| Function | Does |
|---|---|
ring_T_N_new() -> Ring_T_N | An empty ring |
r.len(), r.cap(), r.is_empty(), r.is_full() | |
r.push_back(item) -> bool, r.push_front(item) -> bool | Adds at that end; false when full |
r.pop_front() -> ?T, r.pop_back() -> ?T | Removes from that end; null when empty |
r.peek_front() -> ?*T, r.peek_back() -> ?*T | The element at that end, in place |
r.get(i) -> T | Element i, counted from the front |
r.clear() | Empties it |
r.iter() | Copies, front to back |
@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.
| Function | Does |
|---|---|
fixed_map_K_V_N_new() -> FixedMap_K_V_N | An empty map |
m.len(), m.cap(), m.is_empty(), m.is_full() | |
m.set(key, value) -> bool | Sets key; false when it’s new and the map is full |
m.get(key) -> ?V | A copy of the value; null when absent |
m.get_ref(key) -> ?*const V, m.get_ptr(key) -> ?*V | The value in place |
m.has(key) -> bool | Whether key is there |
m.remove(key) -> bool | Removes 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.
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| Function | Does |
|---|---|
fixed_pool_T_N_new() -> FixedPool_T_N | An empty pool |
p.len(), p.cap(), p.is_empty(), p.is_full() | |
p.insert(item) -> ?FixedHandle_T_N | Stores the value, returns its handle; null when full |
p.has(h) -> bool | Whether h still names a value |
p.get(h) -> ?T | A copy of the value; null when h is stale |
p.get_ref(h) -> ?*const T, p.get_ptr(h) -> ?*T | The value in place; null when stale |
p.set(h, item) -> bool | Replaces the value; false when h is stale |
p.remove(h) -> ?T | Removes 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 == other | Whether 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.
| Function | Does |
|---|---|
fixed_sparse_set_T_N_new() -> FixedSparseSet_T_N | An empty set |
s.len(), s.cap(), s.is_empty() | |
s.set(id: u32, value) -> bool | Sets id; false when id >= N |
s.has(id), s.get(id) -> ?T | Whether it’s there; a copy of its value |
s.get_ref(id), s.get_ptr(id) | The value in place |
s.remove(id) -> ?T | Removes 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 {
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 enemyList
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.
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 */ }| Function | Does |
|---|---|
list_T_new() -> List_T | An 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() -> ?*T | Unlinks and returns the node at that end |
l.first() -> ?*T, l.last() -> ?*T | The node at that end |
l.next(n) -> ?*T, l.prev(n) -> ?*T | The 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
| Generator | Makes |
|---|---|
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):
| Helper | Does |
|---|---|
is_key(K) -> bool | Whether 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: