ADR-124: extern and unsafe Are Reserved, Because They Cost Nothing Now and a Boundary Has to Be Visible

Status: 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

1. Context

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.

2. Decisions

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

D2 — An 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.

D3 — A call to an 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.

D4 — What the declaration does not say is fail-closed, as a described crate’s is.

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

3. Consequences

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.

4. What this does not decide

5. What is built

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.

This site is open source. Improve this page.