ADR-298: A Word This Language Uses Is Reserved and Only for a Construct, and a Word the Language Below Reserves Is Escaped Where It Is Written

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)

1. Question

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 }.

2. How others do it

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.

3. Options, weighed

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

4. Decision

D1 — There is a list of reserved words, and a name may not be one of them.

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.

D2 — The list is the Nikaia level; the grammar sublanguage’s words are not on it.

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(.

D3 — After :: 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.

D4 — 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.

D5 — A parse error over a reserved word says so.

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.

D6 — 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).

D7 — What a released word used to be told, the undeclared-name refusal tells.

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.

D8 — A word is reserved for a construct of this language, built or decided.

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.

D9 — A name the language below reserves is escaped where the emitter writes it. It is not reserved here.

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.

D10 — The escape is at the emitter’s name position, never at 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.

D11 — 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.

D12 — The two questions asked of a declared name are asked by one function.

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.

The numbers that decided it

5. Consequences

6. What this does not decide

This site is open source. Improve this page.