Schemes leave closed

A Scheme leaves the unifier that minted it only if it is closed — every variable in its body is either ground or bound by the quantifier (or is a cyclic-binding root, which the ∀-prefix carries as a μ-equation, or a marked weak residual, below). A scheme that still mentions a residual variable — one free in the minting unifier — must not cross out of that unifier’s lifetime.

This is hard, not stylistic, because quantification is nominal-by-listing over unifier root ids (type-inference): the quantifier is a Vec of u32-tagged variable ids, and a free id is meaningful only relative to the union-find that minted it. A run’s unifier dies with the run and the next run’s ids restart at zero. An open scheme imported into a fresh unifier therefore silently aliases that unifier’s variables — type corruption that no check catches, not an error that surfaces.

Closure is enforced where schemes are minted for exit, by generalising against the empty TyEnv: with no environment to subtract, generalize quantifies every residual variable and resolves the body against the final unifier (core/src/typecheck/generalize.rs). Two sites mint:

  • the top-level Bind-node annotation, where each statement’s scheme rides the IR into the evaluator (typecheck::annotate);
  • the alias arm stored on the persistent handler frame at install (typecheck::alias_arm_scheme).

Every other carrier only transports what those two closed: the build-time prelude bake carries the schemes on the annotated prelude’s Define phrases (typecheck::bake_prelude), read back by SessionSchemes::from_prelude, and the seed’s scope tables carry the installed (value, scheme) pairs to a sealed child engine (seed::table::SerialBinding).

A weak residual is the one variable a scheme may carry unquantified, because it is marked: Scheme.weak lists it, seed_env re-seeds each listed variable as a fresh weak variable of the next unifier — with the kind it had, so a residual refuses in the next unit what it refused in its own — and a Define scheme is settled before it is stored, so no foreign id is ever read (a-let-generalises-what-is-not-weak).

One consequence: α-equivalence is not quotiented. Scheme’s PartialEq is derived, hence structural on the listed ids, so two schemes minted by different unifier runs compare unequal even when they denote the same ∀-type. Scheme comparisons must stay within one unifier run, or go through Scheme’s Display (core/src/ty/fmt.rs), which renames the prefix to canonical Greek letters before comparing.

See session-scheme-continuity for the decision this rule serves, and type-inference for how the prefix is represented.