Status: Superseded by ADR-284 — its D1-D3 are D1-D3 there
Related: ADR-061 D2 (every count plain at one user thread, and
the sentence this examines), ADR-037 D1 (the target decides what
std offers), ADR-037 D2 (user_parallelism bounds your code),
ADR-045 D1 (the verdict never reads the switch),
ADR-038 D7 (a foreign runtime brings threads that are not ours)
ADR-061 D2 makes every reference count plain at
user_parallelism = no, and it rests on one sentence: nothing a user writes
can cross a thread there. That sentence is true because the program owns every
thread it runs on — main is ours, the runtime’s I/O worker carries no user code
(ADR-038 §4.2), and since ADR-053 a Nikaia dependency
is a crate of the same build at the same setting.
The roadmap asks for two things that end that ownership: extern "C" (Part III
15.1, specified and unbuilt) and Python bindings. Both turn the program into a
library somebody else calls, and the caller brings its own threads.
What this does not bring back is the problem ADR-061 D1 is
about. D1 refuses a Shared at a foreign call because a Rust library has one
signature and this compiler chooses the count per value. A C signature names
neither shape — it names a pointer and an integer and nothing else, so the
representation question cannot even be posed there. The exported surface is what
C can spell, which the ledger’s writable types already are.
What it does bring back is that the caller owns the threads. An exported entry point may be called twice at once from two threads of the caller’s, and no switch of ours said yes to that. Two of our tasks are then in flight together with the cheap count under them.
The GIL is not the guarantee it is remembered as. A C extension may release it around its own work, and free-threaded CPython removes it outright. A Python target that assumed one caller thread would be assuming something CPython itself has stopped promising.
And this is the first form in which the question can arise at all. A Nikaia
library is source today, so its settings are the consumer’s. The question needs an
artifact somebody links rather than builds — which this repository has no notion
of: there is no cdylib, no staticlib, no .so anywhere in it.
user_parallelism.The target already decides what std offers and what a panic does
(ADR-037 D1), which is the same kind of question: what is outside
this program, and what does it do to us. user_parallelism bounds your code
(ADR-037 D2 — the word user is the whole of it) and therefore
cannot answer for code that is not yours. Asking it to would make it mean two
things at once, and the second one silently.
So a build that exports entry points is a target of its own. x86_64-linux is a
program that owns its threads; a target that produces a linkable artifact is one
that does not, and it says so in the place where the answer is already kept.
Every exported entry point, and everything reachable from one, takes the safe count and the safe lock. Everything else keeps the per-value answer (ADR-037 D7).
The two alternatives are worse in the same way — they both pay everywhere for a property only the boundary has:
The analysis already answers per value. An entry point is a root, and a root seeded at the floor is the whole of this: what it reaches is atomic because it reaches it, and what nothing reaches from outside stays cheap.
This is the payoff of ADR-045 D1 and ADR-061 D3, and
it is why the decision is small. contracts::send asks nothing about
user_parallelism — the crossing verdict is about a type and a
destination. A contract in the ledger is written at the floor whatever the
build says. So a library with foreign entry points is already checked for the
world it is about to enter; only what is emitted moves, and only under its
entry points.
A Nikaia library does not have to ship twice. The question that opened this — whether a binary library needs a single-threaded and a thread-safe build — has the answer no, and it has it without giving up the per-value count inside.
The switch keeps its meaning at the one place it could have lost it. no goes
on saying nothing you wrote runs twice at once, because on a target where that
could be false the target says so first.
What it costs: the analysis needs the set of exported entry points as roots, which it does not have, because there is no way to export one yet. That is the same order of work as the crossing roots it already seeds.
cdylib, staticlib) is
— answered by ADR-125 D1: artifact = "c-library" beside
target, and the pair is the target this record means. The rest of the
bullet is kept as the question it was:
part of it or a third key beside it.Shared does not (ADR-061 D1), and a C signature could not name
one anyway. Whether an exported entry point may take a Locked[T] at all is
open.Nothing, and nothing can be. extern "C" is a parse error (Part III 15.1),
Target has two values and one of them is refused with a reason, and the
repository has no notion of a linkable artifact. The decision is recorded now
because it is the first direction that touches user_parallelism at its root, and
a target added without it would answer the question by accident.