Model the type system in Lean, and check the solver's subtyping and bound merge against it - #109
Draft
dpmills wants to merge 1 commit into
Draft
Model the type system in Lean, and check the solver's subtyping and bound merge against it#109dpmills wants to merge 1 commit into
dpmills wants to merge 1 commit into
Conversation
This was referenced Aug 18, 2026
dpmills
force-pushed
the
dmills/canonical-discharge
branch
from
August 19, 2026 20:30
145d805 to
643aff2
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 19, 2026 20:30
11b8722 to
fa57099
Compare
dpmills
force-pushed
the
dmills/canonical-discharge
branch
from
August 19, 2026 21:36
643aff2 to
a31b7d6
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 19, 2026 21:36
fa57099 to
a84a09a
Compare
dpmills
force-pushed
the
dmills/canonical-discharge
branch
from
August 19, 2026 23:06
a31b7d6 to
66bc8ba
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 19, 2026 23:06
a84a09a to
5ecb233
Compare
dpmills
force-pushed
the
dmills/canonical-discharge
branch
from
August 19, 2026 23:34
66bc8ba to
ceac235
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 19, 2026 23:34
5ecb233 to
06e1402
Compare
dpmills
force-pushed
the
dmills/canonical-discharge
branch
from
August 20, 2026 00:12
ceac235 to
5ebd593
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
3 times, most recently
from
August 20, 2026 01:39
4cfdc57 to
6991741
Compare
dpmills
changed the base branch from
dmills/canonical-discharge
to
dmills/domain-rule-at-coalesce
August 20, 2026 01:39
dpmills
changed the base branch from
dmills/domain-rule-at-coalesce
to
dmills/domain-set
August 20, 2026 05:37
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 20, 2026 05:37
abf8036 to
b96ca2c
Compare
dpmills
changed the base branch from
dmills/domain-set
to
dmills/empty-product
August 20, 2026 08:21
dpmills
force-pushed
the
dmills/formal-model
branch
3 times, most recently
from
August 20, 2026 20:27
b17e147 to
b2ad7df
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 20, 2026 20:51
4066331 to
6763585
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 20, 2026 21:57
d8be791 to
bbe7fef
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 20, 2026 23:24
6763585 to
b3faa41
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 20, 2026 23:24
bbe7fef to
eaa3093
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 04:29
b3faa41 to
5ec1b3c
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 21, 2026 04:29
eaa3093 to
b21a940
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 04:39
5ec1b3c to
c18566f
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 21, 2026 04:39
b21a940 to
2761e6a
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 05:47
a98df57 to
776dd2b
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 21, 2026 05:47
9e65f4f to
44491f6
Compare
This was referenced Aug 21, 2026
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 21, 2026 18:41
44491f6 to
313631b
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
2 times, most recently
from
August 21, 2026 19:42
6ba5a31 to
ce09a11
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
2 times, most recently
from
August 21, 2026 20:45
048656a to
eab05cc
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 22:23
89b22b5 to
9321478
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 21, 2026 22:23
eab05cc to
4be25f2
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 00:08
9321478 to
f4d7b30
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 22, 2026 00:08
4be25f2 to
46128a8
Compare
dpmills
changed the base branch from
dmills/empty-product
to
dmills/type-merge-fuzz
August 22, 2026 00:09
dpmills
force-pushed
the
dmills/type-merge-fuzz
branch
from
August 22, 2026 00:22
e08acb4 to
fe0519b
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
2 times, most recently
from
August 22, 2026 00:23
b5c2817 to
387c167
Compare
dpmills
force-pushed
the
dmills/type-merge-fuzz
branch
from
August 22, 2026 00:35
fe0519b to
bd42fd4
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 22, 2026 00:35
387c167 to
84a1710
Compare
dpmills
force-pushed
the
dmills/type-merge-fuzz
branch
from
August 22, 2026 00:46
bd42fd4 to
e378367
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 22, 2026 00:46
84a1710 to
0149c9c
Compare
dpmills
force-pushed
the
dmills/type-merge-fuzz
branch
from
August 22, 2026 01:06
e378367 to
e8a20dc
Compare
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 22, 2026 01:06
0149c9c to
df65527
Compare
…ound merge against it The ground subtype relation and the solver's bound merge exist only operationally — `constrain_go` and `CompactType::merge` implement them without ever writing them down — so their properties (is subtyping transitive? is the merge associative? does the checker decide the relation?) have been readings of code, not statements. This PR adds `formal/`, a Lean 4 model that states both declaratively over the index coordinate the stack below establishes, proves the metatheory sorry-free, and pins the model to the implementation with a differential oracle per operation. The headline theorem for subtyping is **transitivity with no fragment restriction and no environment side conditions**: earlier editions of this model (developed on the retired name-coordinate stack, #79) proved it only for a canonical-spelling fragment under six identity-acting rename environments, and the σ-gap they reconciled never forms here — the measured sense in which the coordinate below is the right design. For the merge it is **associativity with no side condition**, and uniqueness: the merge is the only way the induced order admits of combining two bounds. Each oracle earned its keep before landing. The subtype oracle's first sweep caught a real capture in the solver's Fun/Fun opening, fixed below in the telescope PR with the mechanism it repairs. The merge oracle's first sweep caught the model intersecting away refinements a hole should have passed through, fixed here. ### The model `Ty`/`Pred` mirror the ground fragment of `Type`, with `Pred.piBound` mirroring `Name::PiBound` and `refined` carrying a refinement set ([grammar and adjudications](https://github.com/cambra-dev/cambra/blob/dmills/formal-model/formal/design.md)). `Sub` is one constructor per `constrain_go` arm; deliberate departures (no trivial-equality short-circuit, no partition-collapse arm, no binder-correspondence edge) are each recorded as tracked decisions in `CclFormal/Sub.lean`. Proved: `Sub.refl` under the uniquely-keyed `Ty.WF` (naming a builder invariant the Rust leaves implicit), checker soundness/completeness giving decidability (`Equiv.lean`), `sub_trans` for well-formed types (`Transitivity.lean`), refinement-order unobservability, and the M2 safety battery — progress, preservation, refinement soundness (`Safety.lean`). ### The merge algebra `Merge.lean` mirrors `compact.rs`'s bound-merging: `CTy` for the ground fragment of `CompactType`, `merge` for the polar `CompactType::merge`/`CompactFun::merge`, and `eqv` for `CompactType`'s own `PartialEq`, so every theorem is quotient-compatible by construction. Proved: `eqv` is an equivalence, `merge` is commutative, idempotent under the input-bound invariant `wf`, a congruence, and **associative unconditionally at both polarities** — the kinds join in the flat semilattice `unknown < {data, compute} < conflict` (`joinKind`) and the domains are combined by polarity alone, so no fold step reads a value a later step can change. The fold is therefore invariant under permutation and duplication of the bound list, and `merge pol` is the least upper bound of the order it induces (`le`) and the only one up to `eqv` (`join_unique`), with the empty position as the least element. Associativity was a *non*-theorem when this model first stated it, and the counterexample was real: the solver selected a function slot's domain combination from a kind that was not settled, so bound arrival order decided accept-vs-reject and one association accepted a collection whose domain was the meet of two others'. The PR below defers that choice to the resolved kind at coalesce, which is what removes the side condition here. ### The differential oracles `lake build` produces the oracle binary; each case is one JSONL line tagged by `"op"`, and `tests/differential_oracle.rs` generates cases with the seeded generator it shares with the type-merge fuzz (`tests/type_gen/mod.rs`), computes the solver's answer, and diffs. The harness is an integration test rather than a `#[cfg(test)]` module in the library: it is a test, and the only solver internal it cannot otherwise reach is `CompactType::merge`, which the `test-helpers` feature opens for it. - `"sub"`: seeded ground pairs, each closed into the ground fragment (`close_all` — every arrow's own-binder references as indices), diffed against `subCheck`. 150k pairs across five seeds, zero mismatches, roughly one pair in twelve index-bearing. - `"merge"`: bound lists folded through `CompactType::merge` exactly as `compact_go` folds a variable's bounds, every step diffed against `merge` up to `eqv`. Each step's left operand is the previous step's result, so the conflicted and multi-alternative states only merging produces are operands too; operands sometimes carry a kind *variable*, the only route to `KindMerge::Unknown`. Clean at 40k steps. `CoErr` has no `emptyProduct`: the empty product is `IncompatibleBounds` on both sides, since bounds with no common shape is what that error already says. Both skip loudly when the oracle binary is absent, so machines without a Lean toolchain stay green, and `./ci.sh oracle` builds the model and runs them where `lake` exists — without that gate nothing notices the model drifting from the solver, in either direction. `CAMBRA_DIFF_SEED`/`CAMBRA_DIFF_N`/`CAMBRA_DIFF_DUMP` control runs. ### Reading order `formal/design.md` (the plan, adjudications, and findings — every claim in it is true of the code beneath this PR), then `Ty.lean` → `Sub.lean` → `Decide.lean`/`Equiv.lean` → `Transitivity.lean`, with `Term.lean`/`Safety.lean` and `Merge.lean` as independent limbs. In `Merge.lean`, read the refinement-slot laws and `joinKind` before the `merge` theorems that use them; the order and uniqueness section is last and depends only on the semilattice laws. The model's development history, including the name-coordinate editions this replaces, is on closed #79.
dpmills
force-pushed
the
dmills/formal-model
branch
from
August 22, 2026 03:43
df65527 to
a20dd09
Compare
dpmills
force-pushed
the
dmills/type-merge-fuzz
branch
from
August 22, 2026 03:43
e8a20dc to
b417392
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The ground subtype relation and the solver's bound merge exist only operationally —
constrain_goandCompactType::mergeimplement them without ever writing them down — so their properties (is subtyping transitive? is the merge associative? does the checker decide the relation?) have been readings of code, not statements. This PR addsformal/, a Lean 4 model that states both declaratively over the index coordinate the stack below establishes, proves the metatheory sorry-free, and pins the model to the implementation with a differential oracle per operation. The headline theorem for subtyping is transitivity with no fragment restriction and no environment side conditions: earlier editions of this model (developed on the retired name-coordinate stack, #79) proved it only for a canonical-spelling fragment under six identity-acting rename environments, and the σ-gap they reconciled never forms here — the measured sense in which the coordinate below is the right design. For the merge it is associativity with no side condition, and uniqueness: the merge is the only way the induced order admits of combining two bounds.Each oracle earned its keep before landing. The subtype oracle's first sweep caught a real capture in the solver's Fun/Fun opening, fixed below in the telescope PR with the mechanism it repairs. The merge oracle's first sweep caught the model intersecting away refinements a hole should have passed through, fixed here.
The model
Ty/Predmirror the ground fragment ofType, withPred.piBoundmirroringName::PiBoundandrefinedcarrying a refinement set (grammar and adjudications).Subis one constructor perconstrain_goarm; deliberate departures (no trivial-equality short-circuit, no partition-collapse arm, no binder-correspondence edge) are each recorded as tracked decisions inCclFormal/Sub.lean. Proved:Sub.reflunder the uniquely-keyedTy.WF(naming a builder invariant the Rust leaves implicit), checker soundness/completeness giving decidability (Equiv.lean),sub_transfor well-formed types (Transitivity.lean), refinement-order unobservability, and the M2 safety battery — progress, preservation, refinement soundness (Safety.lean).The merge algebra
Merge.leanmirrorscompact.rs's bound-merging:CTyfor the ground fragment ofCompactType,mergefor the polarCompactType::merge/CompactFun::merge, andeqvforCompactType's ownPartialEq, so every theorem is quotient-compatible by construction. Proved:eqvis an equivalence,mergeis commutative, idempotent under the input-bound invariantwf, a congruence, and associative unconditionally at both polarities — the kinds join in the flat semilatticeunknown < {data, compute} < conflict(joinKind) and the domains are combined by polarity alone, so no fold step reads a value a later step can change. The fold is therefore invariant under permutation and duplication of the bound list, andmerge polis the least upper bound of the order it induces (le) and the only one up toeqv(join_unique), with the empty position as the least element.Associativity was a non-theorem when this model first stated it, and the counterexample was real: the solver selected a function slot's domain combination from a kind that was not settled, so bound arrival order decided accept-vs-reject and one association accepted a collection whose domain was the meet of two others'. The PR below defers that choice to the resolved kind at coalesce, which is what removes the side condition here.
The differential oracles
lake buildproduces the oracle binary; each case is one JSONL line tagged by"op", andtests/differential_oracle.rsgenerates cases with the seeded generator it shares with the type-merge fuzz (tests/type_gen/mod.rs), computes the solver's answer, and diffs. The harness is an integration test rather than a#[cfg(test)]module in the library: it is a test, and the only solver internal it cannot otherwise reach isCompactType::merge, which thetest-helpersfeature opens for it."sub": seeded ground pairs, each closed into the ground fragment (close_all— every arrow's own-binder references as indices), diffed againstsubCheck. 150k pairs across five seeds, zero mismatches, roughly one pair in twelve index-bearing."merge": bound lists folded throughCompactType::mergeexactly ascompact_gofolds a variable's bounds, every step diffed againstmergeup toeqv. Each step's left operand is the previous step's result, so the conflicted and multi-alternative states only merging produces are operands too; operands sometimes carry a kind variable, the only route toKindMerge::Unknown. Clean at 40k steps.CoErrhas noemptyProduct: the empty product isIncompatibleBoundson both sides, since bounds with no common shape is what that error already says.Both skip loudly when the oracle binary is absent, so machines without a Lean toolchain stay green, and
./ci.sh oraclebuilds the model and runs them wherelakeexists — without that gate nothing notices the model drifting from the solver, in either direction.CAMBRA_DIFF_SEED/CAMBRA_DIFF_N/CAMBRA_DIFF_DUMPcontrol runs.Reading order
formal/design.md(the plan, adjudications, and findings — every claim in it is true of the code beneath this PR), thenTy.lean→Sub.lean→Decide.lean/Equiv.lean→Transitivity.lean, withTerm.lean/Safety.leanandMerge.leanas independent limbs. InMerge.lean, read the refinement-slot laws andjoinKindbefore themergetheorems that use them; the order and uniqueness section is last and depends only on the semilattice laws. The model's development history, including the name-coordinate editions this replaces, is on closed #79.