Skip to content

Commit c10da80

Browse files
nanavaticlaude
andcommitted
CType/IType: qualification-scoped tycon comparison, documented invariant
BSC's front end enforces one type constructor per qualified name, so a qualified Id determines its (kind, sort) payload. This invariant was already load-bearing in comparison code -- TyCon Eq ignored the sort, and ITCon comparison in IType.cmpT is by Id alone -- but unstated, and the TyCon instances mixed regimes: Eq compared kinds even for pairs where the invariant makes that a dead tail, while remaining kind-sensitive for the unqualified names where the invariant is undefined. Scope the TyCon comparisons by qualification: * Eq: both-qualified pairs compare by Id (qualEq); the kind hedge is kept only when either side is unqualified (resolution-era fuzz). * Ord: lexicographic on (base, qual); within a (base, qual) bucket, both-qualified ties are EQ without consulting the kinds, and the unqualified bucket keeps the kind comparison. This remains a lawful total order. Document at the instances: the invariant and its qualified-only domain, the deliberate non-transitivity of Eq via qualEq (pre-existing), the Eq/Ord disagreement (pre-existing; containers key on Ord), and the pointer to the tconcheck build step that verifies the handwritten payload copies the by-Id comparisons rely on. Also upgrade the cmpT comment in IType.hs: ITCon by-Id comparison is justified by the same invariant (all ITCon Ids are qualified, since IType is born post-resolution at IConv); the ITForAll binder-kind skip is a separate, alpha-structural assumption, unchanged. The only behavior delta is a both-qualified pair with the same name and differing Maybe Kind fields. An audit of the pipeline found no comparison site where a qualified Nothing-kinded TyCon (parse-era trees, cTCon-built compiler-generated code) can meet its Just-kinded twin (symtab/convCQType-era types): type-level Eq/Ord is only exercised within a single kind regime, cross-regime checks use kind-agnostic helpers (leftCon, mkInstId, Id comparisons), and no container is keyed on Type/CType where mixed twins could coexist. The full testsuite and a rebuild of all libraries with the changed compiler back this empirically. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVL9ornS6uf9hVxAuL7GhK
1 parent f6e3540 commit c10da80

2 files changed

Lines changed: 49 additions & 3 deletions

File tree

src/comp/CType.hs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,39 @@ instance Eq Type where
201201
instance Eq TyVar where
202202
TyVar i n _ == TyVar i' n' _ = (n, i) == (n', i')
203203

204+
-- TyCon comparison and the one-tycon-per-qualified-name invariant
205+
--
206+
-- BSC's front end enforces one type constructor per qualified name, so a
207+
-- qualified Id determines its (kind, sort) payload. The invariant holds
208+
-- ONLY for qualified names: unqualified TyCons exist during scope
209+
-- resolution, where the same base name may denote different constructors.
210+
-- The tconcheck build step (src/comp/tconcheck.hs) verifies the compiler's
211+
-- handwritten copies of Prelude tycon payloads against the compiled
212+
-- Prelude, keeping the invariant checkable.
213+
--
214+
-- Eq is therefore scoped by qualification:
215+
-- * both ids qualified: the invariant's regime. Comparison is by Id;
216+
-- the payloads are determined by the name (checker-verified), so a
217+
-- payload comparison would be a dead tail.
218+
-- * either id unqualified: resolution-era fuzz, where the invariant is
219+
-- undefined; the kind comparison is kept as a hedge.
220+
--
221+
-- Two pre-existing quirks, both deliberate and unchanged:
222+
-- * Eq is non-transitive: qualEq compares by base name alone when either
223+
-- side is unqualified, so P.T == T and T == Q.T but P.T /= Q.T.
224+
-- * Eq and Ord disagree: Ord compares the full qualifier on both sides
225+
-- (a lawful total order), while Eq admits the qualEq fuzz. Containers
226+
-- key on Ord.
204227
instance Eq TyCon where
205-
TyCon i k _ == TyCon i' k' _ = qualEq i i' && k == k'
228+
TyCon i k _ == TyCon i' k' _ = qualEq i i' && (bothQualified i i' || k == k')
206229
TyNum i _ == TyNum i' _ = i == i'
207230
TyStr s _ == TyStr s' _ = s == s'
208231
_ == _ = False
209232

233+
-- both ids carry a package qualifier (see the invariant note above)
234+
bothQualified :: Id -> Id -> Bool
235+
bothQualified i i' = not (isUnqualId i) && not (isUnqualId i')
236+
210237
-- Ord instances
211238

212239
instance Ord Type where
@@ -220,7 +247,15 @@ instance Ord TyVar where
220247
TyVar i n _ `compare` TyVar i' n' _ = (n, i) `compare` (n', i')
221248

222249
instance Ord TyCon where
223-
TyCon i k _ `compare` TyCon i' k' _ = (getIdBase i, getIdQual i, k) `compare` (getIdBase i', getIdQual i', k')
250+
-- lexicographic on (base, qual); within a (base, qual) bucket either
251+
-- both ids are qualified -- unique by the invariant documented at Eq,
252+
-- so EQ without consulting the kinds -- or both are unqualified, where
253+
-- the kind comparison is kept, as in Eq. This remains a lawful total
254+
-- order.
255+
TyCon i k _ `compare` TyCon i' k' _ =
256+
case (getIdBase i, getIdQual i) `compare` (getIdBase i', getIdQual i') of
257+
EQ -> if bothQualified i i' then EQ else compare k k'
258+
o -> o
224259
TyCon _ _ _ `compare` TyNum _ _ = LT
225260
TyCon _ _ _ `compare` TyStr _ _ = LT
226261
TyNum _ _ `compare` TyCon _ _ _ = GT

src/comp/IType.hs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,19 @@ instance Eq IType where
7272
instance Ord IType where
7373
compare x y = cmpT x y
7474

75+
-- ITCon is compared by Id alone. This is justified by the
76+
-- one-tycon-per-qualified-name invariant: BSC's front end enforces one
77+
-- type constructor per qualified name, so a qualified Id determines its
78+
-- (kind, sort) payload -- and all ITCon Ids are qualified, because IType
79+
-- is born post-resolution at IConv. The handwritten payload copies this
80+
-- relies on are verified against the compiled Prelude by the tconcheck
81+
-- build step (src/comp/tconcheck.hs).
82+
--
83+
-- The ITForAll case skips the binder kinds; that is a separate,
84+
-- alpha-structural assumption (same binder Id in the same position implies
85+
-- the same kind), unchanged and unrelated to the tycon invariant.
7586
cmpT :: IType -> IType -> Ordering
76-
cmpT (ITForAll i1 _ t1) (ITForAll i2 _ t2) = -- kind comparison skipped for speed
87+
cmpT (ITForAll i1 _ t1) (ITForAll i2 _ t2) = -- binder kind comparison skipped (see above)
7788
case compare i1 i2 of
7889
EQ -> cmpT t1 t2
7990
o -> o

0 commit comments

Comments
 (0)