u32Status: Superseded by ADR-294 — its D1-D6 are D6-D11 there
Date: September 29, 2026
Target Version: Nikaia 0.0.261
Answers: ADR-250 D2’s the syntax tree moves by a record of
its own that answers the three questions in §1 - the owner chose the tree
moves whole and a Span is two u32
Related: ADR-196 (a Nikaia piece joins a Rust program as an
ordinary Cargo module), ADR-204 D1 (what a declared type
derives), ADR-246 (the compiler puts the box in),
ADR-248 (u32, u64, a literal as large as a u64),
ADR-104 (a Rust crate’s surface, described)
The three modules that read nothing of the compiler - fixed,
emit::template, manifest - are Nikaia (0.0.250, 0.0.252, 0.0.260). Every
module left reads the syntax tree, and no Nikaia module can read a tree Rust
declares: a Nikaia match over Expr needs an Expr the language knows the
variants of. So the ring after the leaves - fold, interpreter,
contracts::{trust, ty, send} - waits on the tree, and ADR-250 §1 named what
moving it decides:
Span is. std::ops::Range<usize> today, and Nikaia has no
usize (Part I 2.2 offers i32, i64, u32, u64, f64).Spanned[T] is a generic struct Nikaia writes, and with it
where the boxes of a type that holds itself stand.ast/mod.rs is 35 types in 1,191 lines; 28 files of the compiler read it,
Span is named 289 times in 13 of them and cloned 184 times.Reading the tree against what the language can say today (0.0.260) finds four things it cannot:
Copy. BinaryOp, UnaryOp, Repeat and Receiver are Copy, and
the Rust that reads them passes them by value. The emitter derives Debug
and Clone always and PartialEq/Eq where the checker says a type
compares (ADR-204 D1) - never Copy.i128. Expr::LitInt(i128) holds every literal ADR-248 D2 allows, from
-9223372036854775808 to 18446744073709551615. No Nikaia number holds
that range.winnow_grammar::Symbol, the interner’s Copy handle. A .nika file may
write that type - the checker takes a qualified name as an opaque named type
copies()) and never compares, because
describe writes neither for a foreign type. A tree whose every name is a
Symbol is then a tree whose names cannot be compared in Nikaia.Default. FnParams and FrameAttr derive it, for eight call sites.And two things Rust decides for Nikaia: winnow_grammar::WithSpan is a Rust
trait whose method takes Range<usize>, and the orphan rule lets its impl
for a type declared in nikaia-std stand only in nikaia-std.
ast/mod.rs becomes its adapter.crates/nikaia-std/src/tools/ast.nika declares all 35 types, lowered by
nikaia lower-std and committed beside it as the other tools are (ADR-196);
crate::ast in the compiler becomes a re-export of nikaia_std::tools::ast
and the few lines of Rust that Nikaia cannot say. Nothing is duplicated and
nothing converts: the lowered types are the compiler’s types, which is what
makes the Rust that stays a reader of the Nikaia declaration rather than of a
copy of it.
The WithSpan impl stands in nikaia-std’s lib.rs beside the module, the
place http1::written already stands for the same reason (a Rust caller needs
a Rust-shaped door).
Span is a struct of two u32, and a source is at most 4 GiB.pub struct Span {
pub start: u32,
pub end: u32,
}
A byte offset in a .nika file is never negative, so i64 would carry a sign
nothing uses and twice the width - every node carries one, and the tree is the
compiler’s largest structure. u32 addresses 4 GiB; a source longer than
that is refused where it is read, with a message, rather than wrapped. The
at fields (ListLit, LitStr, With, SelectArm) are byte offsets too and
become u32 with it.
A Span is a copy (D4), so the 184 .clone()s go. What the Rust that
remains needs - a Range<usize> to slice the source with - is a method of the
adapter, not a field of the type.
Spanned[T] is a generic struct Nikaia writes, and the boxes are where the compiler puts them.Spanned[T] with node, span and doc: ?String, and new and
documented in an impl[T] Spanned[T] - both already in the language
(generics, ?T). A field that closes a ring of types is boxed by ADR-246
D1’s rule, not by the hand that wrote ast/mod.rs: where the rule boxes a
different field than the hand did (Type’s code, Stmt::Return), the Rust
readers follow the declaration. The box is the compiler’s to place, which is
the decision ADR-246 made for every program, and the compiler’s own tree is one.
Per ADR-250 D3 these are fixed in the compiler, not worked around in the
.nika:
copies() answers yes for it and the emitter derives Copy beside Clone
(ADR-204 D1, extended). A unit-only enum is the common case, and a struct
of two u32 the other. Nothing a program writes changes meaning: a value
that is copied instead of moved is one no later line could have read as
moved anyway.LitInt { value: u64,
negative: bool }, which is what the source writes and exactly ADR-248 D2’s
range. The 69 readers of LitInt ask for the value they need (as_i64,
fits) rather than doing i128 arithmetic.describe
reads the type’s #[derive(…)] and writes copies and compares into its
contract (ADR-104), and the checker reads them for a foreign type as it does
for a declared one. winnow_grammar’s description gains Symbol.Default. FnParams and FrameAttr are built with a named
constructor at their eight sites, written in the .nika.Span becomes the struct of D2 in ast/mod.rs, still Rust, and every
reader moves to it; at becomes u32; the 4 GiB refusal. The largest diff
of the road and entirely mechanical, so it is taken alone and the suite
proves it.LitInt becomes D4.2’s shape and Default goes (D4.4), still Rust.ast.nika replaces ast/mod.rs (D1, D3). What this step changes in
the Rust readers is only what the declaration decided differently: the
boxes, and nothing else.After step 4 the next ring moves as the leaves did. It will need one more
thing, which is not decided here: ADR-246 D4 refuses a pattern that looks
inside a boxed part, and fold matches Expr::Binary { lhs, .. } on every
line. That is ADR-246 D5’s first refinement, and it is taken when the first
module of the ring asks for it.
Decided by the owner at 0.0.278, when fold was the first module of the ring
after the tree to move, and the first to need a number wider than any the
language has: it folded in an i128.
A magnitude and a sign, the shape D4.2 gave a literal: a u64 and a
bool, 65 bits, which hold every literal ADR-248 D2 allows. The arithmetic is
checked on the magnitudes (wrapping_ and a comparison, since the language has
no checked_), and a step that leaves the 65 bits is too large, which
NK1116 says as more than the widest - no number past it is named. No
i128 joins Part I 2.2: a fold is a handful of steps while a program is built,
and a type every 32-bit target would pay for is not bought for it.
And a declared operand holds every step to its type, which is rustc’s
model: let a: i32 = 2000000000 then a + a - a ends inside an i32 and is
refused at a + a, where the language below would refuse the lowering. A
literal still pins nothing (ADR-060), so a fold of literals is judged by what
it comes to.
Considered: folding in i64 and claiming nothing past it (a sum past i64
would reach rustc unrefused), and i128 in the language (rejected in §4
already). The others Go, Zig and Swift take - an exact number of any size - buy
nothing here: every number past the 65 bits is refused anyway.
u32 against two
usize), and Span is Copy.nikaia-std holds the compiler’s tree. It is the crate every generated
program links, so the tree’s code is compiled into programs that never read
it - dead code the linker drops, and the same trade tools::rust already
made (ADR-196 D1).The tree stays Rust, and Nikaia reads it as a described crate (the owner’s
option B). It needs match over a foreign enum, which describe does not
record at all today (a pub enum has no variants in its contract), and at the
end of it the tree - the one type every module reads - would stay Rust for
good. Stage 1 (ADR-250 D5) would then be unreachable by construction.
i64 for a Span. The natural Nikaia number, and a sign nothing uses at
twice the width, on every node.
i128 in the language, for LitInt. One field of the compiler’s tree is
not a reason for a number type Part I does not offer, and a literal is a
magnitude and a sign - that is how it is written.
A copy of the tree in Nikaia, converted at the boundary. Two declarations of one tree is two answers to what the tree is, and a conversion on every module call is a cost the compiler would pay for ever.
Built at 0.0.262: D5 step 1, Span is two u32, still in Rust.
ast::Span is Span { start: u32, end: u32 }, a copy, with bytes() for
the range that slices the source, at() and stop() for the two offsets
the checker’s tables are keyed by, and From<Range<usize>> for the
grammar’s spans, which winnow_grammar hands over as a range. The at
fields of ListLit, LitStr, With and SelectArm are u32.parser::fits refuses a source longer than u32::MAX bytes before either
entry reads it, so no offset can wrap..clone()s of a span went; 21 files changed, and the tree is 8 bytes
smaller on every spanned node.tests/span.rs holds the size, the copy, the slice back to the source and
the refusal.Built at 0.0.263: D5 step 2, a literal is a magnitude and a sign, and no
Default.
Expr::LitInt { value: u64, negative: bool }. The parser still reads a
number as an i128 and ast::int_literal makes the node; the eight
readers that want the number ask ast::int_value, and the sixty that ask
only is this a number match LitInt { .. }.FnParams::none() and FrameAttr::bare() replace the two derived
Defaults, at the two places a grammar rule wanted one.Built at 0.0.264: D4.1’s first half, the derive.
bool, a char, or a
tuple, nullable or array of those, or another such type, derives Copy
(Checked::copies, the least answer that holds, so a ring of types never
joins - it holds itself through a box). tests/copies.rs, with rustc
accepting every derive it writes.lends makes a parameter &T or T) and the
lowering of a binding, across units through the ledger - a larger change
than the derive, and not one the tree’s move needs. The Rust readers of
BinaryOp, Span and the rest need only that Rust may copy them. It is
taken with D4.3, when a Nikaia module is the first to need it.nikaia_std::tools allows clippy::clone_on_copy, with the
reason beside it: a .nika keeping a value past a move writes .clone(),
as the language still requires.Built at 0.0.274: D4.3, a described Rust type says whether it copies and compares.
tools/rust.nika reads a #[derive(…)] above a struct or enum, and any
other attribute is skipped as before. describe drafts copies = true for
Copy and compares = true for PartialEq, by the last segment of each
path, so std::cmp::PartialEq counts. The line is reviewed like crosses.Checked::copies starts from the library’s types that say copies = true,
so a declared struct holding one derives Copy. == already read
compares.winnow_grammar’s description does not exist yet; its Symbol is taken
with step 4, the first step that reads it. tests/describing.rs holds the
derive.Built at 0.0.275: D5 step 4, the tree is Nikaia.
tools/ast.nika declares the tree’s types, and crate::ast is
pub use nikaia_std::tools::ast::* plus int_value and int_literal,
which need an i128. nikaia-std’s tools::ast adds Span’s usize
methods, offset, LONGEST_SOURCE and WithSpan: an impl of a type
stands only in the crate that declares it.Expr::Return(Expr?)
and Type.code are Box<Option<…>>, Code.result is boxed as well (both
edges of the Type–Code ring), and Pattern::Fold holds its FoldSpec
inline, since no ring runs through it. The readers follow, which is D3.
A null there costs an allocation, which ADR-246 D5.3 (Option<Box<T>>)
would take back; it is the one cost the move added.Span loses Hash, which nothing used, and Default, which became
Span::nowhere() at its five sites.Symbol is still undescribed, so the lowered tree derives PartialEq
only where the hand did: a type holding a name compares in Rust through
its fields and not as a whole. The description is written when the first
Nikaia module compares a node, which is the next ring’s.Built at 0.0.276: the rest of D4.3, Symbol described.
tools/described.contracts is the description a tool module is checked
and lowered against beside std’s ledger: winnow_grammar::Symbol with
copies and compares, and InternerContext::resolve. A module of std
itself does not read it (tests/sysroot.rs).&Symbol), because contracts::keeps::moves names the
copied types itself and reads no ledger - D4.1’s second half, the reading.
A Symbol read through a field (n.name) is unaffected; one passed bare
from a parameter to a described call is &Symbol where Symbol is wanted.
It is the next step, and the first tree reader needs it.Built at 0.0.277: D4.1’s reading, for a described type.
keeps::lends_in (the declaration and the call, in the checker and the
emitter alike) and Checker::takes_away (every move the checker counts)
answer copied for a type a ledger says copies = true. A Symbol
parameter is a Symbol below and may be used after it was handed on.Checked::copies) is
still lent and still moved in the checker’s count. Its answer lives in one
unit’s check, and a caller in another unit reads only the ledger, which
writes copies for a described type and not yet for a unit’s own. Writing
it there is the rest of D4.1.Built at 0.0.278: D6, and fold is Nikaia.
tools/fold.nika is the fold: Constant { magnitude, negative, pinned },
and Folded says a value, a step that left its pinned type, a step past the
65 bits, or nothing. The compiler’s fold is its adapter and hands the
checker the i128 it keeps a constant in; a fold past the 65 bits is one
past u64::MAX.ast.nika beside it, as the files
of a package see one another, and nikaia-std writes use super::ast::*.match, is copied out as a number is;
and a function parameter the function was lent is handed on without a second
& - a recursion that did is a new type at every level, and rustc stops
with overflow evaluating the requirement. Five more are #171
#171, and the open number’s step #170.