ADR-176: A Map the Build Can See Is a List of Pairs, and What It Crosses As Is a Table

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)

1. Context

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.

2. Decisions

D1 — A map the build can see is written as a list of pairs, and the declared type says it is a map.

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.

D2 — What it crosses as is a table, and no generated code.

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.

D3 — Which table is the compiler’s, and the number is measured.

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.

D4 — A duplicate key is refused, and it is refused at the build.

[("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.

D5 — Text keys, and the rest is refused by name.

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.

3. Consequences

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.

4. What this does not decide

5. What is built

All of D1 to D5.

Two things the running test found, both of which would have shipped:

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.

This site is open source. Improve this page.