Skip to content

The book · 08

Memory

book.md · 88 lines · 2 min read

Goal: understand why you almost never think about memory (colorless + GC default), and how to go down to the metal (@mm, @transfer/@promote) when you need to.

Allocation is colorless: each object carries an implicit pointer to its MemoryManager, and the runtime calls the interface without knowing which strategy is underneath. With the default GC, you write normal code and never touch @mm:

fn build() -> Report {
r := Report.new() // allocates through the context's @mm (GC, by default)
r.add(linha)
return r // no manual free; the GC handles it
}

Mutability follows chapter 01: var mutates, immutable aliases for free (shared without a copy). To mutate the caller’s value, use mut (write-back, inout style):

fn accumulate(total: mut int, values: []int) {
loop v in values { total += v }
}
var soma := 0
accumulate(mut soma, dados) // 'mut' at the call site; the target has to be 'var'

A *T pointer (a single type, no *const/*mut) serves to hold a persistent reference (graphs, FFI); it is born from &place with identity on the heap, and deref is explicit (*p). Collections use [*]T (many-pointer) as backing, something you rarely see directly.

@mm defines the allocator of the context (process, function, or object). The strategies: gc (default), arena (frees in a block), none (manual, C style), c (memory coming from C).

fn handle(req: Request) -> Response {
arena := Arena.new()
defer arena.free_all() // everything from here dies all at once
@mm(arena) {
... // allocations in this block use the arena
}
}

Under arena/none you own the lifetime (unsafe territory); the GC is the safety net by construction.

Moving between processes: @transfer / @promote

Section titled “Moving between processes: @transfer / @promote”

Since processes do not share memory, passing data from one to another is explicit. @transfer moves (invalidates the source; the object travels with its @mm intact); @promote(mm) copies to another MemoryManager:

@mm(arena) spawn producer(data @transfer) // moves; 'data' is invalid here
@mm(gc) spawn consumer(chan)
result @promote(gc, deep) // copies the subgraph to the GC (cuts the cord with the arena)

(A plain copy is the unmarked default; there is no @copy.)

@promote copies the object at the pointer. A gc block carries its length in its header, so promoting a gc [*]T copies the whole block, however many elements it holds. A raw [*]T (a non-gc many-pointer) carries no length – like a C pointer – so it promotes as a single object. To promote a multi-element collection out of a non-gc region, promote a length-carrying value (a List, or a gc block) rather than a raw many-pointer.

OOM is a process event, not a per-call error

Section titled “OOM is a process event, not a per-call error”

The high-level conveniences (List.push, string.append, encode) do not thread OOM, because that would be coloring. The rule is BEAM’s: the allocation that fails kills the process, and the supervisor handles it (ch. 07). The recoverable case with a budget (an arena with a ceiling, a @limit) drops down to mem.alloc, which returns Result[Ptr, error{OOM}]:

@limit(16mb)
spawn worker(data) // blew the ceiling → an OOM error becomes the supervisor's |e|

Two worlds without conflict: high-level colorless that crashes (99% of the code), and low-level with explicit Result (an allocator author, OOM with a budget).

Next: 09 · Network channels