ADR-252: The Syntax Tree Moves to Nikaia Whole, and a Span Is Two u32

Status: 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)

1. Context

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:

  1. What a Span is. std::ops::Range<usize> today, and Nikaia has no usize (Part I 2.2 offers i32, i64, u32, u64, f64).
  2. Whether Spanned[T] is a generic struct Nikaia writes, and with it where the boxes of a type that holds itself stand.
  3. How the Rust that remains reads a tree the other language declares. 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:

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.

2. Decisions

D1 - The tree moves whole, and 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).

D2 - A 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.

D3 - 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.

D4 - What the language gains first, each in its own package and tested there.

Per ADR-250 D3 these are fixed in the compiler, not worked around in the .nika:

  1. A declared type whose every part is a copy is a copy. The checker’s 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.
  2. A literal is its magnitude and its sign. 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.
  3. A described Rust type says whether it copies and compares. 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.
  4. No Default. FnParams and FrameAttr are built with a named constructor at their eight sites, written in the .nika.

D5 - The order: the Rust changes that need no Nikaia go first.

  1. 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.
  2. LitInt becomes D4.2’s shape and Default goes (D4.4), still Rust.
  3. The language gains D4.1 and D4.3, each with its tests.
  4. 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.

D6 - A constant fold counts in a magnitude and a sign, held to a pinned type at every step.

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.

3. Consequences

4. Considered and rejected

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.

5. What is built

Built at 0.0.262: D5 step 1, Span is two u32, still in Rust.

Built at 0.0.263: D5 step 2, a literal is a magnitude and a sign, and no Default.

Built at 0.0.264: D4.1’s first half, the derive.

Built at 0.0.274: D4.3, a described Rust type says whether it copies and compares.

Built at 0.0.275: D5 step 4, the tree is Nikaia.

Built at 0.0.276: the rest of D4.3, Symbol described.

Built at 0.0.277: D4.1’s reading, for a described type.

Built at 0.0.278: D6, and fold is Nikaia.

This site is open source. Improve this page.