View Types: A Hard Problem That a Region Makes Easy

A value whose fields point into its own other fields — a document holding slices into its own text — is one of the genuinely hard problems in language design; Rust needs Pin and unsafe, others reach for dependent types. Mere solves it with three small rules, and the reason it can is that the region built in the last two episodes was already doing the hard part.

merememory-modelview-typesregionslanguage-design

Both previous episodes ended pointing at the same deferred question: what does it mean to hold a reference into a region, and how do you stop it from outliving what it points at? The reason it kept getting deferred is that, underneath, it is one of the genuinely hard problems in language design — and it is worth seeing why before seeing how small Mere’s answer turns out to be.

The self-referential value

Consider a document you parse once and then keep, holding on to slices of its own text:

struct Document { text: owned String }

// wanted: parse text once, keep the tokens as slices into it
view DocumentView of Document {
  tokens: Slice<&self.text, str>,   // points inside self.text
}

tokens points into Document.text. This is exactly the shape a parser wants — tokenize the buffer, then carry around cheap slices instead of copying substrings — and it is a minefield. The moment the Document moves, text sits at a new address and every slice in tokens is dangling. The type system also has to be able to say “a reference to another field of the same value,” and construction has to build text first and the slices second, and teardown has to drop the slices before the text or it passes through an invalid state.

This is not a corner case; it is a famous one. Rust pins the value in place with Pin<T> and requires unsafe to actually form the self-reference. Languages with dependent types can express it exactly, at a cost most programs shouldn’t pay. The problem is real, and the usual solutions are heavy.

Three axioms

Mere’s answer is deliberately small — three rules, and no unsafe:

A — a view is an immutable bundle. A view fixes its internal reference relationships at construction time. After that it cannot be mutated and it cannot be moved; it is used only through immutable references. Nothing can change the address of a field, so no slice into it can go stale.

B — a view is built inside a region. A view lives on a region, not on some free-floating heap. Its lifetime is the region’s lifetime, and because every field points into that same region, all the field-to-field references are internal to one area with a single fate.

C — view V[R] of T is real syntax. A view is declared as a derived form of a struct, parameterized by the region it lives in. The parameter R is how the type system says, out loud, “all of this is inside region R” — the constraint is in the type, not in your head.

Written out, a view and its constructor are ordinary code:

view DocumentView[region R] of Document {
  own:    &R Document,        // the Document itself, placed in region R
  tokens: &R [&R str],        // slices pointing into own.text
}

fn make_view[region R](src: &borrowed str, r: &R Region) -> &R DocumentView[R] {
  let doc    = r.alloc(Document { text: copy_to_region(src, r) })
  let tokens = tokenize(doc.text, r)     // slices into doc.text, same region
  r.alloc(DocumentView { own: doc, tokens: tokens })
}

No new construction syntax, no escape hatch — just allocation into a region and an ordinary record.

Why it’s safe, and why that’s not a coincidence

Walk the ways a self-reference normally goes wrong, and each one is closed by a rule you have already met:

  • The value can’t move (axiom A), so the address a slice points at never changes.
  • It can’t be mutated (axiom A), so the relationship set up at construction can’t be broken later.
  • Every field points inside the same region (axiom B), and a region frees all at once (the unification from two episodes ago) — so nothing a slice points at is ever freed before the slice. There is no invalid intermediate state to pass through on teardown; the whole area goes at once.
  • A view can’t be carried out of its region (the R in the type, axiom C), so there is no path by which it outlives what it points into.

The striking thing is how little of this is about views. Almost all of the safety comes from properties the region already had: it doesn’t move, it frees in bulk, references into it are typed with R. The view rules add only “immutable, and declared with of T.” A problem that costs other languages Pin and unsafe, or a dependent type system, costs Mere three small rules — because the memory model built in the previous episodes was already carrying the weight. This is what it looks like when collapsing two concepts into one pays off downstream: the next hard feature turns out to be nearly free.

There is a philosophical point here that the language keeps returning to. Rust’s honest answer to self-reference is unsafe — an explicit hole in the guarantees, which is at least honest, but it is a hole. Mere would rather constrain the feature until it needs no hole: a view can only be immutable, region-bound, and non-movable, and in exchange it is checkable with no escape hatch at all. Less expressive than “anything, anywhere, with unsafe,” and that is the trade being made on purpose — the cases it does cover (parse trees, string interning, documents and their indices) are covered with a guarantee you can read off the types.

What a view costs at runtime

Nearly nothing, and for a satisfying reason. If every field of a view is either a region-internal reference or a primitive, then the whole view is Trivial in that region — the constraint from the last episode. So a view needs no cleanup of its own: it is reclaimed when its region is, by the same single pointer reset that frees everything else there. The type-level story and the runtime story line up — a view is safe and free for the same reason, that it lives and dies with one region.

Part II set out to give Mere an explicit, checkable memory model, and with view types the shape of it is complete: five strategies narrowed to a unified region, a Trivial line dividing what a region can hold from what with must clean up, and now self-references made safe by staying inside a region. What’s left is to make it usable — the everyday containers a program actually reaches for. Next: region-aware collections, and the Trivial hierarchy that decides which of them can nest inside which.

← Back to Mere: Building a Language