const Is a Declaration That Must Be Evaluated at Build Time — a let That Merely Folds Is Not OneStatus: 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")
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.
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.
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.
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
letmay fold; aconstmust, 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.
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.
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:
consts — the fold
ADR-063 already shares between the checker and the emitter;dsl … from "…", which is what
ADR-072 is waiting for.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.
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.
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.
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.
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.