Status: Accepted
Date: September 27, 2026
Answers: open-decisions.md, how a type holds itself - the owner chose
option 1, the compiler puts the indirection in
Related: Part I 4.4 (enums), ADR-094 D4 (the & is the
compiler’s to write), ADR-291 (patterns), ADR-291
(a match over a place lends), ADR-290 D11 (the toolchain is
written in the language)
enum Expr { Add(Expr, Expr) } and struct Node { next: Node? } passed the
check and lowered as written, and the language below said recursive type has
infinite size about a file nobody wrote: a value that holds a whole value of
its own type inline would be larger than itself. Refusing them in this
language’s words left the question of how a type should hold itself.
It is the question that stood between Nikaia and a parser written in Nikaia:
the compiler’s parser is already written in the grammar language Nikaia’s
grammar blocks lower to, and its actions build a syntax tree, which is a type
that holds itself. The three options were the compiler writing the indirection,
a written indirection type in std, and no new form. The owner chose the
first, which is the rule the language already follows for references - the
reference a call needs is the compiler’s to write - applied to the one other
thing Rust makes a program spell for the machine’s sake.
A field is on a ring where what it holds inline reaches back to the type
that declares it: the type itself, or one that holds it, through any number of
types between. Inline is the type itself, nullable or not, a tuple’s parts and
an array’s element; a view, a function, a list, a map or anything this compiler
cannot see through is an indirection already and breaks no ring. Such a field
is Box<T> below, where T is the field’s own lowered type - so next: Node?
is Box<Option<Node>> and Add(Expr, Expr) is Add(Box<Expr>, Box<Expr>).
Both edges of a ring through two types are boxed (Expr holds a Call that
holds an Expr). That is one allocation more than the least that would do, and
it keeps the rule a property of a field rather than of a choice among a ring’s
edges that a reader could not predict.
Such a type is not refused (NK1192 is withdrawn): the walk that finds it is
the walk that finds the fields to box.
A read of a boxed field is (*base.field) below: a place of the field’s
declared type, which is read, lent, moved out of an owned value and assigned to
exactly as the unboxed field would be. A read through ?. reaches the same
place inside the closure it lowers to.
A name a match binds out of a boxed part is taken out of the box at the head
of the arm, through std’s boxed::open: a Box<T> taken by value is a T,
and a &Box<T> lent is a &T, so the arm reads the name as the pattern’s type
says whether the match took the value or lent it.
A struct literal, a with and a variant’s constructor put the value for a
boxed field in Box::new(…), around whatever else the value is written with -
Box::new(Some(tail)) for a node handed to next: Node?, and Box::new(None)
for a null.
The language below matches no pattern through a box. Where D5’s rewrite does
not reach - a pattern that looks more than one level into a boxed part, or one
inside an | - NK1193 refuses it and says what works: bind the part to a
name and match on the name in the arm.
match in the arm
rather than refused. The part is bound to a fresh name, the arm’s guard asks
whether it has the shape (matches! over nikaia_std::boxed::peek), so the
next arm is tried where it does not, and the arm takes it apart with a
let … else before anything else. Such an arm covers no variant on its own
for NK1151, as a guarded one does not. One level per arm and outside an
|. A guard that reads a name bound in there is answered by a match
through the box that binds the names - a number copied out, anything else a
view of the part - which answers the guard where it fits and false where
it does not. Over a value the match was lent, a number taken out of the
box at the head of the arm is copied out too.nikaia_std::boxed::peek), and the checker reads it as a view
there: Expr::Add(a, b) if a == Expr::Num(1) compares the part.null costs an allocation.
Option<Box<T>> would make a null free, and needs the reads D2 makes into
places to become reads of an option of a view, which is a larger change to
the same three sites. It is open (§4).Option<Box<T>> for a nullable field (D5 item 3).Heap[T] beside the box the compiler writes, for a program that
wants the indirection where there is no recursion. Nothing has asked for one.