update Block Says mut and Nothing Else, May Run More Than Once, and the Compiler Picks the LockStatus: Superseded by ADR-281 — its D1-D5 are D12-D16 there, and D6 is D18
Date: September 16, 2026
Target Version: Nikaia 0.0.8
Related: ADR-059 (access reads, update writes),
ADR-065 D2 (update_all), ADR-039 D1 (poisoning
at yes, and §3’s door to a retry), ADR-067 D1 (a door’s block
is sync and touches no lock), ADR-094 D3 (mut on a
parameter is in-place change, and the caller’s value is what changes),
ADR-037 D7 (plain against atomic, decided per value),
lock-free.md (the measurement), Part I 6.3, Part II 12.2
Locked data has four doors. Three of them were never in doubt: get copies
the value out, set stores one computed outside, access reads in place and
may not change what it is handed. The fourth, update, was handed the old
value and returned the new one — a pure function, chosen so that the moment
the lock opens and the moment it closes are the block’s edges and nothing else,
which is what lets an implementation choose which lock, and whether to
retry instead of locking at all.
Two things were wrong with the form and not with the idea. Part I 6.3 said the
opposite of Part II about access — “not copied to append one” against
“may not change it” — so a reader who trusted Part I wrote a change that the
language below refused in its own words. And the pure form made a large value
pay for being pure: handing a list to the block by value means moving it
out of the lock, writing an empty marker where it lay, and moving it back, so
that push on a ten-thousand-entry list was three lines and a vacated slot a
panic could leave behind.
The way out is one form for the block, and one observation: changing a copy
is the same as computing a new value. Whether v is a copy or the address of
the value in the lock is the compiler’s to decide, by type, and that decision
is exactly the one that decides whether a retry is possible.
update has one form: the block takes mut v, changes it, and returns nothing.kasse.update fn(mut v) { v += 100 }
protokoll.update fn(mut log) { log.push(entry) }
mut on the parameter is ADR-094 D3’s word with its meaning
unchanged: the value is changed in place, and the caller’s value is what
changes — here the caller is the lock. A block that hands a value back is
refused as NK1141, and the message says an update changes v; there is
nothing to return. A block that changes v without the word is NK1138, as
any parameter is; a block that changes nothing is a no-op and is not refused.
access fn(v) { … } stays the read door, hands the value where it lies, and
may not change it (ADR-059 D1). Part I 6.3’s sentence about
appending is withdrawn: a ten-thousand-entry list is not copied to be asked its
length.
v is, is the compiler’s, by type: a copy where the value is a word, the address otherwise.| the value | v is |
the door does |
|---|---|---|
fits a machine word — a number, a bool, a small enum |
a copy on the stack | load the word, run the block on the copy, compare-and-swap it in; on a collision load again and run again |
| anything else — a list, a map, a text, a struct | the address in the lock | take the lock, run the block once, release |
Nothing is moved out of the lock in either row and no slot is ever empty.
Which row a value falls in is read off its type, as ADR-037 D7
reads plain against atomic off a value, and explain reports it. The
programmer writes one form and learns nothing about user_parallelism or word
sizes from it. There is no spelling a program needs for speed.
update block may run more than once, and a program cannot tell.This is the permission the first row of D2 uses, and it is sound because the
three things that would let a program notice are already refused at every
door: the block is sync, so it does not wait; it touches no lock, so it does
no I/O and takes nothing else (ADR-067 D1 — a println fails
this, as it takes standard output’s own lock); and it captures its surroundings
as views (Part I 5.3), so it changes nothing outside v. A block that runs
again on a fresh copy leaves no trace of the run that lost.
ADR-039 §3 left this door open for want of “an additional
assurance which does not exist yet”; the assurance is the touches column,
which exists now. The retry is used only where the copy is free, which is the
same boundary as the first row of D2 — a block on an address runs once, because
a change to the address cannot be taken back.
On a copy, a panic leaves the shared word as it was — nothing was swapped in.
On the address, the value stays in the state the block reached, and what
happens next is what ADR-039 D1 already says: an abort at no,
where nobody is left to observe it, and poisoning at yes, where the next door
on that lock refuses. The empty-slot state that update used to leave does not
exist.
get is the one door that copies, and the report says where it copies something large.get copies the whole value out, by definition. --sharing names every get
on a type that does not copy cheaply, beside the count it already prints, so a
list copied out of a lock is a line somebody reads and not a surprise in a
profile. access is the door for a question about a large value.
update_all is D1 widened: one mut per lock, nothing returned.update_all(konto_a, konto_b) fn(mut von, mut nach) {
von -= 30
nach += 30
}
Two words cannot be swapped together by one compare-and-swap on every target, so the pair takes the lock, in ADR-065 D3’s address order. The rule is D2’s, applied to the tuple of types.
access reads, update writes, and the
two pages say so in the same words.push on a large value is one line, and costs a lock and the push. The
three-line form, the move out and back, and the empty slot are gone.lock-free.md) is the compiler’s to take
for word-sized values, without a program changing.mut was already a thing a parameter may
say.&mut T
for the address row and as a function of a stack copy for the word row, and
std’s lock gains the compare-and-swap path for word-sized T; two
diagnostics; every update in the specification is rewritten.yes: recover, reset to a default, or end the
process. A runtime setting (Part III 13.3b) and not a door. Whoever offers
recover offers a copy taken before the block with it, because a
half-changed value is worse than none.;.D1, D4 and D6, and the address row of D2. fn(mut v) parses, update and
update_all are handed &mut T, the Option is gone and with it the empty
slot — and so is the emptied() panic that named it, because the state it
reported cannot occur. update_all takes one mut per lock and returns
nothing.
The block runs exactly once, which is D2’s address row for every value rather than only for the ones that are not a machine word. D3 licenses that outright — may run more than once is a permission and not a requirement — so what is left of D2 is speed: a copy and a compare-and-swap where the value fits a word.
What the corpus said, and it narrowed a rule. NK1138 at a door is D1’s, and
the first build asked it of every lambda parameter, on the reasoning that
one changed without mut is already broken Rust. It is not:
par_fold(…, fn(acc, m) { acc.record(m) }) in examples/1brc.nika changes
acc, has no mut, and compiles — the emitter writes the word itself where
it recognises a fold’s accumulator, and and_modify fn(tally) { tally.bump() }
in k-nucleotide.nika is the same shape one library over. So the question is
asked at a door and nowhere else, and widening it belongs with whoever takes
that mut out of the emitter.
And Part II 12.2’s counter compiles and runs again. It had stopped lowering
the day the page was rewritten to this form, because a lambda’s parameter did
not take mut; tests/specification/COMPILES.txt carries it as a program
rather than a fragment now.
What is left:
NK1141, and NK1138 where the word is missingstd’s lock over &mut T; the Option goesT is not, and is the speed half;explain naming which are not;--sharing naming a get on a type that does not copy cheaply;NK2205’s help text and the tests that assert the old shape#184 carries the rest.