ADR-045: The Crossing Verdict Takes the Destination

Status: Superseded by ADR-312 — its D1-D3 are D6-D8 there Related: ADR-281 (the open half this answers, and the two ways out it set down), ADR-005 §1 Group B (the uniform-verdict rule this keeps rather than bends), ADR-037 D6 (whose “no type answers may not” this ends), ADR-038 D7 (the foreign-thread rule D3 applies), ADR-042 D1 and D2 (the borrow that makes the alternative to D3 writable).

1. Context

ADR-281 D4 kept one lock implementation per setting, and §3 of that record wrote down what it had not: a lock’s implementation follows user_parallelism, the crossing verdict may not consult the switch, so the verdict takes the worse of the two settings and answers may not. A SharedMut[T] therefore cannot be handed to a task, and Part II 12.2’s counter — the program user_parallelism = yes exists to serve — cannot be written.

The sharp form of it: at yes the cheap implementation is not built at all. What stands in the emitted program is a real operating-system lock, under which that counter is entirely safe, and it is refused on account of an implementation that does not occur in it.

§3 set down two ways out. This is the first.

2. Decisions

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

What is judged stops being “may this type be on a foreign thread in the worst case” and becomes “may this value go to this destination”. There are two destinations and each gets an answer that is the same at both settings, which is the whole of what ADR-005 §1 Group B asks — its own note already says “‘At every setting’ is a claim about the verdict, not about the severity.” The justification may differ per setting; the answer may not.

D2 — Into a task of our own: may, at both settings.

At user_parallelism = yes a real operating-system lock is underneath, and several threads are what it is for. At no the task is interleaved on the same thread, so nothing crosses at all. Two different reasons, both sound, one answer.

The duplicated handle is the occasion for the question and not the answer to it. Two handles mean two places at one lock (ADR-040 D1). The owner count plays no part: it is the robust kind at both settings since ADR-037 D6. This is about the lock and nothing else.

With D2, 12.2’s counter is writable. That was the point.

D3 — Into undescribed foreign code: may not, at both settings. Conservative, and not exact.

This is deliberately the worse answer, and the record says so rather than implying a proof.

Strictly, at user_parallelism = yes it would be safe: a real lock is underneath and a foreign thread may touch one. So here the answer really does depend on the switch — and the cautious one is taken anyway, because the rule of equal verdicts would otherwise break for this case. Anyone who trips over this in two years should see that it was chosen.

What happens instead, and is untouched: the caller opens the lock and passes the inner value in. The foreign function sees an ordinary number or connection and no lock at all — the same shape already settled for ordinary functions (ADR-042 D1, D2, Part I 6.2).

3. What D3 costs, measured rather than argued

The cost was first estimated as near zero on the grounds that the Rust type would differ per setting while a Rust library has one signature — so the case would barely be writable. The measurement moves that argument, and the conclusion survives it.

The count half does not depend on the switch. A handle handed to a call nothing describes is forced to the atomic count, and --sharing says why:

atomic  `db` (Shared[Conn])
         could not decide: nothing written down describes `fremd::irgendwas`,
         so this compiler cannot see the end of it - and starting a thread of
         its own is among the things it may do (ADR-038 D7)

So a Shared[T] that reaches foreign code is Arc<T> at both settings, and for that type the foreign case is perfectly writable.

The lock half does. SharedMut[T] is a count around a lock, and the lock is what follows user_parallelism (Part II 12.2). At a foreign call it is therefore Arc<Mutex<T>> at yes and Arc<RefCell<T>> at no. A Rust library has one signature, so it matches at most one setting — and a library written against the yes form would compile at yes and fail at no.

So D3’s restriction is not empty; it is exactly reachable, and exactly the asymmetry ADR-005 §1 Group B exists to prevent. Refusing at both settings is what keeps a library written at one usable at the other. The argument rests on the lock alone, which is also the only thing the crossing question was ever about.

4. Consequences

Part II 12.2’s counter becomes writable, and ADR-281’s open half is answered rather than left standing. The second way out it named — one implementation, the Mutex floor under another name — is not taken, and the measurement that priced it is unchanged.

ADR-037 D6’s coda ends. It said that after it “no type answers may not: NK2501 and NK2502 are built and fire on no program this language can write”, and named the lock as the arm’s candidate. Under D3 the lock answers may not at a foreign call, so the arm has its occupant — but under D2 it answers may into a task, so NK2501 and NK2502 no longer share one verdict. Part III C.5’s “built, and no program reaches it” goes with it.

ADR-005 §1 Group B is kept, not bent. Its rule is that the verdict may not consult the switch; D1 gives two verdicts, each switch-independent, and its own note about severity already draws the distinction D1 relies on.

What it costs in the compiler, from the check that was done when §3 was written. crosses_into_a_task and crosses_into_an_unseen_call are already two call sites with separately recorded polarities, both asking one verdict function that does not know which of them called it — so D1 is that function learning the destination. The debt is the one contracts::send’s own header wrote down: its tables require every name to be safe both to move to another thread and to be looked at from one, and say that the day a name belongs in one column and not the other a second column is needed. This is that day. And contracts::order asks the same function about a signature’s result for ordering, which is a third destination D1 does not answer by itself.

5. Three things found in the same section, and not fixed here

All three predate this record and none is about the destination.

Part III 15.2 promises crate metadata the compiler does not read. Its “Thread Safety (Send/Sync)” block says “The compiler reads the metadata of the Rust Crate”, that a Send Rust type is allowed in a spawn task, and quotes an error about Rc<i32>. No crate metadata is read: a foreign call is one no ledger describes, and the verdict is taken on the argument’s Nikaia type. The section’s Status note covers the lock rule and not this claim, so the claim stands unmarked.

And the same section’s type mapping has no shared entry. It maps i32, String and Option<T> and stops, so nothing written down says which Rust type a Shared[T] is at a boundary — which is the type §3’s whole argument turns on.

Part III 15.3 says a single-threaded build generates no atomic operations. “the compiler does not generate OS-level mutexes or atomic operations in this mode” has been false since ADR-037 D6 made the owner count atomic at both settings, and the measurement in §3 shows a foreign call forcing Arc whatever the setting.

6. What is built

All three decisions. contracts::send::crossing takes a Destination — Ours for a task of our own and for the closure statement overlapping builds, Foreign for a call nothing written down describes — and the parameter is threaded through the walk rather than asked before it, because a lock four fields deep is still a lock. It reaches exactly one row of the table: every other name answers the same wherever the value is going, which a test asserts by comparing the two answers.

Locked and SharedMut are that row. Into Ours they are answered by what they hold, like a container — a lock makes nothing crossable that was not, which is Group B’s transitivity and not an exception to it. Into Foreign they are refused. So NK2501 now refuses nothing a Nikaia program can write and NK2502 has the occupant ADR-037 D6’s coda named, which is §4’s prediction coming true in both halves.

The third call site took Ours, and that was a decision this record did not make for it: contracts::order asks the verdict about a signature’s result, because overlapping two statements puts each inside a closure that hands its value back. That closure is this compiler’s own, on this compiler’s own thread, so it is the destination D2 is about. A probe::held() pair overlaps now where it kept its place before.

Part II 12.2’s counter is writable, which was the point, and the test is the program that record names: a Shared[Locked[i32]] captured by a spawn passes, and the same value handed to fremd::irgendwas is NK2502 with D3’s way out — open the lock, hand over the value inside it.

§4’s debt came due and the second column was the destination. That section said contracts::send’s tables require every name to be safe both to move to another thread and to be looked at from one, and that the day a name belonged in one column and not the other the file would need two. A lock is exactly that name: at user_parallelism = no it may be moved and may not be looked at. It never has to be asked that way, because both destinations answer before the distinction matters — into our own code the lock may go whichever of the two it is doing, and into code nothing describes it may do neither. So the columns stay one and the table splits by destination instead. send.rs’s header says so where the prediction was written.

And the type is built now too (ADR-281), so D2 is a sentence about programs rather than about a verdict waiting for something to be true of. A lock handed to a task compiles and runs — and at user_parallelism = yes, since ADR-055 §6 step 1’s second half, a task is a thread, so “several threads are what it is for” is what actually happens: four tasks bumping one SharedMut[i64] 2 500 times each print 10 000, and the same program prints the same number at no. a_lock_shared_by_four_tasks_counts_every_increment in tests/shared.rs is that program, and it is the one neither half could run alone.

§5’s three findings are untouched, as that section says they would be: Part III 15.2 still promises crate metadata nothing reads, its type mapping still has no shared entry, and 15.3 still says a single-threaded build generates no atomic operations.

This site is open source. Improve this page.