# Arena allocation

An arena (also called a region, a pool, or a bump allocator) is a block of memory that hands out many small allocations and frees them all together. Instead of calling `malloc`/`free` per object, you allocate objects into the arena as you build them, use them for as long as the arena is alive, and drop the entire region in one operation when you're done. The individual objects are never freed on their own.

The idea trades granularity for speed and simplicity. You give up the ability to free one object early, and in return you get very cheap allocation (often just bumping a pointer) and a single, cheap deallocation. It fits any workload with a clear phase boundary: parse a file into an AST, walk it, throw the whole AST away; render a frame, free everything at frame end; handle one request, reset the arena for the next one.

## How allocation works

The simplest arena is a bump allocator. It holds a buffer and an offset. Allocating `n` bytes returns the current offset and advances it by `n`. There is no free list, no size classes, no coalescing — allocation is a pointer add and a bounds check. When the buffer fills, the arena grabs another chunk (arenas are usually a linked list of chunks so that existing references stay valid; a `Vec`-backed arena that reallocates would invalidate them). Freeing means dropping every chunk, or resetting the offset to zero to reuse the space.

Because objects sharing an arena are laid out consecutively in memory, walking them tends to be cache-friendly — a traversal touches nearby addresses instead of chasing pointers scattered across the heap. That locality is often a bigger win than the cheaper allocation itself.

## References vs. indices

There are two common ways to refer to something stored in an arena, and the choice has real consequences.

Reference-handing arenas give you back a real pointer (`&T`) into the region. In Rust the [`typed_arena`](https://docs.rs/typed-arena/) crate works this way: `arena.alloc(value)` returns `&mut T`, and the borrow checker ties that reference's lifetime to the arena's, so you cannot hold a reference after the arena is dropped. This is ergonomic — you can pattern-match through the references directly, which Rust otherwise can't do across a `Box` in a recursive enum — but each reference is a full pointer (64 bits on most targets), and a whole tree of them shares one arena lifetime that then propagates through every function signature that touches it.

Index-handing arenas (the "generational arena" family, e.g. [`generational-arena`](https://docs.rs/generational-arena/) or `slotmap`) store objects in a backing `Vec` and hand out an integer index instead of a pointer. To dereference, you index back into the arena. Indices can be 32 or even 16 bits, so they pack more tightly than pointers — which matters when you have many small nodes. The "generational" part adds a generation counter to each slot so a stale index (pointing at a slot that was reused) can be detected instead of silently reading the wrong object.

## Why index-not-pointer sidesteps borrow-checker fights

The index approach is popular in Rust specifically because it dodges the borrow checker. A graph or doubly-linked structure built from `&T` references forces you to prove to the compiler that every reference outlives every use and that aliasing rules hold — which for cyclic or self-referential structures is often impossible without `unsafe`, `Rc<RefCell<T>>`, or `Pin`. If nodes instead live in a `Vec<Node>` and edges are `usize` indices, the references the borrow checker cares about are just the short-lived `&self.nodes[i]` you take at the moment of access. The graph's own structure carries no lifetimes. This is the same move [[safe-gc]] makes: route every access through indexing into a `Heap` rather than through a raw pointer, and whole categories of use-after-free and aliasing bugs leave the reachable state space. The cost is that a stale or wrong index is a logic bug rather than a compile error — generation counters exist to turn that back into a detectable runtime failure.

## Interning

An arena pairs naturally with interning: allocate-and-deduplicate. Before storing a value, hash it and check whether an equal one already lives in the arena; if so, hand back the existing handle. Now equal values share one allocation and equality is a handle comparison instead of a deep compare. rustc does this heavily — it interns types into an arena so that identical types are pointer-equal, which makes both type comparison and caching cheap, and it interns the HIR during AST lowering, handing out `HirId` handles. The Gleam pretty printer change below uses a lighter version: keywords and separators like `String("fn")` or the list comma are allocated once into the arena and referenced everywhere instead of being reboxed each time.

## Lifetime and deallocation

The defining constraint is that you cannot free one object early. Everything in the arena dies together. For phase-structured work this is a feature — you never leak and never double-free within a phase, and cleanup is O(1) chunks rather than O(n) objects. For long-lived structures with genuinely independent object lifetimes it's the wrong tool: an arena that only grows will hold memory until the phase ends, so a server that arena-allocates per connection must reset the arena between requests or it behaves like a leak. This is the opposite discipline from per-object schemes like [[raii]], where each object's destructor runs deterministically at its own scope exit, and from reclamation schemes like [[epoch-based-reclamation]] that exist precisely to free individual nodes safely under concurrency.

## Example: the Gleam pretty printer

[[gleam-pretty-printer-arenas]] is a concrete case. Gleam's formatter builds a recursive `Document` tree where nested variants were boxed on the heap (`Nest(Box<Self>)`). Switching those `Box`es to arena references (`Nest(&'doc Self)`) backed by `typed_arena` removed most of the per-node heap allocation and let repeated documents be cached once. The formatter got about 24% faster and peak memory dropped roughly 10%. The cost was mechanical: every function that built documents had to take the arena as an extra argument and thread its lifetime through.

Arenas show up across the language-implementation and systems world for the same reasons — bump allocation is fast, bulk free is trivial, and a manual-memory language like those in [[dayvster-manual-memory-management]] gets much of the safety benefit of automatic management within a bounded region. In concurrent settings, sharing an arena across threads runs into the usual [[rust-send-sync]] constraints: a single-threaded bump arena is neither `Send` nor `Sync` by default, and making one safe to share requires either synchronization or per-thread arenas.
