ADR-073: const Is a Declaration That Must Be Evaluated at Build Time — a let That Merely Folds Is Not One

Status: Superseded by ADR-287 — its D1 is D1 there as respelled by ADR-077, its D2-D6 are D3-D7 Date: September 14, 2026 Target Version: Nikaia 0.0.8 Related: ADR-072 (a compile-time read is named three times, and needs somewhere to stand), ADR-026 Q4 (what a build-time body may compute — still open, and deliberately not answered here), ADR-063 (the shared constant fold, which is the evaluator this can ship with), ADR-051 D1 and ADR-071 (reserving a word while it is still free), Part I 9.2 (Constants are items with pub), Part II 10.2 (const CONFIG: Json::Value = dsl Json from "config.json")


1. Context

Three records now end at the same missing word.

Part II 10.2 specifies compile-time parsing and its own status note says the half it specifies cannot be written: “There is no const in the parser at all — at item level it is a parse error, and inside a function body const X = 1 parses as two statements and is then refused by NK1117.” Part I 9.2 lists Constants among the items pub applies to and has to carry a parenthesis saying the declaration has no syntax. ADR-072 decides how a compile-time file read is permitted and has nowhere to put the from line.

The word is also not reserved, so the free moment for it is now — the same argument ADR-071 made an hour earlier for three other words.

2. Decisions

D1 — const is a reserved word.

Thirty-two becomes thirty-three. ADR-051 D1’s rule applies unchanged: reserving costs nothing before programs exist and breaks them afterwards. Unlike ADR-071’s three, this one is reserved for a construct rather than against the possibility of one.

D2 — It stands where an item stands, and inside a function body.

At item level it is package-private and takes pub to leave the package, which is not a new rule but the one Part I 9.2 already states for Constants — the parenthesis it carries about missing syntax is what this removes.

Inside a body it is an ordinary statement and scoped like a let.

Not decided here: a const inside an impl. Associated constants are a separate question about where a name lives, and answering it in passing is how a record ends up deciding something nobody asked.

D3 — Its initialiser is evaluated during the build, and failing to is an error.

This is the decision the word exists for, and it is worth stating against the thing it resembles. The compiler already folds constants: a let bound to 2 * 3 is folded twice over on the way through, and a name that folded keeps the type it took (ADR-063).

So const does not add folding. It adds a demand:

a let may fold; a const must, and says so where it cannot.

That is the difference that earns a keyword. A program that writes const has asked for a guarantee — this is not computed while the program runs — and a guarantee that silently degrades into “it happened to be cheap” is the kind of promise this project treats as a defect.

D4 — The type may be written and does not have to be.

Inferred the way a let’s is, including ADR-060’s rule for a literal nothing constrains. Part II 10.2’s example writes one because a grammar’s result type is worth saying out loud, not because the syntax demands it.

D5 — What may stand in the initialiser is not decided here.

ADR-026 Q4 owns it — whether a body must be sync, what bounds its steps, what bounds its memory — and this record deliberately leaves it open, because a const that parses and evaluates arbitrary code would answer the sandbox question by accident.

What this permits is therefore staged, and the first stage is buildable today:

Q4 was answered by ADR-075, and the second stage is built: a call to a function of this program, with the arithmetic, the comparisons and an if that a called body is made of. A loop is the third stage and is not in it — #174 carries that one.

Everything in the second group is refused by name rather than accepted quietly: a const whose initialiser this compiler cannot evaluate says so and says which question is open. An unevaluated const that falls back to runtime would break D3 in exactly the way D3 is about.

D6 — A const is a value, not a place.

It has no address, nothing may point at it, and there is no mut form. Nikaia has no second declaration for a build-time value that does have an address, and this record does not introduce one: nothing has asked for it, and the rule that keeps a word out until a program needs it is the same one that kept lock out of the touch vocabulary until the doors existed.

3. The example, measured

The question a keyword has to answer is what does a program get that it could not get otherwise, and the answer here is three things of very different sizes.

A route table whose keys come from a file the project owns:

const ROUTES: RouteTable = dsl Routes from "routes.txt"

First, and it is the smallest: the startup disappears. Building a map of 200 string keys costs 3.9 µs, once. For a server that is nothing, and it would be dishonest to sell it as the reason. For a short-lived command it is a larger share of a short life, and it is still small.

Second, and this is the real one: the lookup gets faster, because a table known at build time lowers to a decision on the key rather than a hash and a probe. Measured on 200 keys, five runs each:

HashMap nachschlagen                  18.65  17.94  18.60  17.85  17.84   ns
match nachschlagen                     9.05   8.95   9.05   9.01   9.06   ns

×2, and the honest caveat travels with it: that is against the default hasher, and ADR-010 already has the compiler choose a faster hasher where a map’s keys did not come from a remote peer — so for a table like this one the language’s own map is already better than the number above, and the real factor is smaller than two. It is still the argument, and the 3.9 µs is still not.

Third, and it is not a number: the failure moves. A malformed routes.txt stops the build. Today it stops the program, in production, at the moment it is first read — which for a config file is usually startup and for a rarely-taken branch is usually worse.

And the line this example is on the far side of. A perfect hash over keys the compiler can see in the source needs none of this: it is compiler-side staging, ADR-026 §3 calls it a few hundred lines in the emitter with no evaluator of any kind, and it should be built that way. What needs const is that the keys come from a file, which the compiler cannot know and the program should not pay for at run time. That is the boundary between the two tiers, and this example sits just past it.

4. Consequences

Two records stop waiting. ADR-072’s check has a place to hang, and Part II 10.2’s A-half has a spelling. Neither is built by this.

Two status notes shrink, and one parenthesis in Part I 9.2 goes away.

Q4 becomes the only thing between here and compile-time parsing, which is what it was already, now with nothing else in front of it.

5. What is built

D1 only. const is the thirty-third reserved word, so let const = 1 is refused and the note names it, and Part I 2.1’s table lists it.

Not the declaration. const X = 1 is still two statements to the parser. The order this implies is D5’s: the declaration with the fold as its evaluator first, then Q4, then a call and from with it.

This site is open source. Improve this page.