Placement
Where a demand is evaluated. Placement is cost-model plane: it cannot change a value. What it can change is who observed what, which is why the boundary is typed rather than advisory.
Distinguish two boundaries that are easy to fuse and must not be:
- an island edge carries a value between two computations in one
evaluator. Identity, memo entries and receipts live there
(
machine.identity.*,machine.receipt.*). - a place carries a subgraph of demands to a different evaluator.
[SETTLED, round 10] Placement never changes a value. The same demand evaluated on any admissible machine yields a bit-identical result. Consequences: the scheduler needs no consensus (duplicate work is a duplicate, not a conflict; a partition is not an outage; a stale advertisement is a rejected dispatch); speculation is always sound; kill-anytime is always sound; and a distributed build has a perfect correctness oracle — the local build.
[SETTLED, round 10] A value may cross a place boundary only if its identity
is known without evaluating it. A pinned blob (checksum in the source), a
capability identity, a literal, and an input pinned at the demand root all
cross. A derived value does not: knowing what let x = expensive(); is means
computing it, so either it is computed before the boundary or the place is
drawn wider to contain its demand.
This is the restriction that makes placement analyzable. Before dispatch, the exact set of things that cross — and their weight — is known. No demand discovers in flight that it needs something the boundary never accounted for.
[SETTLED, round 10] A program cannot name where it runs. A program that could
steer placement could make its value depend on the machine it ran on, and the
same source would describe different artifacts in different places. This is
machine.scheduler.no-in-program-forcing applied to location: nothing in a
program observes the world.
Ambient facts arrive as inputs, supplied at the demand root. Target::host(),
uname, cfg!(target_os) evaluated inside a recipe are the same bug: they read
the executor into the artifact. vx build --target is resolved by the CLI,
which is outside the program, and the recipe receives a Target value with an
identity (machine.primitive.target-value).
An ambient read is an observation. An input is a pin.
This constrains the program, not the operator. Placement policy — which machines are admissible, which are preferred — lives outside the language and may be as explicit as its owner likes, precisely because it cannot change a value.
[DESIGN, amended round 12] Placement is constrained along two independent axes, and an earlier draft of this rule collapsed them.
- Execution-platform contract. A tool is built for an ABI, an OS and an
architecture. A content-addressed
x86_64-linuxbinary is materialized everywhere and executable only on a node that can runx86_64-linux. Materialization removes locality, not platform compatibility. Both materialized and ambient closures impose this contract. - Host-specific locality. Only ambient closures (Xcode, MSVC, the platform's system libraries) impose this: they exist solely where a daemon advertises their fingerprint, so they are runnable exactly on the set of nodes advertising that fingerprint — which may be empty, one, or many. Nothing about an ambient closure makes it unique to a single node; what constrains it is the advertisement.
So a materialized closure admits nodes that satisfy its execution contract; an ambient
closure admits the nodes that advertise its fingerprint — a set, possibly of more than
one. The earlier claim, "a materialized closure constrains placement not at all", was
false, and would have let the scheduler dispatch a Linux rustc to a Mac.
"Satisfies the execution contract" is deliberately weaker than "is of that platform": a
node may satisfy x86_64-linux through emulation, a compatibility layer, or a container.
Admissibility is the node's claim to satisfy the contract, not an equality of labels.
The set of closures reachable in a placed subgraph is syntactic (union over branches, fixpoint over recursion), so both requirements are statically derivable — a conservative over-approximation, costing perf and never correctness.
Three things are being distinguished here, and an earlier draft collapsed two of them.
- The target — what the artifact is FOR. Semantic. It changes the value.
- The selected toolchain's host / execution ABI — including Cargo's
HOST. This is a pinned semantic property of the selected toolchain capability, and it enters exec identity viamachine.primitive.exec-probed-toolchain) and a scheduler admissibility constraint. It is not cost-model. - The physical executor — which machine, of the admissible set, actually ran it.
Cost-model, unobservable, absent from the semantic receipt
(
machine.placement.no-in-program-steering).
A cross-compiling x86_64-linux rustc emitting aarch64-darwin objects has an
x86_64-linux execution contract (2) and an aarch64-darwin target (1); which admissible
machine ran it (3) is nobody's business.
[DESIGN, round 12] machine.placement.identity-crosses governs dispatch: a value
captured by, or imported into, a placed subgraph must have an identity known without
evaluating that subgraph, because the boundary must know what it is shipping and what it
weighs before it ships anything.
It says nothing about results. A placed block's derived value — a Tree, a rendered
diagnostic, a solved graph — is computed remotely, so its identity is not and cannot be
known before the block is evaluated. It acquires an identity where it is computed, and
that identity crosses back.
The two directions are not symmetric and must not be conflated. A placed block may consume codata locally and return a finished diagnostic, or another evaluator may subscribe to codata/projections across the boundary. Both use the same remote demand/completion protocol; transport timing is unobservable.
[DESIGN] A tree crosses a place boundary as an identity plus a mount
grant: authority to read a prefix, and the coordinates of its blobs. Nothing
is copied. Blobs materialize per-file, on read, by content hash; every read and
every miss is recorded (machine.receipt.witness-reads, absence-is-an-
observation). A workspace of ten thousand files whose compiler opens two hundred
moves two hundred.
Corollary: the previous run's read-set is a prefetch plan, and changing a
file nobody read invalidates nothing — the memo is indexed by location
(machine.memo.indexed-by-location), which is content-free, and the entry it
finds carries a read-set the change misses.
[DESIGN] Stopping a process early is not a scheduler feature. If a demanded projection of an exec's output is determined, the remainder of that process's output is undemanded, and stopping it is the laziness law arriving at a subprocess boundary. The demanded projection's value is bit-identical whether or not the process was stopped.
This is why kill-anytime is sound, and it is why the kill point must be driven by the demanded projection rather than by a scheduler's judgement — otherwise a scheduling artifact would enter a value's identity.
[SETTLED, round 10] An observation performed inside a place was performed by
another evaluator, and by machine.receipt.fetch-observation-pin its pin
becomes the receipt's authority. There is nothing to check it against.
Placement is trust-free exactly when everything inside it is
content-addressed. A placed subgraph containing an observation is therefore a
different kind of object from one containing only pinned inputs, and must be
visible as such.
[SETTLED] Dispatch ships partitioned/lowered architecture-neutral artifacts, source maps, capture identities, primitive ABI requirements, and grants. The canonical closure AST remains semantic identity and audit authority, but the executor needs Weavy and registered primitives rather than vixc. It may JIT the shipped artifact locally.
[SETTLED] Codata and progressive projections may cross placement boundaries as remote demand edges. The protocol provides semantic keys or byte offsets, credit/backpressure, cancellation, completion, reconnection, and replay/spill. A transport frame is never a stream element. Capability disjointness needs no separate island rule: every effect is already a mandatory cut and carries its own derived admissibility requirements.