One-sided obligations complete the co-inductive unifier
The unifier admits cyclic value and computation types with no occurs check
(type-inference); termination rests entirely on a
co-inductive guard (Pairs) that treats re-entry on an in-progress equality
obligation as immediate success — the cyclic fixed point. That guard memoized
only symmetric Var/Var root pairs. That is incomplete: the same
equi-recursive type can be anchored at a ty-var on one side and a comp-var on
the other, so unification re-enters as variable-vs-concrete-structure and the
guard never fires — the descent unrolls until the host stack overflows (a
--check crash, not a type error).
The shape that exposed it is the Stream. from-lines’
producer (lines_step_ty) closes the recursion at a comp-var:
C = F Step(C) // tail thunk wraps a comp-var; the value is inlined
A stream combinator written to take a Stream value rather than a Thunk
(case $s, recursing through !$p[tail]) closes the same recursion at a
ty-var:
T = Step(F T) // tail thunk wraps a Return whose payload is the ty-var
Unifying T against Step(C) re-enters as the obligations T ~= Step(C)
(ty-var vs concrete Variant) and F T ~= C (concrete Return vs comp-var) —
never a Var/Var pair. apply_ty already survived these via its Visited
stack; only unification lacked the matching guard.
The fix memoizes one-sided obligations too: a variable root paired with a
finite structural key of the other side (TyKey / CompTyKey / RowKey).
The key canonicalizes nested variables through find but never expands their
bindings, so it stays finite even when the root is bound to a cyclic structure;
equal keys denote the same obligation against the same anchor — the fixed point
to discharge. This is the regular-tree / bisimulation completion of the
procedure: it unifies the two anchorings rather than rejecting them, because
they denote one and the same equi-recursive type.
- Rows stay inductive. The recursive equality travels through field types (ty/comp), which already carry the guard; there is no recursive-row key and no recursive row.
- The thesis holds, the corollary did not. In CBPV, recursion lives at the
computation level — the productive cycle closes through
Thunk(F …). But the value cycle can be pinned at a ty-var (aStream-value-taking combinator), which the oldapply_tycomment wrongly called unreachable. It is reachable; the unifier now handles it co-inductively rather than the program being ill-formed. PipeModegainsHashso a resolved channel mode can sit inside a structural key.
This is what lets the prelude stream-* family present Stream-value
signatures — case $s directly, recursing through the forced tail !$p[tail],
no thunk parameter and no inner helper — because the unifier no longer cares
whether the caller’s stream is anchored at a value or a computation variable
(stream-map $f $s against a from-lines producer is exactly the regression
test).
See also types, type-inference,
typecheck, cbpv; judgments in
docs/SPEC.md §20.