Docs · Standard library

`@mem`

@mem is the memory-management package: the Allocator interface, the process-wide systemAllocator, two concrete allocators (FixedBufferAllocator, ArenaAllocator), typed allocation (new / delete), byte operations over [*]u8, and the unchecked-reinterpretation escape hatch transmuteTo. For the tutorial treatment — why allocation is a value you pass around, and how allocators interact with pointer stability — read guide 09 — Pointers and memory.

Exports at a glance:

NameKindPurpose
Allocatorinterfacethe shape every allocator satisfies: alloc + free
OutOfMemoryenumthe single allocation failure; error half of every allocating signature
new / deletemethods on Allocatortyped allocation and its counterpart
systemAllocatorcon of type Allocatorthe platform heap, ready to use
FixedBufferAllocatorstructbump allocator over a buffer you supply
ArenaAllocatorstructbump allocator over a chunk pulled from a parent allocator
copy, equal, compare, fillfunctionsbyte operations over [*]u8
transmuteTofunctionunchecked bit-level cast for pointer-shaped types
offsetPtrfunctionscaled pointer arithmetic on *T (planned — see below)
WasmAllocator / LibcAllocatorstruct (per target)the concrete type behind systemAllocator; import systemAllocator instead

The Allocator interface

Every allocator is a value satisfying one two-method interface. It is an ordinary Vyi interface (guide 12) — any type with matching alloc and free methods boxes into it, no registration step:

vyi
enum OutOfMemory { OutOfMemory }

interface Allocator {
    fn alloc(size: i32, align: i32) OutOfMemory![*]u8,
    fn free(buf: [*]u8),
}
MethodContract
alloc(size, align) OutOfMemory![*]u8reserves size bytes aligned to at least align and returns a [*]u8 view whose .len equals size — or Err(OutOfMemory). Never a null, never a valid-looking empty slice: the failure is in the type.
free(buf)returns a buffer previously handed out by alloc (the same [*]u8). Bump-style allocators make it a no-op — they reclaim wholesale via reset.

align is threaded through so bump allocators can round their cursor up before handing out storage; new (below) passes alignof T automatically. sizeof and alignof are ordinary prefix operators you can use in your own size arithmetic.

Functions that allocate take an Allocator parameter; the failure propagates like any other error union (guide 07):

vyi
import { Allocator, OutOfMemory } from "@mem"

fn makeBuffer(alloc: Allocator, n: i32) OutOfMemory![*]u8 {
    con buf = try alloc.alloc(n, 1)
    return Ok(buf)
}

The result of alloc is an error union, not a buffer — you must try, catch, or switch before touching the bytes:

vyi-error
import { systemAllocator } from "@mem"

fn main() !u8 {
    con buf: [*]u8 = systemAllocator.alloc(8, 1)   // error: OutOfMemory![*]u8 is not [*]u8
    return Ok(0)
}

OutOfMemory

OutOfMemory is a single-variant enum — the only way an allocation fails. It carries an @errmsg method returning "out of memory", so it flows into the default !T error channel: try on an allocation works directly inside fn main() !u8 or any other bare-!T function.

Writing your own allocator

Implement alloc and free on your type and it satisfies Allocator; new and delete come for free because they are methods on Allocator values, not on any concrete type:

vyi
import { Allocator, OutOfMemory } from "@mem"

struct NullAllocator { }

fn (a: *mut NullAllocator) alloc(size: i32, align: i32) OutOfMemory![*]u8 {
    return Err(OutOfMemory.OutOfMemory)
}

fn (a: *mut NullAllocator) free(buf: [*]u8) { }

fn main() !u8 {
    mut na = NullAllocator{ }
    con alloc: Allocator = &na
    con r = alloc.new(i32) catch { return Ok(1) }   // always the catch arm
    return Ok(0)
}

Both methods are required — a type with only alloc does not box:

vyi-error
import { Allocator, OutOfMemory } from "@mem"

struct HalfAllocator { }

fn (a: *mut HalfAllocator) alloc(size: i32, align: i32) OutOfMemory![*]u8 {
    return Err(OutOfMemory.OutOfMemory)
}

fn main() !u8 {
    mut h = HalfAllocator{ }
    con alloc: Allocator = &h    // error: HalfAllocator has no `free`
    return Ok(0)
}

(Allocator implementations vouch for their storage's lifetime with @intrinsics.markStablePtr; code written against Allocator never touches that intrinsic. See stability and escape.)

Typed allocation: new and delete

Raw bytes are rarely the goal. new computes size and alignment from a type and returns typed storage; both forms propagate OutOfMemory:

SignatureReturns
fn (a: Allocator) new(known T: type) OutOfMemory!*mut Tone T: sizeof T bytes at alignof T
fn (a: Allocator) new(known T: type, count: i32) OutOfMemory![*]Tcount elements: count * sizeof T bytes at alignof T; the length lives in the returned value
fn (a: Allocator) delete(known T: type, ptr: [*]T)returns an array-form buffer to its allocator; T is inferred from the buffer, so call it as alloc.delete(buf)

The two new forms are overloads picked by argument shape. T is a known parameter — a compile-time type argument (guide 11):

vyi
import { systemAllocator } from "@mem"
import { stdout } from "@io"

fn main() !u8 {
    con nums = try systemAllocator.new(i32, 4)   // [*]i32 of 4 elements
    defer systemAllocator.delete(nums)
    nums[0] = 7

    con one = try systemAllocator.new(i32)       // *mut i32
    *one = 35
    stdout.writeInt(i64(nums[0] + *one))
    stdout.write("
")
    return Ok(0)
}

delete pairs with the array form: it rebuilds the byte view alloc handed out (ptr.len * sizeof T bytes at the same address) and passes it to free. The single-element form has no typed counterpart — a lone *mut T is typically arena- or fixed-buffer-backed and reclaimed wholesale by reset.

When out-of-memory is genuinely unrecoverable at a call site, say so explicitly — catch { panic "out of memory" } — rather than pretending the failure can't happen.

systemAllocator

yaml
export con systemAllocator: Allocator

The process-wide system allocator, already boxed as an Allocator — pass it anywhere one is expected. The concrete type behind it is selected per target; programs import systemAllocator and never need to name it:

TargetBacking typeallocfree
wasmWasmAllocatorbump pointer over linear memory, growing it in 64 KiB pages on demandno-op — linear memory only grows; layer an arena or your own allocator for reclaim
native (libc)LibcAllocatormallocfree — the libc contract applies: only hand back buffers this allocator produced
vyi
import { systemAllocator } from "@mem"
import { stdout } from "@io"

fn main() !u8 {
    con buf = try systemAllocator.alloc(64, 8)
    defer systemAllocator.free(buf)
    buf[0] = 42
    stdout.writeInt(i64(buf[0]))
    stdout.write("
")
    return Ok(0)
}

On native targets alloc relies on malloc's natural alignment guarantee (suitable for any scalar, which covers the standard library); honouring over-aligned requests there is (planned).

FixedBufferAllocator

A bump allocator over a [*]u8 you supply — a stack array, a static region, an embedded buffer. It performs no allocation of its own and never grows: when the buffer is full, alloc returns OutOfMemory.

MemberSignatureSemantics
newfn (FixedBufferAllocator) new(buf: [*]u8) FixedBufferAllocatorwraps buf; the cursor starts at 0; buf's bytes are the entire capacity
allocfn (a: *mut FixedBufferAllocator) alloc(size: i32, align: i32) OutOfMemory![*]u8rounds the cursor up to align, then bumps it by size; OutOfMemory when the aligned request doesn't fit
freefn (a: *mut FixedBufferAllocator) free(buf: [*]u8)no-op — exists to satisfy Allocator
resetfn (a: *mut FixedBufferAllocator) reset()rewinds the cursor to 0; every pointer handed out beforehand is dead from that call on

The buffer's lifetime governs every pointer the allocator hands out. This is the zero-heap path — heap-designed structures live in storage you already own:

vyi
import { Allocator, FixedBufferAllocator } from "@mem"
import { stdout } from "@io"

fn main() !u8 {
    mut buf = [64]u8[]                            // 64 bytes on the stack
    mut fba = FixedBufferAllocator.new([*]u8(&buf))
    con alloc: Allocator = &fba

    con a = alloc.new(i32, 4) catch { panic "out of memory" }
    a[0] = 42
    stdout.writeInt(i64(a[0]))
    stdout.write("
")
    return Ok(0)
}

ArenaAllocator

The same bump strategy, but the backing buffer is pulled from a parent allocator at construction instead of supplied by the caller. The right default for phase-shaped work: build, use, throw everything away at once.

MemberSignatureSemantics
newfn (ArenaAllocator) new(parent: Allocator) ArenaAllocatorpulls one 4096-byte, 16-aligned chunk from parent; panics if the parent can't supply it — failure to construct an arena is unrecoverable
allocfn (a: *mut ArenaAllocator) alloc(size: i32, align: i32) OutOfMemory![*]u8bumps a cursor inside the chunk; OutOfMemory when the chunk is full
freefn (a: *mut ArenaAllocator) free(buf: [*]u8)no-op — storage is reclaimed wholesale by reset
resetfn (a: *mut ArenaAllocator) reset()rewinds the cursor; every pointer handed out beforehand is dead from that call on
allocatorfn (a: *mut ArenaAllocator) allocator() Allocatorthis arena boxed as an Allocator — the handle to pass where one is expected
vyi
import { ArenaAllocator, systemAllocator } from "@mem"
import { stdout } from "@io"

fn main() !u8 {
    mut arena = ArenaAllocator.new(systemAllocator)
    con alloc = arena.allocator()

    con a = alloc.new(u8, 32) catch { panic "out of memory" }
    a[0] = 1
    arena.reset()                    // everything from the arena, gone at once
    con b = alloc.new(u8, 32) catch { panic "out of memory" }
    stdout.writeInt(i64(i32(b.ptr) - i32(a.ptr)))   // 0 — the chunk is reused from the start
    stdout.write("
")
    return Ok(0)
}

The parent's lifetime governs the arena's. The compiler does not track use-after-reset — dead pointers are the arena's contract with its user, stated here and in the memory model. The arena holds a single fixed-size chunk; chunked automatic growth (fresh chunks linked on demand so existing pointers stay live) is (planned).

Byte operations

Four functions over [*]u8 views, with identical semantics on every target (on libc targets they lower to the C library's memmove / memcmp / memset):

FunctionSignatureSemantics
copyfn copy(dst: [*]u8, src: [*]u8)copies src.len bytes into the front of dst. Overlap-safe (memmove semantics). Panics when dst.len < src.len — silent truncation hides bugs
equalfn equal(a: [*]u8, b: [*]u8) booltrue when the views have the same length and the same bytes
comparefn compare(a: [*]u8, b: [*]u8) i32memcmp ordering: < 0 / 0 / > 0 over the common prefix; a shorter view that prefixes the longer orders first
fillfn fill(dst: [*]u8, b: u8)sets every byte of dst to b
vyi
import { copy, equal, compare, fill } from "@mem"

fn main() !u8 {
    mut store = [8]u8[]
    con view = [*]u8(&store)

    fill(view, u8('x'))            // xxxxxxxx
    copy(view[..3], "abc")         // abcxxxxx

    con isEq = equal(view[..3], "abc")     // true
    con ord  = compare(view[..3], "abd")   // < 0
    if isEq && ord < 0 {
        return Ok(0)
    }
    return Ok(1)
}

Reinterpretation: transmuteTo

An unchecked bit-level cast for pointer-shaped types — the documented trust boundary for viewing one pointer type as another. Same address, different label; the compiler checks only that the target is no wider than the source, not that the bits form a valid value. Use it when the layout equivalence is established outside the type system (it is how new carves a *T out of a fresh [*]u8).

FormSignatureResult
singlefn transmuteTo(known U: type, src: any) Usrc's first sizeof U bytes relabelled as U
arrayfn transmuteTo(known U: type, src: any, count: i32) Ua [*]T whose .ptr is src's pointer and .len is count
vyi
import { systemAllocator, transmuteTo } from "@mem"
import { stdout } from "@io"

fn main() !u8 {
    con nums = try systemAllocator.new(i32, 4)   // [*]i32, 4 elements
    nums[0] = 7

    con bytes = transmuteTo([*]u8, nums, 16)     // same storage, viewed as 16 bytes
    stdout.writeInt(i64(bytes[0]))                          // 7 — low byte of nums[0]
    stdout.write("
")
    return Ok(0)
}

Pointer stability survives the cast: a transmute relabels a pointer's type but never moves it, so a stable source yields a stable result.

offsetPtr(T, p, i) — a *T pointing i * sizeof T bytes past p, the named escape hatch for arithmetic that *T otherwise forbids — is exported alongside transmuteTo; resolving calls to it is (planned). Walk memory with [*]T indexing and slicing instead.