Status: Superseded by ADR-311 — its D1-D2 are D1-D2, D3-D4 are D6-D7, D5 is D16 there
Date: September 15, 2026
Related: ADR-287 D4 (a comptime binding’s fold must succeed)
and ADR-287 (the word), ADR-287 (what a build-time
body may do — this record is about what it may hand over),
ADR-010 (the compiler already chooses a map’s hasher from where its
keys came from), ADR-026 §3 (perfect hashing is compiler-side
staging, not user code), docs/lock-free.md and
ADR-287 (the measurement that says the right lookup depends on
size)
ADR-287 built a comptime binding and
ADR-287 said what its initialiser may do. Neither says what may
cross: a value computed while the program is built has to arrive in a program
that is running, and the crossing is not free.
It is not free because the destination is a constant in the generated file, and a
constant cannot own memory. A parse that produces a Vec[Row] parses perfectly
well and has nowhere to land. So the question is not what can be computed — that
is ADR-287’s — but what shape a result has to have to be written
down at all.
And the use case that motivates the whole feature makes the question sharp rather than academic: a table whose keys come from a file, looked up at run time. That is where the answer has to be good.
Vec[T] arrives as &[T]. String arrives as &str. What was built with
push is fixed once it has crossed, and the binding’s type at run time is the
view.
This is not a new distinction, it is the one Part I already makes. A language that separates a buffer from a view into it has already said this about every value it owns; the crossing is simply the place where the separation is forced rather than chosen.
It also happens to be exactly where the language below draws its own line —
const X: &[T] exists and const X: Vec<T> cannot — so the two boundaries
coincide and nothing has to be negotiated between them.
Not the parse, which was fine. A comptime binding whose value owns memory is
refused because of what it is, and the way out is the view-shaped equivalent.
The alternative was considered and is worse: build the value once at program
start into a static. That gives up precisely what the feature is for, and turns
comptime into a claim about a thing that happens later — which is
ADR-287 D4’s failure mode with extra steps.
Its keys cannot change after the crossing, and that is not a restriction imposed for tidiness: a closed key set is the precondition for a perfect hash, and the crossing is where it closes. The property the feature needs arrives by itself.
So the program writes ROUTES.get(name) and never learns what happens inside.
Perfect hash, binary search over a sorted slice, or a match on the key — the
compiler picks, the way ADR-010 already picks a hasher from the
provenance of a map’s keys with nothing in the source mentioning hashing.
build() to call.A PerfectHashMap with a constructor would freeze one implementation into an
interface, and then it could never change. The measurement in
ADR-287 is the argument against it: over 200 keys a match
beats a hash map 9.0 ns against 18.0 — and nobody should read that as always
use a match, because it is a statement about 200 keys and says nothing about a
million. A decision whose right answer moves with the size of the input is a
decision that belongs where the size is known, and that is not the source.
What a program may say is what it means: this map is fixed. What it may not say is how the compiler should honour that.
Writing the value as bytes and casting is a real technique and was considered properly, because it looks like a way to admit types D1 excludes. It is not, and the reason is worth recording so it is not re-proposed:
usize, or a target-sized length has a different shape in memory on each. Only
fixed-width, pointer-free fields survive the trip — which is D1’s constraint
again, reached from the other side.repr the program never asked
about, and padding bytes are a hazard to
ADR-005 D8’s byte-determinism, which CI enforces with double
builds.Where a blob genuinely wins is a table large enough that emitting it as source is slow to compile. That is a real reason and a later one, and it admits the same set of types.
Named here because the decision is cheap and the work is not.
A fixed map type, as the crossed form of a map. That is a type in std and
not a language rule.
An evaluator that can loop and push. A parse that builds a table does both, and ADR-287 D6’s first stage is literals and arithmetic over them. This is the real work behind the feature, and this record does not shorten it.
The list literal, for the case where somebody writes the pairs by hand rather
than parsing them. Part I 4.5’s status note already says it is not built — [1,
2, 3] is a parse error at the [ — which has the odd consequence that the toy
version of this feature is harder to reach than the real one.
The crossing is one sentence, and a reader who knows the difference between a buffer and a view already knows it.
The motivating case needs no new syntax at all. No PerfectHashMap, no
build(), no annotation: a map whose keys are closed is enough of a statement,
and everything after it is the compiler’s.
One refusal is added (D2) and it is about a type rather than about an operation, which is unusual enough here to be worth the sentence.
D1 and D2. At 0.0.111 a body may build its table with push and the result
crosses fixed; at 0.0.112 the same holds for text, which is the other value
this record names:
comptime MAJOR: i64 = 0
comptime MINOR: i64 = 1
comptime BANNER: &str = f"nikaia {MAJOR}.{MINOR}" // const BANNER: &str = "nikaia 0.1";
A String a body built arrives as a &str, and NK1167 sends a comptime
declared String to the view rather than to .to_string() — which is what the
ordinary type rule used to advise, and is the one thing that makes the problem
worse.
The list half, in full:
fn squares() -> Vec[i64] {
let mut xs = []
for i in 0..<5 { xs.push(i * i) }
return xs
}
comptime TABLE: Array[i64, 5] = squares() // const TABLE: [i64; 5] = [0, 1, 4, 9, 16];
push).[T; N] rather than &[T]. D1 names the view and the
language below has both; the array is what the type language can already
spell — ADR-152’s Array[T, N] — and it carries the length,
which the build knows and a view does not. A &[T] spelling for a list is
#200’s question about text read one type
over, and nothing needs it yet. Text has no such question, because &str
is already the spelling Part I 2.2 gives it — which is why that half is D1 as
written rather than D1 answered with the nearest thing."\u{0041}"
is one character, == "A" is true, and .len() is the byte count
String::len promises. 0.0.112 held the written form instead and refused
both, which was a representation showing through rather than a rule —
corrected at 0.0.113 after the owner asked why a length should be refused.\ and any character (STR_CHAR) and the emitter hands the
literal to the backend unchanged, so rustc is what accepts or rejects it —
measured, println("a\qb") is unknown character escape: q on the .nika
line. What holds the pair honest is a test that runs the program: the same
literal, read at build time and at run time, printing the same bytes.NK1167, and the way out names the number: the build has just
computed the value, so declare it Array[i64, 5] is a line the reader can
take rather than one they have to work out.And D3’s fixed map is built (ADR-176), which is the one thing
§3 asked std for and std did not have. Fixed[&str, V] is that type;
comptime ROUTES: Fixed[&str, i64] = [("get", 1), ("post", 2)] is what a
program writes, so the input needed no map literal — a list of pairs and a
declared type, which is D1’s crossing read through
ADR-152 D4’s rule.
Array[T, N] and text as a &str, so a list of text is
[&str; N]. What asked for it was walking a table’s keys with a for.docs/history/fixed-map-lookup.md first put the
crossover between 8 and 16 keys — a match ahead by 2.4× on a hit and
4.3× on a miss at eight, a perfect hash ahead from sixteen and flat thereafter
— which corrects §3’s reading of ADR-287’s numbers (200 keys
favour a match 9.0 against 18.0, which is about a HashMap: against a
perfect hash a match of two hundred keys is behind by 1.9×). Then §6
measured a linear scan over the same static arrays and found it the
match within 0–17 % up to twenty-four keys, past where the hash already
wins. So the compiler emits no lookup code at all: which shape a table has
is data in the const, and Fixed::get reads it.And what a build-time value is widened at 0.0.125, which is D1 read from the other side: D1 says a result crosses in its view form, and the set of results this compiler could compute at all was narrower than the set the language below can hold. Five shapes joined it, and each one had been a correct program refused (Part III C.4) with a sentence about this compiler rather than about the program:
comptime R: f64 = 1.5 had nothing to arrive as. The literal is
written with {:?} and not {}, because Rust’s Debug for a float is the
shortest text that reads back as the same bits — and only a finite one is
written, so 1.0 / 0.0 stays a comptime this compiler says it cannot
evaluate rather than a const of something the source never named.enum variant, with what it carries. This is the shape that makes the
refusal in D2 a question about the value and not about the type:
Shape::Empty is a const and Shape::Many([1, 2]) is not, and the two are
the same Shape. So NK1167 walks the value, names the variant, and says
that another variant of the same enum may cross perfectly well. Without it
the second lowered and rustc answered about the generated file.[[T; M]; N]. The element type was read one level deep
and asked no further.struct whose field is an Array[T, N], which the walk that asks
whether a field can be written as a const refused — it read Array as a
type with no constant form, which is the opposite of true.u8, the byte Part I 2.2 offers. comptime MAGIC: Array[u8, 4] = [0x7F,
0x45, 0x4C, 0x46] is a buffer a const holds, and it was NK1127. The
byte gained a range check in the same breath (NK1116), because a crossed
form with no range is rustc about the generated file the first time somebody
writes let x: u8 = 300 — which it was, and which had nothing to do with
comptime.And D1’s &[T] is built at 0.0.127 (ADR-179), which is this
record’s own sentence arriving four records after it was written. What had been
built was the array: Array[T, N] carries its length in its type, and a
field whose run is a different length per value has no such type to be. So
struct Section { settings: Vec[Setting] } could not cross, and neither could a
variant’s payload — which is what both corpus grammars produce.
Vec[T] arrives as &[T] now, as D1 says, and Array[T, N] stays what a
program writes where it wants the length in the type. The & is written
against the declaration and not off the value: the same computed list is
[1, 2, 3] for an Array[i64, 3] and &[1, 2, 3] for a &[i64], so the
crossing walks the type beside the value, down through a struct’s fields and a
variant’s payload.&[u8] follows from it, which is the byte buffer Array[u8, N] was
standing in for at 0.0.125.Vec a body just built is gone when the
expression ends. NK1179 says which of the two a line is.Bytes are unchanged. asset("…") now has a result
type it could hand bytes back as and does not; Bytes owns its run. Both are
named in ADR-179 §4 as what this does not decide.Not built: D5’s blob, which waits for a table large enough to make somebody ask — and that is again the only thing on this record’s list.