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: how a second handle on a Shared[T] comes about, and whether
.clone() is the operation; whether a SharedMut[T] may be handed to a task
when the crossing verdict may not read user_parallelism, which is what made
Part II 12.2’s counter unwritable; whether a Shared[T] may be handed to Rust
code nothing describes, and which count it gets at user_parallelism = no
Displaces: ADR-005 §3’s ban on a clone the user did not
write, for a handle on a shared value only (D1, D4); ADR-037
D6’s atomic floor, at user_parallelism = no only (D10)
Related:
ADR-005 §1 Group B (a verdict may not consult the switch — kept by D6),
ADR-037 D2, D3, D7, D8 (one user thread at no; Shared is written, never inferred; the per-value count inference; the cross-file floor),
ADR-303 D7 (a Rust crate may start threads of its own),
ADR-042 D1, D2 (the borrow that makes lending the inner value writable),
ADR-281 D2, D3, D5-D7, D30, D32 (hull constructors; SharedMut[T]; the lock’s shapes; the action at a distance D2 refuses; a foreign call judged by what it reaches),
ADR-283 D3 (promotion to Owned is never automatic — untouched),
ADR-286 D15 (a package is built at the settings of the program that uses it),
ADR-290 D7 (a described call is asked too, by its threads claim),
ADR-292 (statement overlapping, a destination of our own),
ADR-258 (the four weights)
Shared[T] and SharedMut[T] exist to have several owners. Three things have
to be answered for that to be a type a program can use:
.clone(),
already means copy the data for data. One spelling with two opposite
meanings would be told apart only by the receiver’s type.SharedMut[T] cannot be handed to a task — which makes Part II 12.2’s
counter, the program user_parallelism = yes exists to serve, unwritable.user_parallelism = no a lock is always the cheap shape, so a count kept
atomic as a floor would emit an atomic count around a non-atomic lock: a shape
that is right in neither world.The comparison is with one language: Rust, which is what the compiler emits and what a foreign call reaches.
| Language / tool | Its answer | What it costs there | Source |
|---|---|---|---|
| Rust | the programmer picks Rc<T> or Arc<T>, and the second handle is a written .clone() — the same method name that copies data |
a wrong pick stops compiling where the value crosses, often far from where it was chosen | from memory, not checked |
| Rust | a lock is RefCell<T> or Mutex<T>, picked by hand, and a library’s function has one signature naming one of them |
a library written against one form does not accept the other | measured on emitted code |
What does not transfer: in Rust the representation is written, so one signature can name it. Here it is chosen per value and per setting (ADR-037 D7, ADR-281 D6-D7), so no foreign signature can name it — which is the whole of D8 and D9.
| Option | What it is | Why it lost |
|---|---|---|
.clone() makes a second handle |
the existing method, read by type | one name, two meanings (copy the data / add an owner); and a method call is an origin the count analysis cannot see, so it falls to the atomic count in exactly the single-threaded case the plain count serves |
| A new method for a second handle | a name for the operation | there is nothing to decide at the call: the handle is cheap and the type already says several owners; D1 removes the operation and so the naming question |
| Duplicate only if the handle is used again | move on last use, duplicate otherwise | a later line decides when an earlier value is cleaned up — the action at a distance ADR-281 D30 refuses for the lock analysis (D2) |
| Verdict asked of a type alone | the worse of the two settings | a lock is refused into a task even at yes, where a real operating-system lock is underneath; 12.2’s counter cannot be written |
One lock implementation, a Mutex floor |
make the lock switch-independent | pays the robust lock in every single-threaded program; not taken |
Foreign call: answer may at yes |
exact rather than conservative | the answer would depend on the switch, and a library written at yes would compile there and fail at no — the asymmetry ADR-005 §1 Group B exists to prevent |
Keep the atomic count floor at no |
count independent of the switch | pays for crossing on one half of a value that the other half (the cheap lock) forbids; and the only caller it protected, Rust, is closed off by D9 |
Let Shared into foreign code |
accept it | no signature names both an Rc and an Arc of one type, and both occur in one program; it works only where the analysis happens to force the atomic count |
There is no method to call, so there is no name to choose and no second meaning for an existing one. The handle is duplicated where it is handed on by value — passed to a function that keeps it, or used by a task. Where the inner value is lent out, nothing is duplicated and the count is not touched: a borrow hands nothing on (ADR-042 D1, D2 make that writable). This decides whether a function that only uses a shared value pays an atomic instruction.
“A shared value” is Shared[T] and SharedMut[T] alike. Locked[T] is a
field inside one, not a handle on one, and goes with whatever holds it.
Only for a shared value. For ordinary data a move stays a move: an automatic clone there would be an invisible copy of the whole of the data.
Duplicating only where the value occurs again, and moving otherwise, is refused. It would let a line further down decide when a line further up cleans its value up — the action at a distance ADR-281 D30 refuses where it would make unchanged code stop compiling. Here it would make unchanged code free its memory somewhere else.
A handle of one’s own lives to the end of its block even where it is not touched again, so the value is cleaned up there rather than when the task that holds the other handle finishes. A program that wants it earlier ends the block earlier. Both variants are predictable; this one is explained in one sentence.
Shared is still written, and it is made by a written call
(ADR-281 D2). What is automatic is the extra handle, never the decision to
share.Owned: untouched. A
handle is not a view.Part I 8.3’s NK2101 — a task took a value that is used again afterwards — is
about data. For a shared value it is the case D1 serves, and is not raised.
A duplication costs one atomic instruction where the count is atomic, and D1
makes the place it is paid unwritten. So --sharing, which already prints each
value’s count and why, names each duplication site beside it: the cost is
something a reader can look up (ADR-037 D7’s rule that what is
observable is written, applied to a cost).
The question is may this value go to this destination. Each destination
gets one answer, the same at both settings, which is all
ADR-005 §1 Group B asks: the reason may differ per setting, the
answer may not. Only the severity may differ (at no a refusal into a task is
a lint, since nothing runs concurrently).
At yes an operating-system lock is underneath, and several threads are what
it is for. At no the task is interleaved on the one user thread, so nothing
crosses. Two reasons, one answer. A SharedMut[T] may be captured by spawn,
and Part II 12.2’s counter is a program.
The duplicated handle (D1) is the occasion for the question, not its answer: two handles are two places at one lock. The verdict is about the lock, not the count.
The closure statement overlapping builds (ADR-292) is our own code on our own thread, so it is this destination: a result that holds a lock may be overlapped.
Strictly, at yes it would be safe: a real lock is underneath. It is refused
anyway, because a lock’s Rust type follows the setting (Mutex at yes, a
RefCell-like cell at no), a Rust library has one signature, and so a
library written against one setting would fail at the other. Whoever trips over
this should know it was chosen, not missed.
The refusal is judged by what the call’s arguments can reach, transitively
through fields (ADR-281 D32), and a described call is asked by its
threads claim (ADR-290 D7). The way out: the caller opens
the lock and passes the inner value. The foreign function sees an ordinary
value and no lock.
Shared may not go into code nothing describes.The same refusal a lock gets, for the same reason: a Rust library has one
signature, and a Shared[T] is a plain count for one value and an atomic one
for another in the same program, so no foreign signature names it. It is
refused in this compiler’s words rather than left to rustc’s about a type the
program never wrote.
The way out is to pass what is inside, a view or a copy; a foreign library that keeps a value puts it in a hull of its own.
A bridge type is reserved and not decided: a hull whose representation a
program names, for the case where the hull itself must cross. It is the answer
if a program shows that passing the contents is not enough. Its name would not
be Rc or Arc, which are the machine’s words.
user_parallelism = no, every count is plain.Nothing a user writes can cross a thread there: one user thread
(ADR-037 D2), the runtime’s I/O thread runs no user code, a
package is built at the same setting as the program that uses it
(ADR-286 D15) - std’s Nikaia half, lowered once for both
settings, holds no Shared and is kept switch-insensitive by a test
(ADR-002 D4) - and D9 closes the way out into Rust. So no count
needs to be atomic, and the shape atomic count around a cheap lock does not
occur. At yes the count is decided per value, with the atomic count as the
floor (ADR-037 D7).
The verdict is untouched. No answer about a type reads the switch (D6). This is about what is emitted, not what is permitted.
The sharing column a function writes into the ledger is a contract, read by a
person and by a later build. It is computed at the floor whatever the switch
says, since a contract that moved with a build switch is what D6 refuses. The
emitter and --sharing ask the same analysis with the setting in hand; the two
questions have different answers.
For a program. A second owner is written by handing the handle on; there is
no call. A function that only reads a shared value takes a view of what is
inside. A SharedMut[T] goes into a task at both settings. Neither a lock nor
a Shared goes into Rust code; what goes is the inside. At no every count
and every lock is the cheap shape.
For the specification. Part I 6.2 says how a second handle comes about and
that lending duplicates nothing; Part I 8.3 and Part II 11.2 separate the move
of data from the duplication of a handle, and NK2101 belongs to data. Part II
12.2 and Part III C.5 state one verdict per destination; Part III 15.2 and 15.3
state the foreign refusal and the plain count at no.
For the compiler. The crossing walk takes a destination, and the lock and
Shared are the names whose answer depends on it; every other name answers the
same everywhere. At no the count analysis flattens every count after deciding,
so one run is one code path at both settings; the duplication sites, which are
about handing a handle on rather than about threads, do not move. Because every
file of a no build is plain, two files cannot disagree on a slot’s count
there; at yes the floor is what makes them agree (ADR-037 D8).
What it costs. A handle lives to the end of its block (D3). A lock cannot be
handed to Rust even where, at yes, it would be safe (D8). A Shared handed to
a Rust function is refused, even where its count is atomic (D9).