extern and unsafe Are Reserved, Because They Cost Nothing Now and a Boundary Has to Be VisibleStatus: Superseded by ADR-302 — its D1-D4 keep their numbers there
Date: September 16, 2026
Target Version: Nikaia 0.0.8
Related: ADR-298 D1 (a reserved word is not a name),
ADR-276 (a keyword is the most expensive thing a language adds,
and the measurement that decides one), ADR-298 (four words taken
off the list, and the rule for when one belongs on it),
ADR-290 (a crate is described before it is called — the same
boundary, one language over), ADR-024 D1 (?, the absence of a
claim), ADR-284 (letting C call in, which is the other
direction), Part III 15.1
Part III 15.1 writes C interoperability out in full —
an extern "C" block of declarations, and a call inside unsafe { … } — and
the language has none of the three things that example needs. Each was
checked rather than recalled:
| written in 15.1 | what happens today |
|---|---|
extern "C" { … } |
expected end of input; found extern — the word is not reserved (Part I 2.1) and the item is not in the grammar |
unsafe { … } |
parses as a name and a block, and is NK1117, nothing declares unsafe |
Pointer[u8] |
NK1135, nothing declares the type Pointer |
Two of the three are reserved words, which ADR-276 calls the
most expensive thing a language adds, and ADR-298 has just taken
four off the list. A record that puts two back is going the other way and owes
the same number ADR-276 paid for break.
The number is zero. No program in examples/, no fixture in tests/, and
nothing in the three specification pages writes extern or unsafe as a name.
The only occurrences anywhere are a comment about Rust’s unsafe impl Send and
15.1’s own block. Nothing is released, so the corpus is the whole cost, and the
corpus costs nothing.
extern and unsafe join the reserved list, with their constructs.Not reserved ahead of a construct, which is exactly what
ADR-298 has just undone for four words: a word reserved so that a
reader can be told something is a word NK1117’s help can tell them about
instead, at no cost to the grammar. These two are reserved because they have
constructs in the same change — the block and the call site — and a construct
is what makes a word carry its own weight.
The free moment is now, which is the argument overlap was reserved on and
break was measured on: a word costs nothing while no program uses one and
breaks programs afterwards. Nothing is released.
extern "C" declaration is sync and cannot throw, without writing either word.A declaration with no body reads like a function type
(ADR-277 D7, ADR-288 D24): without sync it may
pause, without throws it cannot fail. The first half is turned around here,
and the second is not.
Pausing. Those records are about code somebody will write in this language,
where a suspension point is a thing that exists. C has none. A C function that
sleeps blocks a thread, which is a different question with a different
answer — the one println already has (Part II 12.1, ADR-288 D21) —
and calling it pausing would make every C call an .await of a future nobody
produced. So an extern declaration is sync, and the word is not written
because there is nothing it could be contrasted with.
Failing. C has no failure channel this language reads: a return value that
means “it went wrong” is a value, and a program does with it what the
declaration says. So an extern declaration carries no throws, which is what
a body-less declaration carries anyway.
extern name is written inside unsafe { … }, and nowhere else.That is the whole of what the word buys, and it is why it is a word rather than
an attribute: the boundary is visible at the call, in the body somebody
reads, rather than in a file beside it. unsafe is an expression — a block with
a value, like any other — and it makes no other rule: what is inside is checked
exactly as anything else is.
A call to such a name outside one is refused (NK1143), naming the block to
write. It is this compiler’s refusal and not the language below’s: Rust would
answer about a generated file, which is Part III C.1.
touches is everything and locks is may, which is
ADR-290 D3’s last row and holds here for a stronger reason: a C
signature says less than a Rust one, not more. An argument type the language
cannot name is ? (ADR-024 D1), and what a program may then do
with such a value is what it may do with any ?.
extern "C" { fn getpid() -> i32 } is a program, and unsafe { getpid() } is
how it is called. The list goes from 33 words to 35 — and up from 33 rather than
from 37, because ADR-298’s four leave first.
Part III 15.1’s example does not compile at the time of this record, and §4
says why: Pointer[u8] is a type nothing declares, and this record does not add
one. What it makes writable is every C function whose signature names types this
language already has, which is most of libc’s arithmetic and process surface
and none of its memory surface. (ADR-147 answered §4 and made the
memory surface writable too; the page leads with strlen now.)
NK1117’s help loses two entries and NK1135 keeps one. nothing declares
unsafe stops being the answer, because the word is a construct now.
Pointer[T]. A pointer that outlives what it points at is the one thing
this language is built not to allow, and a type for one wants a record of its
own — with a lifetime story, not just a name. Until there is one, a
declaration whose signature needs a pointer cannot be written, and that is a
refusal rather than a silence.unsafe on anything but a block. Not on a function, not on an impl, not
on a trait. One form, and it is the one 15.1 writes.unsafe too. It does not today and
this does not change it: ADR-290 makes that boundary a described
ledger entry, which is a different mechanism for a different guarantee — Rust’s
signature is checked, C’s is asserted.All five steps. The two words are reserved with their constructs, which
is D1’s condition rather than an accident of timing: extern "C" { … } is an
item and unsafe { … } is an expression, in the same change. The declarations
are ledger entries, sync and without throws (D2), with touches and locks
absent (D4). NK1143 is a call to such a name outside an unsafe block (D3).
The lowering is Rust’s own on both sides — the form means the same thing in both
languages — and extern "C" { fn getpid() -> i32 } with unsafe { getpid() }
compiles and prints a process id.
Two things are worth naming because they are the shape rather than the code.
A declaration is written as a trait method in the AST, because a signature
without a body is the same shape wherever it stands; what differs is how it
reads, and that is D2’s rather than the grammar’s — the emitter is handed
sync by its caller for exactly that reason. And a second ABI is refused by
this compiler rather than by the backend: the grammar takes any string so that
the message about an unknown one names what was written, instead of arriving
about a file nobody wrote (Part III C.1).
Two baselines moved and both moved the right way. unsafe now appears among
the things an expression may start with, and extern among the things an item
may, in the parser’s own also possible here lists. And 15.1’s own block went
from fragment to refused … [NK1135]: the page is a program this compiler
reads and turns down in its own words — nothing declares the type Pointer —
rather than a shape the grammar could not take. That is the whole of what §4
leaves open, said by the compiler instead of by a note.
One arm had to be added for that to be true. The walk behind NK1135 did not
visit an extern declaration’s types, so Pointer[u8] would have reached
rustc intact and come back about a file nobody wrote — the class that refusal
was built for.
§4’s is answered elsewhere, and the page’s example is rewritten.
ADR-147 took the question this record left open and did not answer
it with a pointer: a buffer is a view that lives for the call and a handle is
an opaque type the language never dereferences, so a dangling dereference is
impossible by construction rather than by review. malloc stays unwritable by
design (that record’s D5), which is why Part III 15.1 now leads with strlen —
and that block compiles and runs.