ADR-312: A Handle on a Shared Value Is Duplicated, Never Moved; It May Go Into a Task of Our Own and Not Into Code Nothing Describes; and at One User Thread Its Count Is Plain

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)

1. Question

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:

  1. How does the second owner come about? The obvious spelling, .clone(), already means copy the data for data. One spelling with two opposite meanings would be told apart only by the receiver’s type.
  2. Where may a handle go? The crossing verdict may not read the switch, so if it is asked of a type alone it takes the worse of the two settings, and a 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.
  3. Which count does a handle carry when nothing can cross? At 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.

2. How others do it

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.

3. Options, weighed

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

4. Decision

A handle

D1 — A handle on a shared value is duplicated, never moved. Each handle dies at the end of its own block.

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.

D2 — The duplication is unconditional, not dependent on whether the value is used again.

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.

D3 — What the simple rule costs is the block rule, applied unchanged.

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.

D4 — This reopens none of the rules against an automatic clone.

D5 — The move refusal does not apply to a shared value, and every duplication site is shown where the count is.

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

Where a handle may go

D6 — The crossing verdict is asked of a value and a destination, not of a type alone.

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

D7 — Into a task of our own, a lock may go, at both settings.

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.

D8 — Into foreign code, a lock may not go, at both settings. This is conservative, and chosen.

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.

D9 — A 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.

Which count a handle carries

D10 — At 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.

D11 — A contract stays at the floor.

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.

5. Consequences

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

6. What this does not decide

This site is open source. Improve this page.