Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Memory & lifetime

Coming from Rust / C++? No garbage collector, and no borrow checker either. Memory is region-based: every locus owns an arena, allocations inside it are bump-pointer cheap, and the whole region frees in one shot when the locus dissolves. The locus tree is the ownership graph — so lifetimes are structural, not annotated. You never write free, and you never fight a borrow checker, because no pointer ever crosses sideways.

You’ve used loci for pages without thinking about memory, because the model is automatic. Here’s what’s underneath.

A locus owns a region

Every locus has an arena — a region of memory. Everything the locus allocates (strings it builds, records it constructs, collection storage) comes from that arena. When the locus dissolves, the entire region is freed at once. There is no per-object deallocation, ever.

Regions nest exactly like loci do. A child’s region is a sub-region of its parent’s:

  root
  └── App's region
      └── Server's region
          ├── Conn A's region
          └── Conn B's region

When a locus dissolves, its whole subtree of regions frees wholesale. This is why shutdown cascades cleanly and why flow children reclaim per connection: freeing is structural, not traced.

Why no GC and no borrow checker

Both exist to answer one question — when is it safe to free this? Hale answers it structurally instead:

  • No pointer crosses sideways. Vertical-only flow means a value in one locus’s region is never referenced by a sibling. So when a region frees, nothing dangles into it.
  • Messages are copies, not pointers. A payload crossing a locus boundary is copied into the receiver’s arena. Sender and receiver have independent lifetimes; the sender can dissolve while the receiver still holds its copy.

With those two invariants, wholesale-free-at-dissolve is sound with no tracing and no aliasing analysis. The discipline the borrow checker enforces with annotations, Hale enforces with structure — you got it for free by building a locus tree.

Bounded storage: capacity slots

The arena is for transient, locus-lifetime allocation. When a locus needs bounded, disciplined storage — a recycling pool, a growable buffer — it declares capacity slots:

locus Router {
    capacity {
        heap routes  of Route;     // growable, individually freed
        pool sessions of Session;  // fixed-shape, recyclable cells
    }
}
  • heap X of T — growable storage, cells allocated and freed individually during the locus’s life, the whole slot reclaimed at dissolve.
  • pool Y of T — a bounded population of fixed-shape, recyclable cells (acquire / release).

The forms you’ve been using — @form(vec), @form(hashmap) — are built on exactly these slots; the form annotation just synthesizes the method surface over them. And for a list that belongs inside a value rather than on a locus, there’s bounded[T; N] (see Collections) — fixed-capacity, laid out inline, whole-struct copies carry it, and the memory-bound analysis treats it as bounded by construction. Slots hold values, never locus references: locus membership goes through accept, not storage.

Projection classes: committing to resolution

When a parent has many children, you can commit up front to the resolution at which it observes them — which lets the compiler pick the allocator that makes that resolution cheap:

locus WorkerPool : projection chunked {
    accept(w: Worker) { }
}
  • rich — a handful of named children (≈4–10), each fully observed. Per-child arenas, low churn.
  • chunked — moderate counts (≈10–30), observed in ranges. Per-child sub-regions with free-list reuse — the default when a locus accepts children.
  • recognition — large populations (≈100–500), observed in aggregate (a count, a histogram). Pre-allocated fixed pools.

The projection class changes the allocator strategy, not your code: the same parent and child methods read from a rich pool or a recognition pool unchanged. It’s a commitment about observation resolution; the compiler turns that into a layout.

Sizing is hints, lifetime is law

Declared sizes are hints — an arena that out-allocates its budget just adds another chunk; it doesn’t panic. The load-bearing property is lifetime: wholesale free at dissolve. That’s the contract every other guarantee leans on.

Next: keeping a long-running program’s memory flat — Performance.