ADR-040: A Handle on a Shared Value Is Duplicated, Never Moved

Status: Superseded by ADR-312 — its D1-D5 are D1-D5 there Related: ADR-005 §3 (the rejected implicit clone — the one sentence this displaces, and only for a handle), ADR-037 D3 (that Shared is written and never guessed — a premise this rests on rather than displaces), ADR-283 D3 (promotion to Owned is never automatic — untouched, and why), ADR-037 D6 (the count a handle carries) and D7 (the rule that what is observable is written), ADR-281 D30 (the action at a distance D2 refuses here for the same reason). Evidence and the question this answers: docs/automatic-clone.md.

1. Context

Shared[T] is the type whose whole purpose is several owners, and the specification never said how to obtain the second one. Two halves of the project had each assumed a different answer.

contracts::sharing recognises a second handle only as a bare let b = a: the analysis is union-find over names, so two names on one allocation share one count verdict. Part II 11.2, meanwhile, offers the only instruction a Shared holder finds at all — “call .clone() before spawning” — written about ordinary data, where it means copy the data. And Shared has no constructor either: there is no Rc::new or Arc::new anywhere in the emitter, so making one is unwritten beside copying a handle on one.

.clone() as the answer would also have been mechanically wrong today. let b = a.clone() is a method call, which names_a_handle does not recognise, so it falls to Fallback::UnseenOrigin and is forced to the atomic count — losing the plain count ADR-037 D7 exists to serve, in exactly the single-threaded sharing case it was meant for.

There was a naming question in this: one spelling with two opposite meanings, told apart only by the receiver’s type, in a language that cares about that distinction. This record does not answer it. It removes it.

2. Decisions

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

That is the whole rule. There is no method to call, so there is no name to choose and no second meaning for an existing one.

(Corrected on first use. D1 said a handle is duplicated where it is “handed on - passed to a function, or used by a task”. Read literally that made every serve(&db) duplicate a handle, which is the opposite of what it should do: a borrow hands nothing on, so there is nothing to duplicate and no atomic instruction to pay. So: duplicated where the handle is passed by value, and untouched where the inner value is lent out — which ADR-042 D1 and D2 are what make writable at all. The distinction is not a wording preference: it decides whether a function that only *uses a shared value touches the count.)*

(Scope, stated rather than left to be inferred: “a shared value” is Shared[T] and SharedMut[T] alike — ADR-281 D3 makes the second one the shared mutable type, so it is a shared value by construction. Locked[T] is a field inside one rather than a handle on one, so it is carried by whatever holds it. That a SharedMut[T] cannot cross a thread at all (ADR-281’s open half) narrows where the rule has anything to do, and does not change what it says.)

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, which is a different thing at a different cost.

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

The variant considered and refused: duplicate only where the value occurs again after the handover, and move otherwise. The compiler has the information to do it.

But then a line further down decides what a line further up does — and what it changes is when the value is cleaned up. That is the same action at a distance ADR-281 D30 refuses for the precise lock analysis, where an unrelated line elsewhere could make unchanged code stop compiling. Here it would make unchanged code free its memory somewhere else. It is not better for being about cleanup rather than compilation.

D3 — What the simple rule costs is this language’s block rule, applied unchanged.

A handle of one’s own lives to the end of the enclosing function even where it is never touched again, so the value is cleaned up there rather than when the task that used the other handle finishes.

That is not a special case to be learned. It is the ordinary block rule, not bent for this. Whoever wants earlier cleanup ends the block earlier. Both variants are predictable; this one is additionally explainable in one sentence, which the conditional one is not.

D4 — Why this reopens none of the three records that forbid an automatic clone.

docs/automatic-clone.md collects them, and each is answered by a different half of D1 rather than by one argument.

D5 — Two consequences, and both have to go with the rule.

The move refusal stops being a refusal for a shared value. Part I 8.3’s NK2101 — “this background task takes ownership of message … but message is used again afterwards” — is, for a shared value, no longer an error. It is the trigger that D1 now serves automatically. For ordinary data it stands unchanged, word for word.

(Nothing is switched off by this. NK2101 is catalogued and never raised: the code appears nowhere in the compiler, so this is a rule the check has to be built to rather than one to remove.)

The atomic instruction becomes invisible, so it is written where the count already is. An extra handle costs one atomic instruction, and D1 makes the place it is paid unwritten in the source. --sharing already prints, for every Shared value, which count it was given and why; a duplication site belongs in that output. Then the cost is something a reader can look up instead of something hidden from them — which is ADR-037 D7’s rule about observability applied to a cost rather than to a semantics.

3. Consequences

What this displaces. One sentence: ADR-005 §3’s ban on a clone the user did not write, for a handle only. And D1’s own first wording, which said “passed to a function” where it meant passed by value: lending the inner value out duplicates nothing, which is the case ADR-042 gives a type to. Every other record the question touches is answered rather than displaced (D4), and the naming question that would have needed a decision stops existing (D1).

What the specification has to gain. Part I 6.2 must say how a second handle comes about, because that is the operation the type’s purpose depends on and it is missing today. Part I 8.3 must separate the two cases — a move for data, a duplication for a shared value — and its NK2101 example must say which it is about. Part II 11.2’s .clone() bullet must stop being the instruction a Shared holder finds first, since for them there is nothing to call.

What is still missing after this, and is not decided here. Shared has no constructor. D1 says what happens to a handle that exists; it does not say how the first one is made. That is a separate question and this record does not answer it.

What does not change. The correction that a .clone() belongs outside the task rather than inside it stands exactly as it is. The example it was found in holds a string — ordinary data — and for ordinary data this record changes nothing.

4. What is built

Built for a function, not for a task. Shared[T] is a type a program can write, so D1 is real: a handle handed on by value to a call whose parameter takes one is duplicated, a handle read out of a field likewise, and a borrow duplicates nothing — which is D1’s correction above and the half most easily got wrong, so it is tested in both directions in a program that is compiled and run. D2’s unconditionality and D3’s block rule ride on that. D5’s second half is built too: --sharing names each duplication site beside the count.

What waits on spawn. “Used by a task” cannot be exercised end to end, because the emitter answers spawn with “needs the runtime integration; not emitted yet” — so that half of D1 is tested at analysis level only, and D5’s first half has no NK2101 to exempt because nothing raises one.

And two places a duplication is real but not visible, both gaps rather than decisions: a duplication that lands on a struct field’s slot gets no line in --sharing, which reports the values a person wrote; and a site names the callee and the parameter rather than a line number, because an Expr carries no span in this AST.

This site is open source. Improve this page.