Unitarity lives in the term rules
Superseded by a-table-never-enters-the-unifier. Confinement and the one-optional-type door guarded flags; with the flags gone the row algebra is unitary on its own, and
check_one_optional_type,ContractClashand their gate are deleted. The page below is history.
The assignment Δ is withdrawn. An absent field is absent, full stop: it
carries no type, and retiring a field imposes no equation on the payload that
dies with it.
Δmade the algebra unitary so the unifier is order-independent on its own. Under this design, order-independence is a theorem about the term rules (confinement) plus the one-optional-type door.
Context
a-field-is-a-flag-and-a-type
bought unitarity with an assignment. Δ mapped a label to a type variable
δ_l, minted on the first retirement at that label and living on Unifier;
retiring a field unified the dying payload with δ_l. The deciding equation
Record(l: θ·α ; Empty) ≐ Record(l: θ'·String ; Empty)
then had one solution rather than two, because the solution that kills the
field said something — String ~ δ_l — instead of throwing α away.
That is a real theorem about the algebra, and it is more than was needed. What a typechecker owes is that the programs a user can write get one verdict whichever order their statements are in. The algebra is the wrong quantifier: it ranges over every row the datatype can spell, including rows no rule mints.
Decision
Δ goes. Field::Absent carries nothing and means nothing further; the field
rule unifies the flags and then the payloads when both sides have one, and
drops the equation otherwise.
What replaces the algebraic argument is a confinement invariant on the term rules:
Every variable occurring in the payload of a
Field::Var(θ, τ)slot at labelloccurs nowhere but in payloads of slots whose flag is inθ’s union-find class.
Field::Var is minted in exactly two places — the put rule in
infer_record_val, a fresh flag over a fresh payload, and scope::occurrence
in typecheck/scope.rs, a fresh flag over a ground At(T), a fresh variable
for Decoded/Refused, or a fresh shape for Shaped. No builtin scheme
carries one, and the only operation that touches a Var payload is
unify_field. So the equation Δ used to add could bind only variables that
nothing else names — dead, therefore unobservable — and could fail only when
two declared tables disagree at one label.
That last case is exactly what the door in typecheck/contract.rs already
forbids, so nothing is lost by deleting the equation and the store behind it.
This makes the door load-bearing. Under Δ the one-optional-type condition
was a nicety that kept an assignment variable from being asked for two types;
now it is half the reason a verdict does not depend on statement order.
Confinement must be re-established for every new site that introduces a
variable flag — a third minting site, or a builtin scheme carrying a Var
payload, breaks the argument and not merely a comment.
The door is also sharpened as part of the withdrawal: it compares
unifiability rather than equality, minting both declarations in a scratch
unifier, which subsumes the old ground-type equality test and stops exempting
Holds::Shaped. A shape at a label another table holds at a ground type is the
same order-dependence as two ground types.
Consequences
- Erasure now really erases. A retired payload is dropped rather than
re-homed, so
generalize’s residual cache is sound for all five sorts again:residuals_are_live_rootsrecovers thety_fvclauseΔforced it to drop, and its “holds by construction” reason with it. - Retiring a field costs no depth. The retirement used to be the type
equation
τ ~ δ_l, which fingerprintedτand could raiseTypeTooDeepfor a payload nothing would ever read. It no longer walks the payload at all. - Two latent defects go with the equation. The flag was bound before the
payload equation ran, so a failed payload left the flag
Absentwith the payload never united; and the claim thatδ_lwas always on the left of the union — so a payload some scheme still named never stopped being canonical — was false from the second retirement at a label onward. - The deciding equation has two solutions again, and that is fine. They
differ only in the binding of a dead variable. The unifier test asserts
observables — both flags, both rows as applied — rather than a store
binding, and a new inference-level test writes one wrapper’s
within $oandgrant $oin both statement orders and demands one verdict. - No reachable program changes verdict. Every
.ralfile in the tree gives byte-identical--checkoutput across the withdrawal.