mem

mem is the heap: raw blocks of memory for buffers you hand to C, to t0 code, or manage yourself. Most code never needs it: strings and the containers collections generates allocate and free by themselves.

tape
import mem from "mem";

The module is @profile(t1) (it reaches the runtime library’s memory primitives) and pure.

Example

tape
import mem from "mem";

fn checksum(n: i64) -> i64 or mem.MemError {
    let buf: *u8 = mem.alloc(n) or return;      // zeroed
    defer mem.free(buf, n);
    for (i in 0..n) { buf[i] = (i % 251) as u8; }
    var sum: i64 = 0;
    for (i in 0..n) { sum = sum + buf[i] as i64; }
    return sum;
}

pub fn main() -> i32 {
    let s = checksum(1000) or { return 1; };
    return (s % 256) as i32;
}

Running out of memory

alloc and realloc return MemError.OutOfMemory when there is no memory; handle it like any error (or return, or { ... }). When a program has no better answer than stopping, alloc_or_trap does that, with the line every allocation the compiler makes uses:

plaintext
trap: out of memory

and exit code 101, in the VM and natively. The standard library’s own internals use alloc_or_trap; its functions that already return an error pass running out of memory on as theirs (gfx.new_surface gives GfxError.OutOfMemory).

Functions

FunctionSignatureDescription
alloc(size: i64) -> *u8 or MemErrorsize zeroed bytes, 16-byte aligned. A size of 0 or less is null.
alloc_or_trap(size: i64) -> *u8alloc, trapping with trap: out of memory instead of returning the error.
realloc(p: *u8, old: i64, new: i64) -> *u8 or MemErrorA block of new bytes holding the first min(old, new) bytes of p (the rest zeroed); p is freed. A null p is alloc(new); a new of 0 or less frees p and is null. On OutOfMemory, p is left as it was.
free(p: *u8, size: i64)Give back a block alloc or realloc returned. size is a hint the runtime may ignore: pass 0 when you don’t know it. A null p is nothing.
heap_arena(size: i64) -> arena.Arena or MemErrorAn arena.Arena over size bytes of heap.
free_arena(a: *arena.Arena)Free a heap_arena’s memory; the arena is empty afterwards.
live_allocs() -> i64Heap blocks currently allocated, string buffers included.
alloc_count() -> i64Heap blocks allocated so far, freed or not, string buffers included. Both counts may be read from any thread.
copy_raw(dst: *u8, src: *const u8, n: i64)Copy n bytes (not overlapping).
move_raw(dst: *u8, src: *const u8, n: i64)Copy n bytes that may overlap.
set_raw(dst: *u8, val: u8, n: i64)Fill n bytes with val.

The copies and fills are the builtins @move_bytes and @fill_bytes: the machine’s block move, many gigabytes a second, not a byte loop. A length of 0 or less does nothing. | zero_raw | (dst: *u8, n: i64) | Zero n bytes. | | copy, move, set, zero | (dst: *u8, src: *u8, count: u64), … | The same with a u64 count. | | compare | (a: *u8, b: *u8, count: u64) -> i32 | -1, 0 or 1, by the first differing byte. | | equal | (a: *u8, b: *u8, count: u64) -> bool | Byte equality. |

With slices, use bytes (copy, fill, compare, …) instead of the raw forms.

Checking for leaks

live_allocs() counts the heap blocks currently allocated. Compare it before and after a piece of code: if it grew, something leaked; if it shrank, something was freed twice.

tape
let before: i64 = mem.live_allocs();
work();
@assert(mem.live_allocs() == before);

alloc_count() counts every block allocated so far, freed or not: the difference across a piece of code is how many allocations it made, which finds the stage of a program that allocates most.

tape
let start: i64 = mem.alloc_count();
parse(text);
io.println("parse: {mem.alloc_count() - start} allocations");

Last modified: