ADR-099: The Two Refusals That Come With the Lock’s Doors

Status: Superseded by ADR-281 — its D1-D3 are D20, D21 and D25 there; D4 is not carried Date: September 15, 2026 Target Version: Nikaia 0.0.8 Related: ADR-039 D10 (which decided the four doors and stated these two mistakes), ADR-064 (the type they are about), ADR-081 D2 (the expression span this does not have)

1. Context

ADR-039 D10 gave shared mutable state four doors — get, set, update, access — and named the two mistakes that come with them. Part III Appendix C catalogued both, and nothing raised either:

error[NK2204]: `kasse` holds shared mutable state, and this assigns to it directly
error[NK2205]: this `set` reads `kasse` while computing what to store in it

That is the state that rots fastest: a check with no program to be tested against is correctness that quietly stops being true, because nothing exercises it. The lock itself has been built since ADR-064, and Part II 12.2’s counter compiles and runs at both settings, so these had a type to be about and no reason left to wait.

2. Decisions

D1 — NK2204: an assignment straight into a SharedMut.

kasse = 42 looks like an ordinary assignment and is not: the lock has to be taken for the write, and what takes it is a door.

The message names the door with the value in it. The repair is mechanical, and a help line that said “use a door” would leave the reader to work out which of the four.

D2 — What cannot be quoted back is not quoted.

An expression has no span — ADR-081 D2 gave one to Binary and to nothing else — so there is no source text to put in the help. A literal, a name and a negation are rebuilt; anything else is an ellipsis. A help line that quoted the wrong thing would be worse than one that quotes nothing, and the shape the specification’s own example writes is a literal.

D3 — NK2205: a set given a value that was seen in a lock.

kasse.set(kasse.get() + 100) takes the lock twice — once to read and once to store — and between the two the value can change, so what is stored is computed from a state that may no longer hold. The third door exists for exactly this and takes the lock once.

What makes the refusal reach past the one line is the stamp: what a lock hands out is a Seen[T], the stamp travels with the value through let, arithmetic, calls, fields and time, and a set given a stamped value — or standing under a stamped condition — is this refusal wherever the read was (ADR-111 D1–D4). The get written inside the set is the smallest case of the first shape.

D4 — These two, and not the other three the same entry lists.

NK2201 (no I/O while holding locked data) and NK2203 (a lock taken while a lock is held) need to know what is inside a door and what a chain of calls reaches; NK2503 needs the reachability walk (ADR-039 D6). These two need one statement each, and shipping the two that are local now beats shipping five that are half-analysed.

3. Consequences

Nothing in examples/, tests/samples/ or crates/nikaia-std/src/ is refused, which is what a refusal added to a language with programs in it owes. The walk over a set’s argument is the emitter’s visit_expr, made pub(crate) rather than copied: a second walk over the same shape is a second thing to keep in step with the AST.

#175’s entry shrinks rather than closes — three of its five codes are still catalogued and not emitted, and the entry now says which and why.

4. What this does not decide

5. What is built

Checker::a_write_that_skips_the_door and Checker::written for NK2204; Checker::a_set_that_reads_what_it_writes and Checker::a_get_inside for NK2205, the second asked at the MethodCall arm rather than in call_on because the rule is about the receiver’s name and that arm is handed a type. emit::visit_expr becomes pub(crate). Exercised by seven programs in crates/nikaia/tests/lock_doors.rs — both messages against the ones Appendix C prints, the ellipsis, the get found three levels down, the three shapes that must stay quiet, an ordinary assignment, and the corpus.

This site is open source. Improve this page.