T::fields Is a List the Build Walks, and the Loop Over It Is Unrolled Per TypeStatus: Superseded by ADR-304 — its D1 is D4 there, D2 is D6, D3 is D8 and D5 is D10; D4 is not carried (displaced by ADR-217 D1)
Date: September 21, 2026
Target Version: Nikaia 0.0.129 (D5 at 0.0.130)
Builds: ADR-088 D2, D4, D5 and D6, which that record left as
what the bound reaches
Answers: #174’s the build-time
evaluator has no field walk, and the half of NK1171 that said so
Related: ADR-088 D1 (there is no macro system), D3 (the bound
moves one class of failure to the call), D6 (--comptime, built here as
D5), ADR-295 (a bound is answered from a declaration),
ADR-288 (the emitter knows no types, so the checker hands it the
answer), ADR-296 D17 (the generated file says what the program
said)
ADR-088 decided Part II 10.3 in full and built two of its six
decisions. D2’s bound — [T: Struct] — arrived at 0.0.120, and with it D3’s
NK1164 at the call. What the bound reaches did not. T::fields was
NK1171, a refusal whose whole content was this is specified and this
compiler does not have it, and that sentence was the only honest thing to say:
the member had no value, the loop had no unrolling, and the body had no check.
Three records had been waiting on the build-time evaluator, and by 0.0.128 two of them were answered — ADR-079’s table and ADR-287 D6’s call. This is the third, and it is the one that needed something the other two did not: a type’s shape as a value, and a loop that is not a loop in the language below.
T::fields is a list, and the for over it is the for this language already has.What it hands back is a sequence of reflected fields, each answering .name —
the field’s own name as text — and .of(value), what that field holds on this
value. Those two are the whole of it (ADR-088 D2), and NK1180
says so when a third is written.
A function walks a shape when its body says so, not when its bound allows
it: fn tell[T: Struct](v: T) that never writes T::fields is an ordinary
generic function, generic in the language below and emitted once. The bound says
a shape may be asked for; the body says whether it is.
T::fields is known while the program is built, so a loop over it cannot be
anything but unrolled — ADR-088 D4’s own derivation, and the
reason this needs no second keyword. What is emitted is one copy of the function
per type argument a call gave it, named describe__User, with the loop replaced
by its turns: field.name is a literal and field.of(value) is value.name.
At run time nothing is left — no loop, no descriptor, no dispatch, which is
D5’s what remains is the code somebody would have written by hand.
The instantiations are the checker’s, handed to the emitter keyed by the
byte a call stands at, because this emitter has no types
(ADR-288) and which copy describe(u) means is a question about
one. The name a call writes and the name a definition writes come from one
function, so the two cannot drift.
And the unrolling is a fact about the package, not about a file. The
files of a package share one namespace (Part I 9.1),
so describe may be declared in shapes.nika and every call to it stand in
main.nika — and the two halves are read from different places: which
functions walk a shape off the items, which types they were used with off
the calls. Both are collected over the program’s other files as well as
this one, so the unit that declares the function writes the copies the unit
that calls it asked for. Collected per unit, that program had neither a copy
nor a generic original, and the name reached the language below undeclared.
A copy has no type parameter left. The whole of what the parameter was for
is the shape, and the shape is written out — so the copy is an ordinary function
the language below compiles, and #[allow(non_snake_case)] rides with it
because describe__User is a name this compiler chose and Rust’s lint would
otherwise ask the author to rename a function they wrote as describe
(Part III C.1).
field.of(value) is a String for one field and an i64 for the next, so
there is no single type to check the body against — and a body that is wrong for
one field is right for the others, on the same line. So the walk is
repeated with the field bound, and what it finds carries
= unrolling `T::fields` for `User`, at field `name`
Without that line this is the error class C++ templates carried for twenty years (ADR-088 D5).
What the generic walk already said is not said again. The same body is walked once with nothing known, and a message from that walk is about the function — a misspelled member, say. Repeating it once per turn would turn one mistake into as many as the type has fields.
And the generic walk refuses nothing about a field it cannot see: there
field.of(value) answers ?, which fits everything
(ADR-024 D1). A refusal from that walk would be a refusal about
every turn at once, which is exactly what D5 says cannot be made.
variants is not built, and the refusal says which half is which.Superseded by ADR-217 D1, 0.0.195: T::variants is built.
An enum’s shape is a different value: a variant carries a payload where a
field carries a type, and what a loop over one would bind has not been decided.
NK1171 stays, with a sentence that now names what is built rather than
listing D4 to D6 as open — and a way out that names match, which is what this
language has for an enum’s shape today.
--comptime prints what was unrolled, and there is no syntax for it.ADR-088 D6’s report: the same information D3’s diagnostic carries,
offered on demand instead of on failure. Every build-time system shares one
readability problem — you cannot see what a function becomes for a given type
without unrolling it in your head — and the usual answer is to invent syntax.
This project already has the other one, and --overlaps, --sharing,
--tethers and --trust are it.
A shape walk nobody calls has its own line, and that is the one thing only this report can say: no copy is written for it, so nothing in the generated file says it exists.
Printed once for the program and not once per file, which is the difference from the other four: an unrolling is a fact about a call, and a call may stand in a different file from the function it names.
NK1180 for a member a reflected field does not have, and
NK1171 narrowed to variants.RefCells that
hold which copy is being written and which turn it stands at. And, for a
unit that declares a shape walk, one walk of each of the package’s other
files to find the calls: a unit that declares none pays nothing, which is
every unit of every program in the corpus.T::variants (D4), which wants a value nobody has designed.describe is a free function here; a
[T: Struct] method would need the instantiation read off a receiver,
which is the type checker’s answer and a different lookup.D1 to D4 at 0.0.129, and D5 at 0.0.130.
T::fields with a sequence of $Field — a name no program can write, because
$ is not in an identifier — records one instantiation per call, and walks
each body once per turn with the field bound.--comptime beside the four reports that already existed, printed once
for the program — over every unit, because a function, its calls and the
build that asks may stand in three different files.crates/nikaia/tests/project.rs
builds and runs that program, because every unit test of the unrolling
passed throughout.crates/nikaia/tests/reflection.rs runs programs, because what an
unrolling produces is a question the language below answers: the specification’s
own describe over two types, a body wrong for one field and right for the
next, and the members a reflected field does not have.