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.
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.
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.
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.
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.
docs/automatic-clone.md collects them, and each is
answered by a different half of D1 rather than by one argument.
Shared[T] by hand; what is automatic is the handle, never the
decision to share. And the cleanup point here is decided by D3’s block rule,
which a reader can see, rather than by an analysis they cannot.Owned is never automatic.
Untouched: no view is promoted, and a handle is not a view.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.
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.
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.