]> Repositorios git - classgraph.git/commitdiff
Document predicate-node dedup caveats in INTERNALS.md
authorJavier Sagredo <[email protected]>
Wed, 6 May 2026 22:36:11 +0000 (00:36 +0200)
committerJavier Sagredo <[email protected]>
Wed, 6 May 2026 23:51:06 +0000 (01:51 +0200)
New section between the resolution-chain determinism notes and the
"viewer is not doing" wrap-up, covering the structural-hash dedup,
the context-vs-superclass reduction asymmetry, tyvar-index aliasing
across instances, the role-sticky-to-context behaviour, and the
fact that predicate nodes are view-scoped (not persisted).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
docs/INTERNALS.md

index 4a5b7473ea3ff9c8d9a7abcbae8c83648a842389..573c16d1fb262a7749b526bd7113617b01e9de56 100644 (file)
@@ -16,6 +16,7 @@ or an inferred (possibly fragile) reconstruction.
 - [Type family instances](#type-family-instances)
 - [Type-family resolution in the viewer](#type-family-resolution-in-the-viewer)
 - [Are the resolution chains deterministic?](#are-the-resolution-chains-deterministic)
+- [Predicate nodes and how they dedup](#predicate-nodes-and-how-they-dedup)
 - [What the viewer is *not* doing](#what-the-viewer-is-not-doing)
 
 ## Pipeline at a glance
@@ -343,6 +344,71 @@ instance view are hints; the underlying transcribed data
 (`pdInstances`, `pdFamInstances`, `tfEquations`, etc.) is ground
 truth.
 
+## Predicate nodes and how they dedup
+
+The instance view introduces a synthetic node type — `kind: 'predicate'`
+— that doesn't appear in the schema. Two cases produce one:
+
+- **Context constraint** (the predicate sits in `iiContext`). Edge
+  from focused instance → predicate node, labelled `instance context`.
+- **Unmatched superclass** (a `ciSuperclasses` entry whose substituted
+  args don't match any instance in `pdInstances`). Edge from focused
+  instance → predicate node, labelled `superclass constraint`. If the
+  predicate happens to be structurally identical to a context
+  predicate the same node receives both edges; otherwise a fresh node
+  with `role: 'extern'` is created (rendered grey).
+
+This is a viewer-only abstraction. The Haskell side knows nothing about
+it; predicate nodes have no on-disk representation. The dedup is what
+makes the "context AND unmatched superclass" case collapse into a
+single node — which is what a typical reader expects to see when the
+same constraint is required twice.
+
+A few things to be aware of:
+
+- **Dedup key is a structural hash.** Two predicates collapse to one
+  node when their class `QualName` and the JSON-stringified
+  `[TypeArg]` are byte-identical. The hash sees through tyvar names
+  (TyVarRef carries an integer index, not a name), so two instances
+  with `[a, b]` vs `[x, y]` produce the same id structurally. Their
+  *labels* differ (rendered against each instance's own
+  `iiTyVars`) — first call wins on the label.
+
+- **Reduction asymmetry between context and superclass.** Context
+  predicates carry `piArgs` as written. Superclass requirements go
+  through `reduceTypeArg` so we can match them against
+  `pdInstances`. If a fam-instance reduction changes the arg shape —
+  the context says `Foo (F a)`, the reduced superclass says
+  `Foo Int` — the dedup id differs and the user sees two predicate
+  nodes for what is morally one constraint. Realistic but rare in
+  practice.
+
+- **Tyvar index aliasing across instances.** When the focused class
+  has many instances and they happen to produce structurally
+  identical context predicates (different tyvar names, same indices),
+  the predicate node is shared across all of them. That's *probably*
+  what the user wants — fewer noisy duplicates — but means the
+  arrows from several instance nodes land on one shared predicate.
+  No information is lost; the labels just match the first instance
+  that contributed.
+
+- **'extern' role is sticky to the first observation.** If a
+  predicate is observed first as a context constraint (`role:
+  'context'`) and later as an unmatched superclass, the node keeps
+  the `'context'` role and just gains a second edge. If observed in
+  the reverse order — superclass first, context second — same thing
+  happens because context predicates are processed before superclass
+  requirements in `buildInstanceView`. This is intentional; the
+  styling shouldn't downgrade once we know the predicate is also a
+  legitimate context constraint.
+
+- **Predicate nodes don't survive view changes.** The set is rebuilt
+  from scratch every time the user enters an instance view; nothing
+  is persisted. The same predicate, viewed from a different focused
+  class, gets a different node id (because the focused-instance
+  arg/tyvar context differs). That's fine — predicate nodes are
+  scoped to "this view".
+
 ## What the viewer is *not* doing
 
 A few things we deliberately don't do, in case you wonder: