Status: Superseded by ADR-311 — its D1-D5 are D8-D12 there
Date: September 21, 2026
Target Version: Nikaia 0.0.118
Answers: open-decisions.md’s what does a program
write, for a map the build can see? — option B
Related: ADR-079 D1 (growable going in, fixed coming out) and
D3 (a fixed map, and the lookup is the compiler’s),
ADR-152 D4 (a use answers a literal), ADR-175
(the aggregate value), ADR-293 (a map read is a T?),
ADR-296 D17 (this emitter knows no types),
ADR-010 (the compiler already picks a hasher from where keys came
from), fixed-map-lookup.md (the measurement)
ADR-079 D3 decided that a map built at build time crosses as a fixed map and that how it is looked up is the compiler’s. Two things stood between that and a program.
The threshold, which fixed-map-lookup.md
measured: a match is ahead below a dozen keys and a perfect hash from there on,
flat where the match climbs.
And the input, which is this record. The build cannot see a map’s contents: a
program builds one with collections::HashMap() and insert, both std’s,
whose bodies are Rust — and there is no map literal.
comptime ROUTES: Fixed[&str, i64] = [("get", 1), ("post", 2)]
No new syntax. The list literal is built (ADR-135), the tuple
is a type this language has, and a use answers a literal is the rule
ADR-152 D4 already makes for Array[T, N] — the same sentence one
shape out. A map literal was the other option and is the expensive one: a new
form in the grammar, and a second question about what it means outside a
comptime.
It stays additive: a map literal could still arrive later and would find this crossing already built under it.
Fixed[K, V] is a type in std — which is what ADR-079 §3 asked
for — holding four &'static things: a seed, displacements, keys and values.
The comptime lands as a const of that type. ROUTES.get(k) is an ordinary
method call on an ordinary value.
This is a measurement and not a preference. The obvious shape was to emit a
match for a small table, because that is what the numbers favour — and
fixed-map-lookup.md §6 then measured the third
shape, a linear scan over the same static arrays, and found it within 0–17 %
of a generated match up to twenty-four keys, which is past the point where the
perfect hash has already won. So the match buys nothing that a table does not,
and a table buys three things it does not: a Fixed is a value a program may
pass and store, the emitter stays one that knows no types
(ADR-296 D17), and the threshold is data in a const rather than a
branch in the compiler.
Below twelve keys the table carries no displacements and get is a linear
scan with a length check. From twelve it carries them and get is one hash, one
displacement, one slot, one compare — CHD, the algorithm the phf crate uses.
Twelve is where the two cross in fixed-map-lookup.md.
The band is wide and forgiving — anywhere from eight to sixteen costs under 30 %
on the wrong side of it — which is worth saying so that nobody re-measures this
to move it by two.
The hash is FNV-1a and not SipHash, for ADR-010’s reason one construct over: the keys are the compiler’s own, so there is no remote peer to be protected from, and the faster hash is the honest choice.
[("get", 1), ("get", 2)] has no meaning a compiler may pick between. It is
NK1169, naming the key and both places — and it is the build that says so,
which is the whole of what a comptime is for
(ADR-287 D4): the failure moves from the first read at run time to
the line that wrote it.
Fixed[&str, V] is what this builds. A key that is a number or a bool wants a
different table — a dense array, most likely, which is a different decision with
a different measurement — so it is refused with a sentence rather than hashed as
if it were text.
std gains one type and the compiler gains one generator. Fixed<V> is
sixty lines of Rust, and CHD is eighty in the compiler; neither is a shape the
language has to explain.
What a reader writes is a list of pairs, which is what they would write in a comment anyway.
The failure moves (ADR-287’s third point): a duplicate key in a routing table stops the build rather than resolving to whichever the map happened to keep.
Fixed built at run time. There is nothing to build it from: the point
is a closed key set, and a closed key set is what the crossing makes.All of D1 to D5.
nikaia_std::Fixed<V> with get, has, len and is_empty, and
[type."Fixed"] with its entries in std.contracts.Value::Tuple in the evaluator, so a list of pairs is a value it holds.NK1169 for a
duplicate key and NK1170 for a key that is not text.crates/nikaia/tests/fixed_map.rs, which runs a program over both table
shapes — under twelve keys and over — because a lookup that type-checks and
answers wrongly would pass every other kind of test. The compiler’s hash and
std’s are two implementations of one function in two crates with no type
between them, so that test is what holds them together; a generator that
agreed with itself would prove nothing.Two things the running test found, both of which would have shipped:
get hands back a value and not a view of one, which is what this
record’s -> $V? says. Option<&V> put a &&str in the generated file for a
table of text, where ?? is ambiguous between two Or impls — rustc
complaining about a file nobody wrote, which
Part III C.1 does not allow. It went unseen for a
table of i64 because one of the two impls happens to fit there.comptime EMPTY: Fixed[&str, i64] = [] was
NK1166, this is a Vec[?] and the const says Fixed[&str, i64] — a
correct program refused (C.4) with a way out that
asks the reader to write what they already wrote (C.2).
? fits everything (ADR-024 D1), which the array crossing one
shape over had read that way all along.And one thing beside it, which the running test asked for and
ADR-079 D1 already decided: a comptime list of text crosses
as [&str; N]. A list crosses as an Array[T, N] and text as a &str, so an
array of text is both of those at once and follows from the rule rather than
extending it — and it is what lets a for walk the keys that built a table.
And a value may be a declared type since 0.0.128
(ADR-180). Fixed[&str, Row] was NK1127 — this compiler cannot
evaluate it — for a value that evaluated fine and could not cross, and what
stood in the way was get’s own shape: it hands back a value, which has to be
Copy, and a Nikaia struct is not. A reference to one is, so the table
holds &'static Row and nothing here changes. Deriving Copy was the other way
and is the option that record declines, because it would make a struct’s
copyability a consequence of its fields one file away.