aeaether
$docs / more

Allocators

Aether's memory model is deterministic scope-exit cleanup with no garbage collector. Most code never thinks about an allocator: values are freed when they leave scope. std.alloc adds an explicit, swappable allocator for the cases that want to control where a data structure's memory comes from, a request-scoped arena, a fixed pool, or a leak-tracking wrapper in tests.

The convention (std.alloc)

An allocator is a plain handle (ptr), not an implicit ambient context. You pass it explicitly to the entry points that accept one. This keeps data flow visible and composes with the actor boundary: an actor can hold its own allocator in state without any hidden per-call parameter.

import std.alloc

a  = alloc.system()          // malloc/free; the default everywhere
p  = alloc.raw(a, 128)       // 128 bytes
p  = alloc.resize(a, p, 128, 256)
alloc.release(a, p, 256)
FunctionMeaning
alloc.system()The process-wide system allocator (malloc/realloc/free). Static, never freed.
alloc.raw(a, n)Allocate n bytes through a. Null on failure.
alloc.resize(a, p, old, n)Resize p from old to n bytes. n == 0 frees and returns null; a null p allocates.
alloc.release(a, p, n)Release a block. n is the size it was allocated with (a hint size-aware allocators use).

Arena-backed

alloc.of_arena turns an existing std.arena into an allocator: every allocation bump-allocates from the arena and individual frees are no-ops. Reclaim everything at once with arena.reset (O(1)); it is ideal for a request-scoped structure that is built up and thrown away as a unit.

import std.alloc
import std.arena

ar = arena.create(64 * 1024)
a  = alloc.of_arena(ar)
// ... build a big request-scoped structure through `a` ...
arena.reset(ar)              // frees everything the request allocated, at once
alloc.arena_free(a)          // frees the handle (not the arena)
arena.destroy(ar)

Collections that allocate through an allocator

Collection constructors gain an _in variant that takes an allocator; the default constructor is unchanged and uses the system path. The collection's own memory (its control struct and backing arrays) is then owned by the allocator, so an arena or a tracking wrapper manages the container. Element ownership is unchanged.

import std.list (list_new_in, add)
import std.arena
import std.alloc (of_arena)

ar = arena.create(1024 * 1024)
l  = list_new_in(of_arena(ar))   // the list lives in the arena
add(l, item)
// ...
arena.reset(ar)                  // frees the list and its array

std.list (list_new_in) is the first collection to adopt this. Others (map, bytes, strbuilder) follow the same shape.

Leak detection in tests (std.tracking)

Because there is no GC backstop, a missed release/free is a silent leak. std.tracking wraps any allocator, records every live allocation, and reports the outstanding set, turning a leak from something only a coarse external CI gate can catch into an ordinary in-test assertion.

import std.alloc
import std.tracking

t = tracking.wrap(alloc.system())

l = list_new_in(t)            // or alloc.raw(t, n), or any _in constructor
// ... exercise the code under test ...

if tracking.count(t) != 0 {   // 0 == no leaks
    tracking.report(t)        // prints each outstanding block (order + size)
}
tracking.destroy(t)
FunctionMeaning
tracking.wrap(inner)Wrap inner (e.g. alloc.system()); returns an allocator handle.
tracking.count(t)Live (allocated but not freed) block count.
tracking.bytes(t)Total live bytes.
tracking.report(t)Print each outstanding block to stderr; returns the live count.
tracking.destroy(t)Free the tracker. Does not free the leaked blocks (those are the leaks).

The tracker allocates its own bookkeeping with the system allocator, so it never counts itself. tracking.destroy deliberately does not free the still-live blocks: they are exactly the leaks the code under test failed to release, and freeing them would hide the bug.