ADR-181: T::fields Is a List the Build Walks, and the Loop Over It Is Unrolled Per Type

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


1. Context

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.

2. Decisions

D1 — 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.

D2 — The loop is unrolled once per type the function was used with, and the generic original is not emitted.

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

D3 — The body is checked once per turn, and every message says which turn it came from.

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.

D4 — 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.

D5 — --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.

3. Consequences

4. What this does not decide

5. What is built

D1 to D4 at 0.0.129, and D5 at 0.0.130.

This site is open source. Improve this page.