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:
| Name | Kind | Purpose |
|---|---|---|
Allocator | interface | the shape every allocator satisfies: alloc + free |
OutOfMemory | enum | the single allocation failure; error half of every allocating signature |
new / delete | methods on Allocator | typed allocation and its counterpart |
systemAllocator | con of type Allocator | the platform heap, ready to use |
FixedBufferAllocator | struct | bump allocator over a buffer you supply |
ArenaAllocator | struct | bump allocator over a chunk pulled from a parent allocator |
copy, equal, compare, fill | functions | byte operations over [*]u8 |
transmuteTo | function | unchecked bit-level cast for pointer-shaped types |
offsetPtr | function | scaled pointer arithmetic on *T (planned — see below) |
WasmAllocator / LibcAllocator | struct (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:
enum OutOfMemory { OutOfMemory }
interface Allocator {
fn alloc(size: i32, align: i32) OutOfMemory![*]u8,
fn free(buf: [*]u8),
}| Method | Contract |
|---|---|
alloc(size, align) OutOfMemory![*]u8 | reserves 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):
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:
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:
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:
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:
| Signature | Returns |
|---|---|
fn (a: Allocator) new(known T: type) OutOfMemory!*mut T | one T: sizeof T bytes at alignof T |
fn (a: Allocator) new(known T: type, count: i32) OutOfMemory![*]T | count 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):
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
export con systemAllocator: AllocatorThe 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:
| Target | Backing type | alloc | free |
|---|---|---|---|
| wasm | WasmAllocator | bump pointer over linear memory, growing it in 64 KiB pages on demand | no-op — linear memory only grows; layer an arena or your own allocator for reclaim |
| native (libc) | LibcAllocator | malloc | free — the libc contract applies: only hand back buffers this allocator produced |
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.
| Member | Signature | Semantics |
|---|---|---|
new | fn (FixedBufferAllocator) new(buf: [*]u8) FixedBufferAllocator | wraps buf; the cursor starts at 0; buf's bytes are the entire capacity |
alloc | fn (a: *mut FixedBufferAllocator) alloc(size: i32, align: i32) OutOfMemory![*]u8 | rounds the cursor up to align, then bumps it by size; OutOfMemory when the aligned request doesn't fit |
free | fn (a: *mut FixedBufferAllocator) free(buf: [*]u8) | no-op — exists to satisfy Allocator |
reset | fn (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:
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.
| Member | Signature | Semantics |
|---|---|---|
new | fn (ArenaAllocator) new(parent: Allocator) ArenaAllocator | pulls one 4096-byte, 16-aligned chunk from parent; panics if the parent can't supply it — failure to construct an arena is unrecoverable |
alloc | fn (a: *mut ArenaAllocator) alloc(size: i32, align: i32) OutOfMemory![*]u8 | bumps a cursor inside the chunk; OutOfMemory when the chunk is full |
free | fn (a: *mut ArenaAllocator) free(buf: [*]u8) | no-op — storage is reclaimed wholesale by reset |
reset | fn (a: *mut ArenaAllocator) reset() | rewinds the cursor; every pointer handed out beforehand is dead from that call on |
allocator | fn (a: *mut ArenaAllocator) allocator() Allocator | this arena boxed as an Allocator — the handle to pass where one is expected |
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):
| Function | Signature | Semantics |
|---|---|---|
copy | fn 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 |
equal | fn equal(a: [*]u8, b: [*]u8) bool | true when the views have the same length and the same bytes |
compare | fn compare(a: [*]u8, b: [*]u8) i32 | memcmp ordering: < 0 / 0 / > 0 over the common prefix; a shorter view that prefixes the longer orders first |
fill | fn fill(dst: [*]u8, b: u8) | sets every byte of dst to b |
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).
| Form | Signature | Result |
|---|---|---|
| single | fn transmuteTo(known U: type, src: any) U | src's first sizeof U bytes relabelled as U |
| array | fn transmuteTo(known U: type, src: any, count: i32) U | a [*]T whose .ptr is src's pointer and .len is count |
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.