Skip to content

Hold a refined type's refinements in one unordered set instead of nested layers - #77

Open
dpmills wants to merge 1 commit into
dmills/scoped-infer-varsfrom
dmills/refinement-set
Open

Hold a refined type's refinements in one unordered set instead of nested layers#77
dpmills wants to merge 1 commit into
dmills/scoped-infer-varsfrom
dmills/refinement-set

Conversation

@dpmills

@dpmills dpmills commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

A coalesced type's refinement layers stacked in constraint arrival order: two refined upper bounds meeting at one variable produced {{T | q} | p} or {{T | p} | q} by which arrived first. Subtyping never cared — the deficit machinery compares layers as a set — but Type's equality did, and equality is load-bearing wherever a type is an identity: the trivial-equality short-circuit, cache keys, the recorded-vs-recomputed walls. One Vec was serving three readings at once: a set to subtyping, a stack to planning, an identity to SpecKey.

Type::Refinement now carries a RefinementSet — unordered, deduplicated, set-semantic Eq/Hash — and Type::refined flattens, so {{T | p} | q} is unrepresentable and "which layer is outermost" cannot be asked. A canonical sort was tried twice and rejected: it pins the ambiguity instead of deleting it, and it denies planning the freedom to apply filters in whatever order it likes.

Three things the change makes explicit. Materializing refinements is a pipeline, so planning chooses an application order — which order is free, choosing differently in two places is not, and application_order is the one place it is chosen, keyed on rendered content rather than physical order. The recognizers were reading a chain positionally: groupby now finds the refinement shaped like its key equation and join tries each refinement as the join condition, both leaving the rest on the domain they pass downstream, where dropping them silently discarded a filter. An in-place predicate rewrite can make two refinements equal, and PartialEq reads cardinality, so RefinementSet::rewrite_each re-establishes the dedup instead of each of the eight rewrite sites arguing it.

CAMBRA_REFINEMENT_ORDER=reverse flips every set's physical order and CI runs the suite both ways. Set semantics makes that order meaningless by contract, and a consumer that lets it become observable compiles clean either way.

Review guide: ty.rs carries RefinementSet, the flattening constructors, application_order, and rewrite_each, and the rest follows from them mechanically. The behavioural sites are compact.rs's merge_refinements, the two planning walks over the application order, and the two recognizers. One test pins each of the three sections above: refinements_meeting_at_one_variable_do_not_depend_on_arrival_order, application_elem_types_follow_the_refinement_not_the_position, a_rewrite_that_collapses_two_refinements_leaves_a_set. The docs' "refinement layer" and "innermost first" are converted throughout.

@dpmills
dpmills force-pushed the dmills/refinement-set branch 2 times, most recently from 471a5f7 to f0c8c69 Compare August 12, 2026 00:09
@dpmills
dpmills force-pushed the dmills/refinement-set branch from f0c8c69 to 26a9d57 Compare August 12, 2026 00:38
@dpmills
dpmills force-pushed the dmills/refinement-set branch from 26a9d57 to 6597b8f Compare August 12, 2026 20:43
@dpmills
dpmills force-pushed the dmills/refinement-set branch from 6597b8f to dc09c89 Compare August 12, 2026 22:30
@dpmills
dpmills force-pushed the dmills/refinement-set branch from dc09c89 to 2952fc6 Compare August 12, 2026 22:45
@dpmills
dpmills force-pushed the dmills/refinement-set branch from 2952fc6 to f85a5ce Compare August 13, 2026 05:39
@dpmills
dpmills force-pushed the dmills/refinement-set branch from 8c3604d to d840cdb Compare August 19, 2026 23:34
@dpmills
dpmills force-pushed the dmills/refinement-set branch from d840cdb to edb1f5f Compare August 20, 2026 00:12
@dpmills
dpmills force-pushed the dmills/refinement-set branch from edb1f5f to 945e56c Compare August 20, 2026 20:51
@dpmills
dpmills force-pushed the dmills/refinement-set branch 3 times, most recently from e00d838 to 487c221 Compare August 21, 2026 04:39
@dpmills
dpmills force-pushed the dmills/refinement-set branch from 487c221 to a41b3e3 Compare August 21, 2026 04:55
@dpmills
dpmills force-pushed the dmills/refinement-set branch from a41b3e3 to 8a5d902 Compare August 21, 2026 05:47
@dpmills
dpmills force-pushed the dmills/refinement-set branch 3 times, most recently from 5e588d6 to 63d35b7 Compare August 21, 2026 20:45
@dpmills
dpmills force-pushed the dmills/refinement-set branch from 63d35b7 to c345287 Compare August 21, 2026 22:23
@dpmills dpmills changed the title Represent a refined type's claims as one unordered set instead of nested layers Hold a refined type's refinements in one unordered set instead of nested layers Aug 21, 2026
@dpmills
dpmills force-pushed the dmills/refinement-set branch 5 times, most recently from 707bc7e to f9b95a9 Compare August 22, 2026 01:06
A coalesced type's refinement layers stacked in constraint *arrival* order: two refined upper bounds meeting at one variable produced `{{T | q} | p}` or `{{T | p} | q}` by which arrived first. Subtyping never cared — the deficit machinery compares layers as a set — but `Type`'s equality did, and equality is load-bearing wherever a type is an *identity*: the trivial-equality short-circuit, cache keys, the recorded-vs-recomputed walls. One `Vec` was serving three readings at once: a set to subtyping, a stack to planning, an identity to `SpecKey`.

`Type::Refinement` now carries a `RefinementSet` — unordered, deduplicated, set-semantic `Eq`/`Hash` — and `Type::refined` flattens, so `{{T | p} | q}` is unrepresentable and "which layer is outermost" cannot be asked. A canonical sort was tried twice and rejected: it pins the ambiguity instead of deleting it, and it denies planning the freedom to apply filters in whatever order it likes.

Three things the change makes explicit. **Materializing refinements is a pipeline**, so planning *chooses* an application order — which order is free, choosing differently in two places is not, and `application_order` is the one place it is chosen, keyed on rendered content rather than physical order. **The recognizers were reading a chain positionally**: `groupby` now finds the refinement shaped like its key equation and `join` tries each refinement as the join condition, both leaving the rest on the domain they pass downstream, where dropping them silently discarded a filter. **An in-place predicate rewrite can make two refinements equal**, and `PartialEq` reads cardinality, so `RefinementSet::rewrite_each` re-establishes the dedup instead of each of the eight rewrite sites arguing it.

`CAMBRA_REFINEMENT_ORDER=reverse` flips every set's physical order and CI runs the suite both ways. Set semantics makes that order meaningless by contract, and a consumer that lets it become observable compiles clean either way.

Review guide: `ty.rs` carries `RefinementSet`, the flattening constructors, `application_order`, and `rewrite_each`, and the rest follows from them mechanically. The behavioural sites are `compact.rs`'s `merge_refinements`, the two planning walks over the application order, and the two recognizers. One test pins each of the three sections above: `refinements_meeting_at_one_variable_do_not_depend_on_arrival_order`, `application_elem_types_follow_the_refinement_not_the_position`, `a_rewrite_that_collapses_two_refinements_leaves_a_set`. The docs' "refinement layer" and "innermost first" are converted throughout.
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