Skip to content

Canonicalize each arrow's Pi binder as the solver flattens types - #76

Closed
dpmills wants to merge 4 commits into
mainfrom
dmills/canonical-pi-binders
Closed

Canonicalize each arrow's Pi binder as the solver flattens types#76
dpmills wants to merge 4 commits into
mainfrom
dmills/canonical-pi-binders

Conversation

@dpmills

@dpmills dpmills commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

A dependent function type's binder name leaks into two identities that are meant to be blind to it. spec_key excludes the binder from the key, but a dependent refinement predicate references it, so (𝑥: 𝐷) ⤇ {Int | __elem == 𝑥} and its 𝑦-twin key apart and split a specialization two uses should share. Merging α-variant bounds keeps the first arrival's binder while unioning both copies of one predicate, coalescing to an arrival-order-dependent type whose second predicate references a binder that is no longer there. Both flattening walks now rename each arrow's binder, and every codomain reference to it, to the reserved __pi{depth} name, so α-equivalence coincides with structural equality on the flattened form.

Subst::canonical_pi_binder owns the rule; its doc comment names the three walks that apply it and what a silent divergence between them costs. Type::alpha_normalized is that scheme as a pure function, which lambda_elim's debug assertions compose with without_pi_names to compare canonical solver output against a type rebuilt from the term's own binders.

A regression test in compact.rs and one in spec_key.rs state the repaired findings.

Why the rewrite sits at the flattening layer rather than at emission or in a comparison, and what the four alternatives break on, is recorded in type-inference.md; canonical_pi_binder cites it.

@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 13b4fae to 14faed2 Compare August 11, 2026 22:34
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 14faed2 to 0a9ee2e Compare August 12, 2026 00:09
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 0a9ee2e to cc899f9 Compare August 12, 2026 00:38
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from cc899f9 to e64b690 Compare August 12, 2026 20:08
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from e64b690 to 53e505c Compare August 12, 2026 20:43
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 53e505c to 7689793 Compare August 12, 2026 22:30
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 7689793 to fbfda85 Compare August 12, 2026 22:45
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from fbfda85 to 991642a Compare August 13, 2026 05:39
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 991642a to 254df81 Compare August 13, 2026 06:01
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch 2 times, most recently from 800ecc1 to bd29729 Compare August 13, 2026 23:42
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from bd29729 to b167942 Compare August 14, 2026 19:49
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from b167942 to 97be0a9 Compare August 14, 2026 21:34
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 97be0a9 to 1a42ebf Compare August 14, 2026 21:41
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 1a42ebf to 4162d96 Compare August 14, 2026 23:11
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 4162d96 to b497221 Compare August 17, 2026 17:57
Base automatically changed from dmills/partition-collapse-composes to main August 18, 2026 03:17
Two α-identity defects — `SpecKey` splitting on α-variant dependent types, and α-variant bound merges keeping an arrival-dependent binder plus a dangling predicate twin — get the repair the codebase already applied to refinements: **one reserved name per position**. `ReservedName::Pi(depth)` (spelled `__pi0`, `__pi1`, …) joins `Elem`, and the solver canonicalizes every arrow's binder to it as types flatten:

- `compact_go` renames the binder to `Name::pi(depth)` and rides the rename on the accumulated substitution, so every predicate reference inside the codomain is rewritten as it lands — the same force that discharges dependent applications. α-variant bounds now compact to *identical* shapes: they merge, their refinement copies dedup instead of accumulating a dangling twin, and every identity built on the flattened form (equality walls, caches) is α-insensitive. The rename also subsumes the previous `shadow(b)` (insertion over any outer mapping of the source binder).
- `spec_key::key_go` does the same, so a keyed predicate references its binder α-insensitively — two uses whose instantiation types differ only in source binder names key together. (The binder name itself was already excluded from the key; this closes the leak through the *references*.)
- `Type::alpha_normalized` exposes the same form as a pure function; `lambda_elim`'s recorded-vs-rebuilt asserts compose it with `without_pi_names` (whose `Some`-vs-`None` job is unchanged), since the coalesced original now carries canonical binders while the point-free construction rebuilds from the term's own names.

Both defects are pinned as `spec_key_shares_alpha_variant_dependent_types` and `alpha_variant_bound_merge_is_canonical`, the latter also asserting the merged type carries one binder and exactly one deduped refinement layer.

`constrain`'s semantics are untouched — this stratum deliberately leaves mid-inference source-named types to the existing α-reconciliation. Retiring that machinery too needs the minting-level stratum (`emit_lambda` producing canonical binders).
The rule — *rename a function type's binder at codomain-depth `d` to the reserved `Name::pi(d)`, rewrite references to it through that rename, recurse into the codomain at `d + 1` while the domain keeps `d`* — was written out three times: in `compact_go` (the flattened bound graph), in `spec_key::key_go` (the specialization key), and in `Type::alpha_normalized` (the pure function the recorded-vs-recomputed walls compare through).

The first two already carried a comment that they must be changed together, and by `compact_go`'s own documentation a divergence between them is **silent**: it yields a shared clone whose interior was resolved against a different use's argument. `alpha_normalized` was a third copy that no comment mentioned. Three copies of one rule, agreement load-bearing and unenforced.

All three now call `Subst::canonical_pi_binder`, which owns the rule and states why the walks applying it have to agree. `key_go` discards the returned canonical name — the binder name is deliberately not part of a specialization key (`SpecKey::fun`); what it needs is that the *references* were rewritten — and that asymmetry is now visible at the call site instead of being implicit in a hand-inlined copy.

Behaviour is unchanged; this is the rule stated once rather than asserted thrice. The lockstep note on `compact_go` is narrowed to what genuinely remains duplicated between it and `key_go` (polarity flips, edge-substitution composition, the cycle guard).
`freshen_pi_binders`'s structural walk names the variant it recurses through,
so the rename downstack reaches it. No behavior change.
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from b497221 to 9be5020 Compare August 18, 2026 04:11
@dpmills dpmills changed the title Canonical Pi binders at the solver's flattening layer Canonicalize each arrow's Pi binder as the solver flattens types Aug 18, 2026
…un-derivable

`canonical_pi_binder`'s doc justified the rule by its effect — one shared name per position makes α-variant types flatten alike — which reads as convenience and invites the question of why a comparison could not do the same job. It cannot: a Pi reference has no position until flattening, because a dependent refinement rides a bound edge and the variable holding it need not sit under the binder its predicate references. `type-inference.md` §4.5 gains that as a section, with the group-by shape its own lowering tests pin as the exhibit, and the four alternatives recorded against what each breaks on — minting at emission, counting inside out, an α-invariant `PartialEq`/`Hash`, and renaming at merge.

The rule was already shared; the *depth* was not. Each of the three walks took the canonical binder from `canonical_pi_binder` and then computed `depth + 1` itself, so a walk could get the name right and the depth wrong. `canonical_pi_binder` now returns a `PiCodomain` carrying the binder, the codomain's substitution, and the codomain's depth together, and no `+ 1` remains outside it.

What the walks owe each other is not what the doc previously claimed. `compact_go` and `alpha_normalized` must assign the same index, since `lambda_elim` compares a solver-produced type against a rebuilt one by normalizing both. `key_go` owes only injectivity: a key carries no binder name, so a consistent relabelling is invisible, while conflating two binders makes two uses share a specialization resolved against the other's argument.

Injectivity is what the new tests assert, and it is the property the shared rule cannot establish. Entering a codomain at the arrow's own depth names every binder `__pi0` — internally consistent and idempotent, so a type compared against its own canonical form still matches. The first version of these tests did exactly that and passed under the injected fault; `canonical_binders_keep_distinct_binders_distinct` and `spec_key_keeps_distinct_binders_distinct` fail under it.
@dpmills
dpmills force-pushed the dmills/canonical-pi-binders branch from 777736f to 2194bc3 Compare August 18, 2026 05:22
@dpmills

dpmills commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by the scoped-inference-variables branch (dmills/scoped-infer-vars): the flatten-time canonicalization is replaced by the locally-nameless coordinate — indices assigned once at abstraction instead of canonical names assigned across three walks with an unchecked birth-to-flatten window. This PR's regression tests carried over as that design's acceptance tests (see type-inference.md, "Scoped inference variables: stored fragments close against a telescope"). #77 and up are restacked onto the new branch.

@dpmills dpmills closed this Aug 18, 2026
@dpmills
dpmills deleted the dmills/canonical-pi-binders branch August 18, 2026 22:00
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