Skip to content

Surface type-system soundness: type-check = expr bodies + cycle guard + Lean cardinality preservation - #9

Merged
chessai merged 4 commits into
masterfrom
type-system-soundness
Aug 5, 2026
Merged

Surface type-system soundness: type-check = expr bodies + cycle guard + Lean cardinality preservation#9
chessai merged 4 commits into
masterfrom
type-system-soundness

Conversation

@chessai

@chessai chessai commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Discovers and fixes two soundness holes in the surface type checker (theseus-check), with reproducing tests, and adds the corresponding Lean type-soundness property. Grounded in the Pi (James–Sabry, Information Effects, POPL'12) and Theseus (RC'14) type theory: a well-typed iso f : A <-> B must denote a bijection, which requires |A| = |B| and that the body's synthesized type equals the declared boundary.

Bug 1 — = expr iso bodies were never type-checked

The Typing stage (T0102) only walked clause patterns. A combinator/composition body (= f ; g, = iter T h, = p, = ~q) was never checked — neither internally nor against the declared signature — so check accepted, silently:

iso f : Bit <-> Bit = swap2          -- body is Bit*Bit<->Bit*Bit  (|.| = 4 != 2)
iso f : Bit <-> Bit = notb ; swap2   -- ill-typed composition (out(notb) != in(swap2))
iso f : Bit <-> Bit = iter Bit notb  -- non-numeral counter (T0601 existed but was never surfaced)

The declared signature — used for value marshaling and as the iso's type for downstream composition — was thus unchecked, so eval/compile could crash or produce out-of-domain values.

Fix: after the clause checks, elaborate each = expr body and require type_of(body) == (lower(lhs), lower(rhs)); an internally ill-typed body or a signature mismatch is T0102, and the combinator diagnostics (T06xx) the elaborator already localizes are now surfaced. Reuses the core type_of so id / iter / fold rules are handled once, authoritatively.

This immediately caught a real latent bug in the checker's own prelude test: tofMid = asc ; (toffoli * idb) ; ~asc was asserted clean but is ill-typed — asc yields a 2-bit group while toffoli needs 3 (cardinality 8 vs 16). Replaced with a well-typed tofUse = toffoli ; toffoli that still exercises prelude toffoli in an = expr body.

Bug 2 — ill-founded = expr recursion crashed the checker

A reversible iso may only recurse through a structural combinator (iter/fold); naming itself in an = expr body — directly (iso f = f) or mutually (f = g ; g = f) — is ill-founded. Elaboration recursed with no guard and stack-overflowed / SIGABRTed. Harmless before (check never elaborated = expr bodies) but reachable now, and a latent crash for any direct elaborate caller.

Fix: thread a visiting stack of iso names through elaboration; re-entering one is reported as T0102 instead of looping.

Lean — cardinality preservation (theseus-proofs)

Adds the Pi/Theseus structural type-soundness fact to Reversibility.lean:

  • card_preserved : (i : Iso a b) -> Fintype.card (denoteTy a) = Fintype.card (denoteTy b) — every combinator relates equinumerous types (a corollary of denote_bijective via Fintype.card_congr).
  • card_unit/card_bit/card_prod/card_sumdenoteTy realizes the type-level semiring on cardinalities.
  • no_iso_of_card_ne — the sharp contrapositive: there is no iso between types of different cardinality. This is precisely the obligation the checker fix enforces on = expr bodies; accepting f : Bit <-> Bit = swap2 would assert an iso this theorem proves cannot exist.

lake build green; AxiomGuard clean (propext / Classical.choice / Quot.sound only).

Tests / verification

  • 6 reproducing cases in typing.rs (cardinality mismatch, ill-typed composition, surfaced T0601, valid bodies stay clean, self-recursion, mutual recursion).
  • nix flake check green (fmt + clippy --all-targets --deny warnings + nextest); conformance green.

Not fixed (reported for a later, careful pass)

The clause path (Typing + Coverage + Linearity) looks sound — each side is partitioned, forcing a bijection and hence |A| = |B|. Not exhaustively audited: linearity.rs (var drop/duplication) and a cosmetic is_un_type laxness in typing.rs (not exploitable — Names gates it).

chessai added 4 commits August 4, 2026 23:08
…ndness)

Soundness hole: the Typing stage only checked clause (| p <-> q) bodies.
A combinator/composition body (= f ; g, = iter T h, = p, = ~q) was never
type-checked -- neither internally nor against the declared signature. So
check() accepted ill-typed and wrong-cardinality isos:

  iso f : Bit <-> Bit = swap2            -- body is Bit*Bit<->Bit*Bit (|.|=4 != 2)
  iso f : Bit <-> Bit = notb ; swap2     -- ill-typed composition
  iso f : Bit <-> Bit = iter Bit notb    -- non-numeral counter (T0601 never surfaced)

The declared signature -- used for value marshaling and as the iso type
for downstream composition -- was thus a lie: eval/compile could then
crash or produce out-of-domain values. This is exactly the Pi/Theseus
rule a combinators synthesized type must equal its declared boundary
(and hence |A| = |B|, a prerequisite for the denotation being a
bijection).

Fix: after the clause checks, elaborate every = expr body and require
type_of(body) == (lower(lhs), lower(rhs)); an internally ill-typed body
(type_of Err) or a mismatch is T0102, and combinator errors (T06xx) the
elaborator localizes are now surfaced. Reuses the core type_of so id /
iter / fold rules are handled once, authoritatively.

Exposed a real latent bug: the prelude test asserted `tofMid = asc ;
(toffoli * idb) ; ~asc` was clean, but it is ill-typed (asc yields a
2-bit group; toffoli needs 3 -- cardinality 8 vs 16). Replaced with a
well-typed tofUse = toffoli ; toffoli that still exercises prelude
toffoli in an = expr body.

Tests: 4 reproducing cases in typing.rs (cardinality mismatch, ill-typed
composition, surfaced T0601, and valid bodies stay clean).
…flow)

A reversible iso may only recurse through a structural combinator
(iter/fold); naming itself in an = expr body -- directly (iso f = f) or
mutually (f = g ; g = f) -- is ill-founded (no terminating denotation).
Elaboration recursed on the reference with no guard, so it stack-
overflowed and SIGABRTed. Harmless before (check never elaborated = expr
bodies) but reachable now that typing does, and a latent crash for any
direct elaborate caller regardless.

Thread a visiting stack of iso names through elab_iso/elab_expr; re-
entering one is reported as T0102 (an ill-formed iso) instead of looping.
Reproducing tests: self and mutual recursion now yield T0102, not a crash.
Add the Pi/Theseus structural type-soundness fact to Reversibility.lean:
every well-typed combinator relates types of EQUAL cardinality
(card_preserved, a corollary of denote_bijective via Fintype.card_congr),
the semiring realization on cardinalities (card_unit/bit/prod/sum), and
the sharp contrapositive no_iso_of_card_ne: there is NO iso between types
of different cardinality.

This is exactly the obligation the surface checker now enforces on = expr
bodies -- an iso body type must equal its declared signature, hence
|A| = |B|. Accepting f : Bit <-> Bit = swap2 (|Bit|=2 vs |Bit*Bit|=4)
would assert an iso no_iso_of_card_ne proves cannot exist.

lake build green; AxiomGuard clean (propext/Classical.choice/Quot.sound).
# Conflicts:
#	crates/theseus-check/src/elaborate.rs
@chessai
chessai merged commit fed66d8 into master Aug 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant