Status: Accepted
Date: October 3, 2026
History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached
Answers: the open-decisions.md entry are this language’s keywords
reserved? (a name was any identifier, so let fn = 3 parsed); the three rows
of error-corpus.md (B3, C4, D2) that parsed as
statements about a variable named after a keyword;
#235’s missing
diagnostic for a dsl block; whether loop, const, macro and quote
stay reserved with no construct behind them; and let type = 3 lowering to a
file rustc refused with “expected identifier, found keyword type”
(Part III C.1’s class, in every position a name can
stand in)
Related:
ADR-001 D2 (the scannerless grammar: no lexer, so no place a list would have lived),
ADR-296 D5 (Self::dsl, the type spelled with a keyword),
ADR-277 (the fn: … refusal, whose fail shape does not work here),
ADR-292 D2 and D7 (overlap reserved ahead of its construct; un-reserving is the free direction),
ADR-296 D17 (the emitter writes a name for a name),
ADR-056 (a rule and not a list, at the same boundary one position over),
ADR-285 D14 (the backend does not decide this language’s vocabulary, the other direction),
ADR-300 D9 (the source map is by line),
ADR-276 (break, continue reserved with their constructs; loop a name),
ADR-287 (comptime reserved; const a name),
ADR-304 (reflection; macro and quote reserved there, released by D6),
ADR-310 (from, released the same way),
ADR-118 (with’s construct),
ADR-302 (extern and unsafe), ADR-292 (select),
ADR-184 (ref), ADR-278 D4 (null),
Part I 2.1 (the list), Part III C.1 and C.3 (NK1117, NK1119, NK1128)
Which words may a name be? Three things ask it: the words this language uses for its own constructs, the words it once set aside without a construct, and the words the language it lowers to reserves.
Without an answer the grammar admitted every identifier as a name, and that is not a neutral default. It produced programs that meant something other than what was written, with no diagnostic:
if { } // read as: the variable `if`, then a block
let 5 = x // read as: the variable `let`, then `5 = x`
if a = b { } // read as: the variable `if`, then `a = b`, then a block
and names that this compiler accepted and rustc refused about a file nobody
wrote: let fn = 3, let type = 3, struct Event { type: String }.
| Language | Its answer | What it costs there | Source |
|---|---|---|---|
| C# | var, async, await, nameof are contextual: keywords only where they cannot be a name |
a reader must know the position to know the word; done for backward compatibility | D1, from memory |
| Go | every keyword reserved; it never had to add one | none at the time it was decided | D1, from memory |
| Java | goto and const reserved on stock and unused for thirty years; later words came as contextual keywords |
names nobody may write, for nothing | D8, from memory |
| JavaScript | sixteen of its stock reserved words released again in 2009 | the same | D8, from memory |
| Rust | editions to take a new word; a keyword is usable as a name through a raw identifier r#type — except crate, super, self, Self, which cannot be raw |
the escape exists; four words have none | D11, measured against rustc |
What does not transfer: the reason a language makes a keyword contextual is programs that already used the word. When the list was decided Nikaia had no programs outside this repository.
| Option | Lost because | See |
|---|---|---|
| Keep every identifier a name (the state before) | three silent miscompilations and C.1 refusals from rustc; the state was not a decision, only the absence of one |
D1 |
| Contextual keywords | the reason for them is backward compatibility, which does not apply before programs exist | D1 |
A local cut for the dsl block only |
repairs one diagnostic for one construct; the list repairs the class, and every later keyword comes with it | D1 |
A fail("…") arm in NAME |
fail records an error and lets the arm succeed: NAME returned an empty symbol and let fn = 3 lowered to let = 3; |
D1 |
Take self out of NAME |
refuses every method body (self.min is a name) |
D4 |
Keep loop, const, macro, quote reserved for their messages |
the undeclared-name refusal already gives a name nothing declares a sentence (assert); a word does not have to be forbidden to be explained |
D6, D7 |
| Reserve the words the language below reserves | lets the backend decide this language’s vocabulary; type is the field name of every tagged record |
D9 |
Escape once in Emitter::text |
text also keys the checker’s recorded answers by the source’s name; every lookup would miss, on exactly these programs |
D10 |
Rename crate, super, Self to a mangled name |
puts a name the program never wrote into the generated file — the C.1 class by another road | D11 |
The list is printed in Part I 2.1 so that a reader can see it, and is
parser::RESERVED_WORDS in the compiler so that a diagnostic can read it. The
grammar’s check is not(RESERVED) inside NAME, one alternation of the KW_*
rules, each carrying its own not(ident) boundary — so there is still no lexer
and the order of the alternation does not matter (throw does not match the
start of throws).
Reserved rather than contextual, because the reason for contextual keywords is backward compatibility and there were no programs to be compatible with. Which words are on the list is decided by the record that gives each its construct, not here.
rule, boundary, fold, par_fold and unchecked are keywords inside a
grammar block (Part II 10.1) and names everywhere else. A reserved word is one
the reader of an ordinary statement could mistake for a construct, and no
Nikaia statement begins with fold(.
:: and after ., a reserved word is a name.A segment follows a :: and a member follows a .; no construct begins in
either position, so nothing can be misread: Self::dsl, scope.spawn fn { … }.
In the grammar this is SEGMENT, NAME without the check. Every position that
declares a name, or where one stands alone, uses NAME, and that is where
the list applies.
self is on the list and is also the receiver’s name, so the checker refuses declaring it.NAME both declares and refers, and excluding self from it would refuse
every method body. So declaring self is NK1119 — at a let, a for
binding, a lambda’s argument and a struct field (the field has a span of its
own so the caret lands on it) — which can also say that it, this and me
are free. A parameter named self never parsed (the receiver rule takes
the word); its arm in receiver consumes the colon rather than peeking it, so
it reaches further than the bare alternative and its sentence wins.
The headline is the grammar’s and is about the position; a note names the word, says it is reserved and so not a name (Part I 2.1), and says to pick another name or that the construct does not belong there. The note is read off the rendered message, and where the shape it looks for is absent the note is absent — it adds a sentence and corrects nothing.
loop, const, macro and quote are names.No construct of this language stands behind them (D8), so they are ordinary
names: for loop in loops, let quote = '"'. The language below reserves loop, const and
macro, and they are answered as type is — escaped (D9).
Where one of them stands as a statement and nothing declares it, it is
NK1117 like any undeclared name, and the help carries the sentence:
| written | help |
|---|---|
loop { … } |
write while true { … } (Part I 3.3) |
const X = … |
write comptime X = … (Part II 10.2) |
macro …, quote … |
Nikaia has no macros; generating code from a type’s shape is comptime and a bound (Part II 10.3) |
Each sentence ends by saying the word is an ordinary name otherwise. Where the word is declared, nothing is said.
That is the one ground for reserving: with is reserved because its construct
— a copy with named fields changed — is ADR-118’s. A word with no
construct goes the way of D6.
type becomes r#type at every position the emitter writes a name — a local, a
parameter, a field, a function’s own name, a struct, an enum, a variant, a
lambda’s argument, a for binding — and stays type in the source, in every
diagnostic and in the ledger. D1’s rule is about words this language needs;
these are not, and reserving them would let the backend decide the vocabulary.
gen, which Edition 2024 reserves, is one of them (ADR-305 D6).
The cause is ADR-296 D17 writing a name for a name, so the fix is
where the name is written.
text.escaped(name) is written; name is looked up. text keys the checker’s
recorded answers (pausing_methods, fallible_methods, nullable_fields) by
the source’s name, and escaping there would make each lookup miss.
crate, super, self and Self are refused, because the language below has no escape for them.Rust refuses r#crate and the other three. So crate, super and Self are
NK1128 at every position that declares a name, and the diagnostic says this
is a rule about the target, not a keyword of this language — Part I 2.1 does not
list them. self keeps NK1119 (D4), which says more.
NK1119 and NK1128 are asked at the same positions, so Checker::nameable
asks both, and a declaring position added later gets both - an item’s own name
(struct crate) included.
let fn = 3.union is accepted
by Rust and needs nothing.if { }, let 5 = x and if a = b { } are refusals, not other programs.struct Event { type: String } compiles and runs. The generated file says
r#type, which only a reader of that file sees; let mut loop = 1 lowers to
let mut r#loop = 1.tests/reserved_below.rs compiles every escaped word in every position and
checks the refused ones at each declaring position, so the set is measured
again whenever the language below changes.crate, super
and Self available at the cost of a name in the generated file the program
never wrote. If those words ever matter, that is the trade to reopen.x.type() on a
std type is whatever the ledger says.union. Rust takes it as a name today; if a future edition makes it a
keyword, the sweep is what will say so. (gen is already reserved by Edition
2024 and escaped, D9.)=>.
It does not change D1’s order of preference, a categorical repair before a
local one.