-# How classgraph gets its data
-
-This document explains where every piece of information in the viewer
-comes from, how type-family resolution works, and what the user can and
-cannot trust about the rendered chains. It's targeted at someone who
-wants to know whether a given edge in the graph is "real" GHC information
-or an inferred (possibly fragile) reconstruction.
-
-## Table of contents
-
-- [Pipeline at a glance](#pipeline-at-a-glance)
-- [Where extraction runs](#where-extraction-runs)
-- [Classes](#classes)
-- [Class instances](#class-instances)
-- [Type and data families](#type-and-data-families)
-- [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)
-- [Synthetic "Family args = ?" placeholders](#synthetic-family-args---placeholders)
-- [What the viewer is *not* doing](#what-the-viewer-is-not-doing)
-
-## Pipeline at a glance
+# How classgraph processes the data
-```
- ┌────────────────────┐ typecheck ┌─────────────────────┐
- │ user's source code │ ──────────────► │ TcGblEnv (in GHC) │
- └────────────────────┘ │ tcg_tcs │
- │ tcg_insts │
- │ tcg_fam_insts │
- └──────────┬──────────┘
- │ typeCheckResultAction
- ┌──────────▼──────────┐
- │ Classgraph.Extract │
- │ (this repo's lib) │
- └──────────┬──────────┘
- │ JSON
- ┌──────────▼──────────┐
- │ classgraph-view │
- │ merge + render │
- └─────────────────────┘
-```
-
-All extraction happens **after typechecking** but **before desugaring to
-Core**. By that point GHC has already:
-
-- Resolved all class superclass theta to fully simplified `PredType`s.
-- Fully kind-elaborated every `Type`.
-- Computed the closed normal form of class instance heads.
-- Fully populated `tcg_insts` (the local instances) and
- `tcg_fam_insts` (the local family instances).
-
-That's important: every fact we record is *what GHC itself sees and
-uses* once typechecking is done. We don't run our own typechecker, our
-own constraint solver, or our own type-family reducer over the source
-code. We just *transcribe* GHC's `TcGblEnv` into JSON.
-
-The single exception — **class-instance resolution chains via type
-families** — happens in the viewer (JavaScript), purely against the
-already-extracted JSON. That logic *is* a reconstruction, and is
-called out explicitly in the
-["Type family resolution in the viewer"](#type-family-resolution-in-the-viewer)
-section below.
-
-## Where extraction runs
-
-The plugin registers a single hook:
-
-```haskell
-plugin = defaultPlugin
- { typeCheckResultAction = \_args _summ tcg_env -> do
- liftIO (writeJsonForModule tcg_env)
- return tcg_env
- }
-```
-
-`typeCheckResultAction :: [CommandLineOption] -> ModSummary -> TcGblEnv -> TcM TcGblEnv`
-runs once per module compiled with the plugin, after the typechecker
-has finished but *before* GHC desugars to Core. We never modify
-`tcg_env`; the plugin is observation-only.
-
-This is the latest hook at which all the extracted data is reliably
-available without us having to do interface-file fishing. In
-particular, `tcg_insts` and `tcg_fam_insts` are already populated by
-this point with every instance the user wrote in this module, including
-deriving-clause instances and standalone-deriving instances.
-
-## Classes
-
-Source: `tcg_tcs :: [TyCon]` filtered by `tyConClass_maybe`. For each
-`Class` the schema records:
-
-| Schema field | GHC API |
-|-------------------|-----------------------------------------|
-| `ciName` | `className cls` — the class's `Name` |
-| `ciTyVars` | `classTyVars cls` |
-| `ciSuperclasses` | `classSCTheta cls`, decomposed below |
-| `ciAssocTypes` | `classATs cls` — the assoc family TyCons|
-| `ciMethods` | `classMethods cls` (only the names) |
-| `ciSrc` | `nameSrcSpan (className cls)` |
-
-Superclass theta needs a tiny bit of decomposition because GHC
-represents some constraint shapes structurally:
-
-- We use `classifyPredType :: PredType -> Pred` to split every
- superclass into a (`ClassPred`, `EqPred`, `IrredPred`, `ForAllPred`).
-- `ClassPred (cTupleClass _) [a, b, ...]` is a constraint tuple — we
- recurse into each `a`, `b` and emit one `SuperclassEdge` per element.
- This is needed because GHC sometimes hands us a single tuple-shaped
- pred where the user wrote `class (Foo a, Bar a) => …`.
-- The boxed equality classes `(~)` and `(~~)` are technically
- `ClassPred`s (they really are classes in GHC's type theory), but for
- rendering we recognise them by their `OccName` and emit them as
- equality predicates with two operands rather than as edges to a
- synthetic `~` node.
-- For all other class predicates we emit one edge.
-
-Args of class predicates run through a `visibleArgs` filter
-(`tyConBinders` + `isInvisibleTyConBinder`), so kind / runtime-rep
-arguments don't leak into the rendered superclass list.
-
-## Class instances
-
-Source: `tcg_insts :: [ClsInst]`. Each `ClsInst` becomes one
-`InstanceInfo`:
-
-| Schema field | GHC API |
-|----------------|------------------------------------------------------|
-| `iiClass` | from `instanceSig inst` |
-| `iiArgs` | from `instanceSig inst` (the head args) |
-| `iiContext` | from `instanceSig inst` (the theta), decomposed |
-| `iiTyVars` | from `instanceSig inst` |
-| `iiOrphan` | `isOrphan (is_orphan inst)` |
-| `iiOverlap` | pretty-print of `is_flag inst` |
-| `iiSrc` | `nameSrcSpan (varName (is_dfun inst))` |
-
-`instanceSig :: ClsInst -> ([TyVar], [Type], Class, [Type])` returns the
-fully resolved (`tvs`, `theta`, `cls`, `args`) for the instance's
-dictionary function. That theta is whatever GHC stored on the dfun
-after typechecking. So the context we display is exactly what the
-solver would require to use the instance.
-
-Two non-obvious choices:
-
-- **`iiSrc` from the dfun, not the class.** The dfun's `Name` is tagged
- with the source location of the `instance …` declaration GHC
- synthesised it from. The class's `Name` only has a useful span when
- the class was defined in a module compiled in the *current* package —
- for classes loaded from another package's `.hi` file the span is
- `UnhelpfulSpan` (interface files don't preserve source locations).
- Using the dfun span means "Defined at" works uniformly.
-
-- **`iiContext` decomposition** uses the same `classifyPredType` +
- CTuple / equality logic as superclass theta, so a user-written
- `instance (Foo a, Bar a) => Baz a` produces two distinct context
- predicates rather than one tuple-shaped one.
-
-## Type and data families
-
-Source: `tcg_tcs` filtered by `isFamilyTyCon`, plus the union with
-every class's `classATs` (associated families belong to a class but
-aren't always also in `tcg_tcs` directly). Per family:
-
-| Schema field | GHC API |
-|-----------------|--------------------------------------------------|
-| `tfName` | `tyConName tc` |
-| `tfTyVars` | `tyConTyVars tc` |
-| `tfFlavor` | from `famTyConFlav_maybe tc` and the class index |
-| `tfResultKind` | `tyConResKind tc`, pretty-printed |
-| `tfSrc` | `nameSrcSpan (tyConName tc)` |
-| `tfEquations` | for closed families: branches of the `CoAxiom` |
-
-Flavor taxonomy:
-
-- `OpenSynFamilyTyCon` → `OpenFam`
-- `ClosedSynFamilyTyCon (Maybe (CoAxiom Branched))` → `ClosedFam`,
- with the branches extracted into `tfEquations`
-- `BuiltInSynFamTyCon{}` → `ClosedFam` (treated as closed for our purposes)
-- `DataFamilyTyCon{}` → `DataFam`
-- Anything found under some class's `classATs` → `AssocFam parentClass`
-
-Closed-family equations don't live in `tcg_fam_insts` — they're
-`CoAxBranch`es inside the family's `CoAxiom Branched`. We extract them
-via `coAxBranches (co_ax_branches ax)` and treat each branch as a
-`FamInstInfo` for rendering purposes.
-
-## Type family instances
-
-Source: `tcg_fam_insts :: [FamInst]`. Each becomes one `FamInstInfo`:
-
-| Schema field | GHC API |
-|---------------|------------------------------------------------------------|
-| `fiFamily` | `fi_fam fi` |
-| `fiTyVars` | `fi_tvs fi` |
-| `fiArgs` | `fi_tys fi`, filtered through `visibleArgs (famInstTyCon fi)` |
-| `fiRhs` | `fi_rhs fi` (note: for data fam-instances this is the synthetic R: TyCon, which we then rewrite — see below) |
-| `fiSrc` | `coAxBranchSpan (coAxiomSingleBranch (fi_axiom fi))` |
-| `fiIsData` | `fi_flavor fi == DataFamilyInst _` |
-| `fiDataCons` | `tyConDataCons rep_tc`, where `rep_tc` is pulled out of `DataFamilyInst rep_tc` (see below) |
-| `fiDefinedIn` | filled in by the merge step from the enclosing `ModuleDump`'s package id (after normalisation) |
-
-A few extraction choices worth documenting:
-
-- **Visible-arg filter on `fi_tys`.** GHC stores the kind args of a
- poly-kinded family alongside the value args. For
- `data family Wrap (l :: K)` we'd otherwise get
- `Wrap (((TYPE (BoxedRep Lifted)) -> …) l)` instead of `Wrap l`.
- `tyConBinders (famInstTyCon fi)` tells us which positions are
- invisible binders; we drop those.
-
-- **`fiSrc` from the axiom branch, not the family name.** Each
- fam-instance has its own `CoAxiom Unbranched` representing the
- equation; `coAxBranchSpan` of its single branch points at the
- `data instance` / `type instance` declaration in the user's source.
- The family's `Name` would point at the `data family` / `type family`
- declaration, which is wrong for every instance after the first.
-
-- **Data-family RHS is rewritten to the abstract family form.** GHC
- represents `data instance Foo Args = …` internally as a synthetic
- `R:FooArgs` TyCon. When `R:FooArgs` shows up inside a constraint
- or instance head, the user reads it as `Foo Args` (the abstract
- application). `Classgraph.Extract.typeArg` gates on
- `tyConFamInst_maybe` *before* the regular `TyConApp` path: a TyCon
- for which it returns `Just (parent, parentArgs)` is emitted as
- `FamilyApp parent parentArgs'`, where `parentArgs'` is
- `parentArgs` after substituting the rep TyCon's tyvars
- (`tyConTyVars rep_tc`) with the use-site args from
- `splitTyConApp_maybe`. Without that substitution, `parentArgs`
- references the rep's *internal* tyvars and they leak through
- `typeArg` as `OtherArg "<reptv>"` because they're not in the
- caller's `boundTvs`.
-
-- **Data-family RHS is *also* circular by construction.** The
- rewrite above means the `fiRhs` of a data fam-instance is
- structurally equal to its LHS (e.g. for `data instance Crate
- Int = …`, `fiRhs` is `FamilyApp Crate [Int]`). The viewer hides
- the RHS for data fam-instances in node labels (and in the side
- panel — see below); `reduceTypeArg` skips `fiIsData` rows
- entirely to avoid an infinite recursion that would otherwise
- loop forever rewriting `Crate Int` → `Crate Int`.
-
-- **`fiDataCons` from `rep_tc`, not `famInstTyCon`.** For a data
- fam-instance, `famInstTyCon` returned the family TyCon (not the
- rep) on at least GHC 9.14.1, opposite to what the GHC docs
- suggested. We pull the rep TyCon directly out of the
- `DataFamilyInst rep_tc` constructor and call `tyConDataCons` on
- it. Each `DataCon` becomes a `DataConInfo` with the constructor's
- name, its arg types (as `TypeArg`s in the fam-instance's
- `fiTyVars` scope), and its field labels (when record syntax is
- used; otherwise an empty list). The viewer renders just the
- constructor *names* on the node label (`Family args = ConA |
- ConB`), and the full constructor declarations (with field types
- / record fields) in the side panel.
-
-## Type-family resolution in the viewer
-
-So far everything has been *transcription*: we record what GHC told us
-and stop. The one place we go beyond transcription is the **resolution
-chains** in the instance view. This part is computed in JavaScript
-against the already-extracted JSON, *not* by GHC.
-
-Setup. For an instance view focused on a class `C`, we draw — among
-other things — every superclass requirement of `C`'s declaration with
-the focused-instance's args substituted in. Concretely, for each
-superclass edge `seSuperclass = S, seArgs` of `C`, and each instance
-`inst` of `C`, the constraint is `S (subst seArgs inst.iiArgs)`.
-
-That `subst seArgs inst.iiArgs` may produce something with a
-`FamilyApp` in it, e.g. `S (F a) (F b)` where `F` is some type family.
-The viewer then asks "for which fam-instances of `F` does this pred
-become solvable, and which class instance of `S` would satisfy it?".
-
-The algorithm:
-
-1. **Find the family applications.** `collectFamilyRefs` walks the
- substituted args and collects every `FamilyApp` qualname.
-
-2. **For each fam-instance `fi` of that family:**
-
- 1. **Bidirectional unification** between `fi.fiArgs` and the
- `FamilyApp`'s args. `biUnify` allows TyVarRefs on either side
- to bind to anything on the other side. *If unification fails,
- this `fi` is irrelevant — skip it entirely.* (This is what
- stops e.g. a `Foo Int`-shaped predicate from being matched
- against a `type instance F Bool = …` equation.) Note that
- `biUnify` operates on the args *as written*: we deliberately
- do **not** pre-reduce nested type-family applications inside
- the pivot's args before unifying. Doing so would let the
- outer family match every concrete fam-instance once the
- inner reduction collapsed to a tyvar, which produces an
- explosion of spurious chain edges (every concrete data
- instance "matches" an abstract `F a`). The trade-off: we
- miss legitimate chains through nested families like `F (G
- a)` where `G a` would reduce to a useful shape — those will
- take the placeholder path in the next section.
-
- 2. **Substitute `fi.fiRhs` into the predicate.** The bidirectional
- unification gave us substitutions for both fi's tyvars (used to
- rewrite fi's RHS) and the predicate's tyvars (used to specialise
- the rest of the predicate). The result is a fully-substituted
- predicate with no occurrence of the originating family.
-
- 3. **Reduce the result.** `reduceTypeArg` walks the resulting
- `TypeArg` and tries to apply *any* available fam-instance
- (using one-sided unification — predicate vars stay free) to
- collapse remaining family applications. This handles chained
- reductions like `F (G x)` where `F` and `G` are both reducible.
- Data fam-instances are skipped here (their `fiRhs` is
- structurally equal to the LHS post-rewrite, so naïve
- recursion looped forever).
-
- 4. **Find a matching class instance.** `findMatchingInstances`
- scans `pdInstances` for an instance of the predicate's class
- whose `iiArgs` shape-match the reduced args (TyVarRefs on
- either side acting as wildcards). If one or more are found,
- draw a `fam-resolves` edge from the `fi` node to each.
-
- 5. **Fall back to the predicate node.** If no class instance
- matched (typically because the class's defining package
- isn't in the dumps), draw a `fam-resolves` edge from the
- `fi` node to the originating *predicate node* (the one
- created from the context constraint or unmatched superclass).
- Without this fallback the fam-instance ended up visually
- orphaned even though we knew exactly which constraint it was
- discharging.
-
- 6. **Draw the chain.** Net result: focused instance → predicate
- node → fam-instance(s) → class instance(s) (or back to the
- predicate node if no class instance was found).
-
-If the relevance check kills every `fi` of `fa` — see the next
-section on placeholders for what happens in that branch.
-
-## Are the resolution chains deterministic?
-
-For the **transcribed data** (classes, instances, type families,
-fam-instances, contexts, superclass thetas, source spans, flags) the
-answer is unambiguous: it's exactly what GHC observed at the end of
-typechecking. No reconstruction, no heuristic, no overlap resolution
-on our side. If you re-run the plugin against the same source with the
-same GHC version you get bit-identical JSON.
-
-For the **resolution chains in the instance view**, the picture is
-more nuanced. Here's what can and cannot vary:
-
-- **Open type families with overlapping instances.** GHC forbids
- overlapping open-family equations, so for any concrete argument list
- there is at most one applicable `type instance`. But the viewer
- walks fam-instances by *order in `pdFamInstances`*, which depends on
- the order modules were compiled. If you have `type instance F Int = …`
- in `pkg-A` and `type instance F Bool = …` in `pkg-B`, both are
- presented as alternative resolutions; the user sees both chains
- because both are *possible* satisfiers — without further input
- (`a := Int` vs `a := Bool`) GHC itself can't pick one. The viewer
- is honest about that ambiguity rather than hiding it.
-
-- **Closed type families with branch order.** GHC's evaluation rule is
- "first matching branch wins". The viewer's `reduceTypeArg` mimics
- this: it tries fam-instances in extraction order and stops at the
- first that unifies. For closed families this is the same order as
- the source-code branches, so the result matches GHC's. For open
- families across multiple modules the order is build-graph-dependent
- but, because open-family overlap is forbidden, the *set* of
- applicable equations is unique even if the iteration order isn't.
-
-- **Ambiguous matches in `findMatchingInstances`.** Our class-instance
- matcher treats TyVarRefs on either side as wildcards. That's
- intentionally permissive: in the absence of a full constraint solver
- we'd rather show the user "any of these instances might apply" than
- hide instances behind a stricter check that would miss real
- resolutions. The user sees every match as a separate chain edge and
- decides which is meaningful in their context.
-
-- **Reductions stopping early.** If a fam-instance's RHS *itself*
- mentions another type family (a common shape: `type Element (Wrap
- c) = Element c` inside an associated family with the same name),
- the viewer's reducer can't make further progress unless the
- next-step fam-instance is also in the dumps. Chains terminate at
- whichever fam-instance node the reducer last successfully unified
- with — honest about the dependency boundary.
-
-- **Type-family functions defined by GHC itself** (e.g.
- `Data.Type.Equality.==`, `If`, type-level arithmetic from
- `GHC.TypeLits`) come through as `BuiltInSynFamTyCon` and are
- transcribed but **not** evaluated by the viewer. We don't
- reimplement GHC's built-in family rewrites. If a chain involves
- `1 + 1` where `(+)` is `GHC.TypeNats.+`, the viewer will stop at
- `+` rather than producing `2`.
-
-- **`UndecidableInstances` / cyclic reduction.** `reduceTypeArg`
- recurses on `applySubst(fi.fiRhs, subst)` after a matching
- fam-instance is found, so a `type instance F a = G a` / `type
- instance G a = F a` pair *would* loop forever if it weren't for
- the fact that each step requires structural progress (the RHS
- isn't structurally equal to the LHS). In practice such a circular
- pair shows up as two separate fam-instance nodes pointing at each
- other via `fiRhs` references, with no resolution-chain edge.
-
-- **Data fam-instances are skipped by `reduceTypeArg` entirely.**
- After the data-family R: rewrite at extraction time (see
- `Classgraph.Extract`'s `typeArg`), a data fam-instance's `fiRhs`
- is structurally equal to its LHS — for `data instance Crate Int`,
- `fiRhs = FamilyApp Crate [Int]`. Reducing `FamilyApp Crate [Int]`
- through that fam-instance just produces `FamilyApp Crate [Int]`
- again, so naïvely recursing through it loops forever. The reducer
- guards against this by skipping any fam-instance with
- `fiIsData = true`; data-family use sites then fall through
- unchanged and are surfaced by the predicate-node + chain machinery
- as the use sites they actually are. The synthetic R: TyCon (the
- pre-rewrite form) was opaque for the same reason — we just never
- hit the loop because the rewriter wasn't yet circular.
-
-So: *the data is deterministic and faithful, the chains are an honest
-best-effort visualization*. When in doubt, the chain edges in the
-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".
-
-## Synthetic "Family args = ?" placeholders
-
-When a context predicate or unmatched superclass mentions a `FamilyApp`
-that *no* fam-instance can be unified with, the viewer synthesises a
-placeholder fam-instance node showing the use-site args and `= ?` for
-the RHS. The chain reads:
+The pipeline has three stages:
```
-SigDSIGN (grey diamond) ──► SigDSIGN Ed448DSIGN = ? ╶╶► NoThunks (SigDSIGN Ed448DSIGN)
+ plugin (per module) merge + render one HTML file
+ TcGblEnv ──► JSON dumps ──► classgraph-view ──► classgraph.html
+ (.classgraph/*.json)
```
-Where this lives: `addFamilyLinksFromArgs` in `viewer.js`, after the
-real-fam-instance loop. If the loop's `anyRelevant` flag is still
-false at the end, we walk the args again with `collectFamilyAppArgs`
-to find every distinct application of that family and synthesise one
-placeholder per use site.
-
-A few things to know:
-
-- **Trigger is broader than "the family is external".** The
- placeholder fires whenever no fam-instance unifies with the use site
- — typically because the family is genuinely external (no equations
- in our dumps), but also when a local family has equations for `Int`
- and `Bool` and the constraint mentions `F SomeOtherType`.
- Structurally honest, just slightly overgenerous in name.
-
-- **Chain edge to the predicate is conditional.** The synthetic
- placeholder gets a `fam-resolves` chain edge `placeholder →
- origin-pred-node` *only when* the caller passes an `originPredId`.
- That's set when the family use was inside a context predicate (we
- always have a pred-node id) or an unmatched superclass requirement
- (we just created one). It's `null` for matched superclass calls,
- where there's no single pred-node target — the placeholder still
- appears connected to the family, just without the chain leg.
-
-- **Dedup is per (family, use-site args).** If the same `SigDSIGN
- Ed448DSIGN` shows up in multiple constraints of the focused
- instance, only one placeholder appears, with multiple incoming
- chain edges if multiple predicates referred to it. The dedup key
- is `'unresfaminst:' + qid(family) + ':' + JSON.stringify(args)`.
-
-- **The placeholder pretends to be a fam-instance node.** Internally
- it's a synthetic `FamInstInfo`-shaped object with `_unresolved:
- true`, `fiRhs: { tag: 'OtherArg', contents: '?' }`, and the
- focused-instance's tyvars in `fiTyVars` (so its label renders
- against the right tyvar context). `ensureFamInstanceNode` mirrors
- `_unresolved` to a top-level `data.unresolved` so cytoscape
- selectors can target it for the dashed-grey styling.
-
-- **External families pick up matching styling.** A family node
- whose `qid` isn't in `pdTypeFamilies` is tagged `external: true`
- by `ensureFamilyNode`; the cytoscape rule
- `node[kind = "family"][?external]` paints it grey-dashed. Same
- visual language as the placeholder, so the chain reads as one
- continuous "outside this project" thread.
-
-- **Unapplied-family use sites get no placeholder.** When a
- constraint references a family with *no* args — passed as a
- higher-kinded thing, e.g. `class HasTables l where …` and a
- context `HasTables LedgerState` where `LedgerState` is a data
- family of kind `Type -> MapKind -> Type` left unapplied — the
- use-site args list is empty. A `LedgerState = ?` placeholder
- would convey nothing beyond what the family node itself shows,
- so the synthesizer skips empty-args use sites entirely.
- Concretizing the unapplied form to e.g. `LedgerState
- (ShelleyBlock proto era) mk` would require constraint-solver-
- like propagation across the constraint set (matching `l ~
- LedgerState (ShelleyBlock proto era)` from elsewhere) — work
- GHC does at typecheck time but we don't preserve in the dump.
-
-- **Side panel honestly explains why a placeholder fired.** The
- panel for a placeholder fam-instance lists both common reasons
- (external family vs. nested/abstract args we couldn't reduce
- through) so the user knows which dump to add or which limitation
- is in play, rather than silently looking unresolved.
-
-## What the viewer is *not* doing
-
-A few things we deliberately don't do, in case you wonder:
-
-- **No instance overlap resolution.** GHC's instance solver picks one
- instance from a set of applicable ones using overlap pragmas
- (`{-# OVERLAPPING #-}` etc.), specificity, and module-context rules.
- We record `iiOverlap` as text on each instance but don't simulate
- the solver. If two instances of the same class match a given
- argument shape, the viewer shows both with the same chain edge.
-
-- **No constraint solving.** We don't try to compute, given a type, the
- full set of class instances reachable through context constraints.
- The instance view shows direct context predicates (`iiContext`) and
- one hop of superclass requirements; transitive resolution is left
- to the user clicking through.
-
-- **No quantified-constraint handling.** `forall a. Eq a => Eq (T a)`
- appearing in a context is `ForAllPred` and we drop it from the
- `iiContext` display. Quantified constraints are GHC-9-and-up but
- rare; adding them would require rendering the implication shape.
-
-- **No analysis of `INCOHERENT` / `OVERLAPPABLE` / overlap warnings.**
- We just show every instance.
-
-- **No re-typechecking when JSON is loaded.** The viewer trusts the
- JSON. If you hand-edit `.classgraph/*.json`, the viewer will happily
- render whatever's there — it has no way to detect inconsistency.
-
-If any of these matter for your use case and the answer is "we should
-do it", the place to start is `Classgraph.Extract` (for fields we'd
-need to add to the schema) or `viewer.js`'s `buildInstanceView` /
-`reduceTypeArg` / `biUnify` (for the in-viewer reconstruction).
+The guiding rule: **the plugin transcribes, it does not infer.** Everything
+in the JSON is what GHC itself computed by the end of typechecking. The only
+place we go beyond transcription is the type-family *resolution chains* drawn
+in the instance view — that runs in the browser against the JSON, and is
+called out explicitly below.
+
+## Extraction (`Classgraph.Extract`)
+
+The plugin registers `typeCheckResultAction`, which runs once per module
+after typechecking (so superclasses, instance heads, and family equations are
+fully resolved) and before desugaring. It only reads `TcGblEnv`; it never
+modifies it.
+
+**Classes** — `tcg_tcs` filtered by `tyConClass_maybe`.
+
+| field | source |
+|---|---|
+| `ciName` / `ciTyVars` / `ciMethods` | `className` / `classTyVars` / `classMethods` |
+| `ciSuperclasses` | `classSCTheta`, decomposed (see below) |
+| `ciAssocTypes` | `classATs` |
+| `ciSrc` / `ciDoc` | `nameSrcSpan`; Haddock via the docs map (when `-haddock`) |
+
+Superclass theta is decomposed with `classifyPredType`: constraint tuples
+(`class (A x, B x) => …`) expand into one edge each, and the boxed-equality
+classes `(~)`/`(~~)` become equality predicates (rendered infix) rather than
+edges to a synthetic class node. Class-predicate args are filtered through
+`visibleArgs` so kind/runtime-rep arguments don't leak in.
+
+**Instances** — `tcg_insts`, via `instanceSig` (gives `tvs`, `theta`, class,
+head args). The context (`iiContext`) is decomposed the same way as
+superclass theta. Two deliberate choices: `iiSrc` uses the *dfun's* name span
+(the class span is `UnhelpfulSpan` for classes from other packages' `.hi`
+files), and `iiDoc` is looked up by the dfun's name.
+
+**Families** — `tcg_tcs` filtered by `isFamilyTyCon`, unioned with each
+class's `classATs`. Flavor is decided by checking `classATs` membership first
+(→ `AssocFam`), then `famTyConFlav_maybe` (open / closed / data; built-in and
+abstract-closed families count as closed). Closed families keep their
+equations: they live as `CoAxBranch`es in the family's `CoAxiom`, pulled out
+with `fromBranches` into `tfEquations` (they are *not* in `tcg_fam_insts`).
+
+**Family instances** — `tcg_fam_insts`. `fiArgs` is `fi_tys` after
+`visibleArgs`; `fiSrc` is the axiom branch span (each `type/data instance`,
+not the `family` declaration). For a `data instance`, `fiDataCons` comes from
+the representation TyCon (`tyConDataCons rep_tc`) with the constructor arg
+types substituted from the rep TyCon's tyvars into the instance's `fiTyVars`,
+so positional `TyVarRef`s resolve correctly.
+
+**Type arguments** (`typeArg` → `tyConArg` → `dataFamilyRepArg`) turn a GHC
+`Type` into the structured `TypeArg` the viewer consumes:
+
+- bound variable → `TyVarRef <index>` (index into the enclosing scope);
+- function arrow → a synthetic `(->)` application (no multiplicity/kind args);
+- a `data instance` representation TyCon (`R:Foo…`) → the abstract
+ `Foo Args` it stands for, substituting the rep tyvars for the use-site args;
+- any other saturated TyCon → `TyConApp` / `FamilyApp` (invisible binders
+ dropped), and a type literal → `LitArg`;
+- anything else → `OtherArg` with GHC's pretty-printed text. Printing uses an
+ `SDocContext` that suppresses runtime-reps, explicit kinds, linearity,
+ etc., so fallbacks read like source Haskell rather than compiler internals.
+
+## Merge (`Classgraph.Merge`)
+
+`classgraph-view` reads every `*.json` under each `--input` dir and merges
+them. Because directory listing order is unspecified, dumps are **sorted by
+`(package, module)` first**, making the merge — and the rendered element
+order — reproducible. Then:
+
+- classes and families are deduplicated by `QualName` (first occurrence
+ wins), so a class defined in one dump and seen as `external` in another
+ collapses to one node;
+- package ids are normalised (`pkg-1.0-inplace`, `pkg-1.0-<hash>`, … → `pkg`)
+ before dedup, so cross-dump references actually unify;
+- each instance / family-instance is tagged with its defining package
+ (`iiDefinedIn` / `fiDefinedIn`), which the viewer uses for editor links —
+ this can't be recovered after the merge for orphan instances.
+
+## Render (`Classgraph.Render`)
+
+The page is one self-contained file: Cytoscape + dagre, the styling, the
+program JSON, `typecore.js`, and `viewer.js` are all inlined via `file-embed`
+into placeholder slots in `viewer.html`. No server, no CDN. In the embedded
+JSON every `<` is rewritten to its `\u003c` escape, so a `</script>` inside a
+string (e.g. a Haddock comment) can't terminate the data `<script>` or inject
+markup.
+
+## The type core (`typecore.js`)
+
+The unification / matching logic is a DOM-free module so it can be unit-tested
+(`test/typecore.test.js`, plus `test/views.test.js` driving the real builders
+through a stub harness; `npm test`). `viewer.js` binds it to the program with
+`makeTypeCore({ famInstsByFamily, pdInstances })`. The pieces:
+
+- `matchTypeArg` — **one-sided** structural match. A variable in an instance
+ *head* is polymorphic and matches anything; a variable left in the
+ *requirement* is abstract and only matches another variable. (A concrete
+ `Eq Int` instance does not discharge an abstract `Eq a` requirement.)
+- `unify` — one-sided unification (binds the pattern's vars only), used by…
+- `reduceTypeArg` — one rewrite step for a `FamilyApp`: find a family instance
+ whose LHS unifies and return its substituted RHS, recursing. It walks the
+ family index `famInstsByFamily`, which includes closed-family `tfEquations`,
+ and skips `data` instances (their RHS equals their LHS, so reducing would
+ loop). For closed families the equations are in declaration order, matching
+ GHC's first-match rule; open families can't overlap, so order is irrelevant.
+- `biUnify` / `replaceFamilyApp` — bidirectional unification used to ask "if
+ this family resolved via *this* instance, what would the predicate become?"
+- `findMatchingInstances` — instances of a class whose head matches given args.
+
+## Resolution chains in the instance view
+
+This is the one reconstruction. In an instance view for class `C`, each
+superclass `S` of `C` is instantiated with the focused instance's args, giving
+a requirement like `S (F a)`. For each family `F` mentioned
+(`addFamilyLinksFromArgs`):
+
+1. For each instance of `F`, `replaceFamilyApp` tries to substitute it into
+ the requirement. If its LHS doesn't unify with the use site, it's
+ irrelevant — skip it (this is what stops `F Bool`'s equation from matching
+ an `F Int` use). Unification is on the args *as written*; we don't
+ pre-reduce nested families, which would over-match.
+2. The substituted requirement is `reduceTypeArg`'d, then
+ `findMatchingInstances` looks for a class instance of `S` that satisfies
+ it. Each match gets a `fam-resolves` edge from the family-instance node.
+3. If no class instance is in the dumps, the edge instead points back at the
+ predicate node, so the family instance isn't left orphaned.
+
+The chain reads: focused instance → predicate → family instance → satisfying
+instance.
+
+**Placeholders.** If *no* instance of `F` unifies with the use site — usually
+because `F` is defined in a package we didn't extract — a synthetic
+`F args = ?` node is drawn per use site so the chain still terminates
+somewhere (`_unresolved: true`, styled grey-dashed). Use sites where the
+family is unapplied (passed higher-kinded, empty args) are skipped — a bare
+`F = ?` would say nothing the family node doesn't.
+
+**Predicate nodes** are a viewer-only abstraction (no on-disk form). A context
+constraint and an unmatched superclass that are structurally identical
+(class + JSON-encoded args, so tyvar *names* don't matter) collapse to one
+node with two incoming edges. The first role observed sticks. They're rebuilt
+per view and not persisted.
+
+## What's deterministic
+
+- **The transcribed data is ground truth**: same source + same GHC → identical
+ JSON. No heuristics, no overlap resolution on our side.
+- **The merge and render order are reproducible** (dumps are sorted).
+- **The chains are best-effort hints.** They can legitimately show several
+ alternatives (e.g. both `Eq Int` and a polymorphic `Eq a` match a concrete
+ requirement), and they stop at a dependency boundary when the next equation
+ or the satisfying instance lives in a package you didn't extract. When a
+ chain edge looks wrong, the transcribed data is the thing to trust.
+
+## What the viewer does *not* do
+
+- No instance overlap resolution — `iiOverlap` is recorded as text, not acted
+ on; all applicable instances are shown.
+- No constraint solving — only direct context predicates and one hop of
+ superclass requirements; transitive resolution is left to clicking through.
+- No built-in family evaluation — `GHC.TypeLits` arithmetic, `If`, `==`, etc.
+ are transcribed but not computed.
+- No quantified-constraint (`ForAllPred`) display.
+- No re-typechecking — the viewer trusts the JSON as-is.
+
+To extend any of this: the schema and extraction live in `Classgraph.Extract`
+/ `Classgraph.Schema`; the in-viewer reconstruction is `typecore.js` and
+`buildInstanceView` in `viewer.js`.