arena

arena hands out blocks of memory from a buffer you provide: a global array, a stack buffer, a kernel page. It never touches the heap or the OS, so it works in every profile, t0 included. Allocation is a pointer bump; freeing is all at once (reset) or back to a saved position (release).

tape
import arena from "arena";

The module is @profile(t0) and pure: no imports, no I/O, no global state.

Example

tape
@profile(t0);
import arena from "arena";

struct Point { x: i64; y: i64; }

var memory: [4096]u8;

pub fn main() -> i32 {
    var a: arena.Arena = arena.over(memory[..]);

    let p: ?*u8 = a.alloc(16, 8);           // room for one Point
    if (p == null) { return 1; }
    let pt: *Point = p! as *Point;
    pt.x = 3;
    pt.y = 4;

    let m: i64 = a.mark();
    let scratch: *u8 = a.alloc(1000, 1)!;   // temporary work space
    scratch[0] = 1;
    a.release(m);                           // scratch is gone, pt stays

    return (pt.x + pt.y) as i32;            // 7
}

A t1 program uses it the same way. Pass *arena.Arena to the functions that allocate, so they all bump the same cursor:

tape
@profile(t1);
import io from "io";
import arena from "arena";

var scratch: [1024]u8;

// Takes `n` bytes from the arena, or reports that it is full.
fn take(a: *arena.Arena, n: i64) -> bool {
    let p: ?*u8 = a.alloc(n, 8);
    if (p == null) { return false; }
    let bytes: *u8 = p!;
    for (i in 0..n) { bytes[i] = 0; }
    return true;
}

pub fn main() -> i32 {
    var a: arena.Arena = arena.over(scratch[..]);
    for (round in 0..3) {
        let m: i64 = a.mark();
        var blocks: i64 = 0;
        while (take(&a, 100)) { blocks = blocks + 1; }
        io.println("round {round}: {blocks} blocks");   // 9 blocks each round
        a.release(m);
    }
    return 0;
}

Arena

tape
pub struct Arena {
    base: *u8;   // start of your memory
    size: i64;   // its length in bytes
    used: i64;   // bytes handed out so far, padding included
}

You can read the fields: a.used is what is in use, a.size - a.used what is left. Don’t write them; the functions keep 0 <= used <= size.

An Arena is a value. Copying it copies the cursor, not the memory, so two copies would hand out the same bytes. Keep one per buffer and pass a pointer to it.

arena.over(buf: []u8) -> Arena

An empty arena over buf. The arena borrows buf: the memory has to outlive every pointer the arena hands out. Over an empty buf the arena gives nothing: every alloc returns null.

tape
var memory: [4096]u8;
var a: arena.Arena = arena.over(memory[..]);

a.alloc(size: i64, align: i64) -> ?*u8

size bytes at an address that is a multiple of align, or null when the rest of the buffer is too small. A null result leaves the arena unchanged, so you can try a smaller request.

  • The bytes are not zeroed.
  • Padding to reach the alignment counts as used. The address is aligned, so a buffer that doesn’t start aligned still gives aligned blocks.
  • A request for 0 bytes succeeds while the aligned position is inside the buffer or at its end. Don’t dereference the pointer it returns.
  • Traps if size is negative or align is not a power of two.
tape
let p: ?*u8 = a.alloc(64, 16);
if (p == null) { return 1; }            // out of space
let block: *u8 = p!;

a.reset()

Frees everything: the whole buffer is available again. Pointers you got before must no longer be used.

a.mark() -> i64

The current position, to pass to release later.

a.release(mark: i64)

Frees everything allocated since mark was taken; blocks from before stay. Marks nest like a stack: releasing to an older mark frees what came after the newer ones too.

tape
let m: i64 = a.mark();
let tmp: *u8 = a.alloc(256, 1)!;
// ... use tmp ...
a.release(m);                           // tmp is freed

Traps if mark is negative or beyond the current position: a mark taken after one you already released, or before a reset.

Alignment helpers

arena.align_up(x: i64, align: i64) -> i64

x rounded up to the next multiple of align (x itself if it already is one). Traps if align is not a power of two.

tape
arena.align_up(13, 8)     // 16
arena.align_up(16, 8)     // 16

arena.is_aligned(x: i64, align: i64) -> bool

Whether x is a multiple of align. Traps if align is not a power of two.

tape
arena.is_aligned(64, 16)  // true
arena.is_aligned(6, 4)    // false

Traps

Each trap is a broken contract, a mistake in the calling code, and stops the program with trap: <message> at <file>:<line>:<col> (exit code 101):

MessageCause
arena.align_up: align is not a power of twoalign is 0, negative, or not 1, 2, 4, 8, …
arena.is_aligned: align is not a power of twoas above
arena.alloc: align is not a power of twoas above
arena.alloc: negative sizesize < 0
arena.release: mark is beyond the current positionmark < 0 or mark > used

Running out of space is not a trap: alloc returns null.

For heap memory in t1 code, use mem.

Last modified: