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 [*mut]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 | error | 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 |
Buffer | struct | owned bytes: a growable in-memory Writer/Reader and the extraction point |
copy, equal, compare, fill | functions | byte operations over [*mut]u8 / [*con]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![*mut]u8,
fn free(buf: [*mut]u8),
}| Method | Contract |
|---|---|
alloc(size, align) OutOfMemory![*mut]u8 | reserves size bytes aligned to at least align and returns a [*mut]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 [*mut]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![*mut]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: [*mut]u8 = systemAllocator.alloc(8, 1) // error: OutOfMemory![*mut]u8 is not [*mut]u8
return Ok(0)
}OutOfMemory
OutOfMemory is a single-member error declaration
— the only way an allocation fails. Its intrinsic message is
"out of memory", and it coerces into the open error channel: try on an
allocation works directly inside fn main() !u8 or any other bare-!T
function, and an escaped failure prints error: out of memory.
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![*mut]u8 {
return Err(OutOfMemory.OutOfMemory)
}
fn (a: *mut NullAllocator) free(buf: [*mut]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![*mut]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 the
:<markStablePtr() decorator on the binding that holds the outgoing pointer;
code written against Allocator never touches it. 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![*mut]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: [*mut]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) // [*mut]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("\n")
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 { stdout } from "@io"
import { systemAllocator } from "@mem"
fn main() !u8 {
con buf = try systemAllocator.alloc(64, 8)
defer systemAllocator.free(buf)
buf[0] = 42
stdout.writeInt(i64(buf[0]))
stdout.write("\n")
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 [*mut]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: [*mut]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![*mut]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: [*mut]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([*mut]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("\n")
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![*mut]u8 | bumps a cursor inside the chunk; OutOfMemory when the chunk is full |
free | fn (a: *mut ArenaAllocator) free(buf: [*mut]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("\n")
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).
Buffer
Buffer is owned bytes — the standard library's one materialization
point. Everything else writes lazily into a sink; a Buffer is where bytes
actually accumulate, growing through the Allocator it was built with
(allocate-new + copy + free-old — allocators themselves are never resizable).
It satisfies both of @io's interfaces
(Writer and Reader), so anything
that renders onto a Writer can render into memory.
Two constructors define it, two extractions empty it:
| Member | Signature | Semantics |
|---|---|---|
fromAlloc | fn (Buffer) fromAlloc(a: Allocator) Buffer | an empty, growable write sink over a; a fixed-capacity sink is fromAlloc over a FixedBufferAllocator |
fromBytes | fn (Buffer) fromBytes(src: [*con]u8) Buffer | a read-only source over existing bytes; shares storage with the caller |
write | fn (b: *mut Buffer) write(buf: [*con]u8) error!i32 | appends all of buf, growing as needed; raises OutOfMemory when growth fails; panics on a sealed or read-only buffer (a logic bug, not an error) |
read | fn (b: *mut Buffer) read(buf: [*mut]u8) error!i32 | drains from an internal cursor; Ok(0) at the end of the written region — the same contract stdin.read has |
bytes | fn (b: Buffer) bytes(a: Allocator) OutOfMemory![*con]u8 | the copying extraction: a fresh, exact-sized, caller-owned copy; the buffer stays usable |
takeBytes | fn (b: *mut Buffer) takeBytes() [*mut]u8 | the consuming extraction: zero-copy transfer of the backing bytes; the buffer seals |
asBytes | fn (b: Buffer) asBytes() [*con]u8 | zero-copy view of the written region; valid until the buffer grows or is freed |
len | pub len: i32 / fn (b: Buffer) len() i32 | bytes written so far (fromAlloc) / source length (fromBytes) |
deinit | fn (b: *mut Buffer) deinit() | returns the backing storage to the allocator; a no-op when read-only (nothing owned) or sealed (ownership transferred) |
Build, extract a copy, then hand the storage off:
import { Buffer, ArenaAllocator, systemAllocator } from "@mem"
import { stdout } from "@io"
fn main() !u8 {
mut arena = ArenaAllocator.new(systemAllocator)
con alloc = arena.allocator()
mut b = Buffer.fromAlloc(alloc)
try b.write("hello")
try b.write(" world")
con copied = b.bytes(alloc) catch { panic "out of memory" }
try stdout.write(copied) // "hello world"; b is still usable
con taken = b.takeBytes() // zero-copy: the backing bytes, sealed
return Ok(u8(taken.len - 11))
}Consume means seal, not move. After takeBytes() the caller owns the
returned {ptr, len} run (freed through the allocator the buffer was built
with) and the buffer relinquishes it: deinit becomes a no-op — no double
free — and any later write is a use-after-free-class logic bug, so it
panics rather than raising an error:
import { Buffer, ArenaAllocator, systemAllocator } from "@mem"
fn main() !u8 {
mut arena = ArenaAllocator.new(systemAllocator)
mut b = Buffer.fromAlloc(arena.allocator())
try b.write("x")
con taken = b.takeBytes()
try b.write("y") // panics: "buffer: write after takeBytes"
return Ok(u8(taken.len))
}fromBytes sources are read-only: write and takeBytes on one panic (a
view owns nothing to append to or hand off), while read drains it exactly
like a stream:
import { Buffer } from "@mem"
import { systemAllocator } from "@mem"
fn main() !u8 {
mut src = Buffer.fromBytes("hello world")
con chunk = try systemAllocator.alloc(4, 1)
mut total = 0
mut n = try src.read(chunk)
for n > 0 {
total = total + n
n = try src.read(chunk) // Ok(0) at the end
}
return Ok(u8(total - 11))
}One representation covers both modes — read-only and sealed are ordinary
fields, never layout variants. There is no resize on allocators and none is
needed here: a Buffer composes growth out of alloc/copy/free, which is
the allocator model working as designed.
Byte operations
Four functions over [*mut]u8 / [*con]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: [*mut]u8, src: [*con]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: [*con]u8, b: [*con]u8) bool | true when the views have the same length and the same bytes |
compare | fn compare(a: [*con]u8, b: [*con]u8) i32 | memcmp ordering: < 0 / 0 / > 0 over the common prefix; a shorter view that prefixes the longer orders first |
fill | fn fill(dst: [*mut]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 = [*mut]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 [*mut]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 [*mut]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) // [*mut]i32, 4 elements
nums[0] = 7
con bytes = transmuteTo([*mut]u8, nums, 16) // same storage, viewed as 16 bytes
stdout.writeInt(i64(bytes[0])) // 7 — low byte of nums[0]
stdout.write("\n")
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 [*mut]T indexing and slicing instead.