ADR-079: Growable Going In, Fixed Coming Out — and the Lookup Is the Compiler’s, Not an Interface

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)


1. Context

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.

2. Decisions

D1 — A result crosses in its view form.

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.

D2 — What cannot be written down is refused, and the message names the type.

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.

D3 — A map built at build time crosses as a fixed map, and how it is looked up is the compiler’s.

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.

D4 — There is no user-visible perfect-hash type, and no 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.

D5 — A serialised blob is a size optimisation for later, and does not widen D1.

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:

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.

3. What this needs that does not exist

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.

4. Consequences

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.

5. What is built

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];

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.

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:

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.

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.

This site is open source. Improve this page.