]> Repositorios git - classgraph.git/commitdiff
Render instance-context equalities as arrows too
authorJavier Sagredo <[email protected]>
Fri, 19 Jun 2026 16:03:56 +0000 (18:03 +0200)
committerJavier Sagredo <[email protected]>
Fri, 19 Jun 2026 16:03:56 +0000 (18:03 +0200)
Follow-up to the equality-superclass change: instance-context equality
constraints (`instance (a ~ b) => ...`) were drawn as `where a ~ b` chips
baked into the instance node's label. Now they render the same way as
equality superclasses — an edge to a shared `(~)` node labelled
`-{a}-{b}->` — so the two views are consistent.

- ensureEqNode: the shared `(~)` (or `(~~)`) node, modelled as an external
  class node, deduped by the operator's qid so all equalities converge.
- parenthesiseOp: JS mirror of Render.hs's externalLabel (`~` -> `(~)`).
- addInstanceContext draws an `#eq#` edge per equality predicate and still
  surfaces any type families inside it (e.g. `Norm` in `Norm a ~ Int`).
- The instance node label drops the `where` chips; the `eqs` strings stay
  on the node data for the side panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
data/viewer.js

index 15a30c9a69baeefc1c9acbc18c196b5b254db07a..d26d49673c81fe477abac45fa34efb3b7e500615 100644 (file)
       if (seenNodes.has(id)) return id;
       seenNodes.add(id);
       const head = renderInstanceHead(inst, originClassQn);
-      // Equality constraints from the instance's iiContext are rendered
-      // as "chip" lines beneath the head, indented and prefixed with
-      // `where`, instead of being drawn as fake-class edges. This keeps
-      // them visible at-a-glance without polluting the layout.
+      // Equality constraints from the instance's iiContext used to be
+      // baked into the node label as `where a ~ b` chips. They're now
+      // drawn as edges to a shared `(~)` node (see addInstanceContext),
+      // mirroring how the classes view renders equality superclasses, so
+      // the node label is just the instance head. The `eqs` strings are
+      // still exposed on the node data for the side panel / tests.
       const eqs = (inst.iiContext || [])
         .filter(p => p.piIsEq)
         .map(p =>
           p.piClass.qnName + ' ' +
           renderArg(p.piArgs[1], inst.iiTyVars)
         );
-      const label = eqs.length === 0
-        ? head
-        : head + '\n' + eqs.map(s => 'where ' + s).join('\n');
       els.push({ group: 'nodes', data: {
         id,
-        label,
+        label: head,
         kind: 'instance',
         orphan: !!inst.iiOrphan,
         classId: qid(inst.iiClass),
       return id;
     }
 
+    // The shared node an equality constraint (`a ~ b`) points at. Modelled
+    // as an external class node labelled `(~)` — identical to how the
+    // classes view renders equality *superclasses* — so all equalities in
+    // this instance view converge on one `(~)` (or `(~~)`) node, each via
+    // its own `-{a}-{b}->` labelled edge. Deduped by the operator's qid.
+    function ensureEqNode(opQn) {
+      const id = qid(opQn);
+      if (seenNodes.has(id)) return id;
+      seenNodes.add(id);
+      els.push({ group: 'nodes', data: {
+        id,
+        label: parenthesiseOp(opQn.qnName),
+        kind: 'class',
+        external: true,
+        classId: id,
+        package: opQn.qnPackage,
+        module: opQn.qnModule,
+      }});
+      return id;
+    }
+
     // For every distinct type family referenced anywhere inside `args`,
     // ensure a family node exists, draw a "via family" edge from the
     // origin node, and lift the family's known type family instances into
     // node builders and appends to the shared element list.
 
     // Context constraints — the "new classes the instance declares as needed
-    // for the implementation". Equality predicates (a ~ b) don't point at a
-    // class node (they appear in the side panel), and globally muted classes
-    // are skipped.
+    // for the implementation". Globally muted classes are skipped. Equality
+    // predicates (a ~ b) point at a shared `(~)` node via a `-{a}-{b}->`
+    // labelled edge, the same way the classes view renders equality
+    // superclasses.
     function addInstanceContext(inst, instId) {
       inst.iiContext.forEach((pred, pi) => {
-        if (pred.piIsEq) return;
         if (mutedSet.has(qid(pred.piClass))) return;
+        if (pred.piIsEq) {
+          const eid = ensureEqNode(pred.piClass);
+          els.push({ group: 'edges', data: {
+            id: instId + '#eq#' + pi,
+            source: instId,
+            target: eid,
+            kind: 'context',
+            label: pred.piArgs
+              .map(a => '{' + renderArg(a, inst.iiTyVars) + '}')
+              .join(' '),
+          }});
+          // Surface any type-family applications hiding in the equality
+          // (e.g. the `Norm` in `Norm a ~ Int`), chained to the `(~)` node.
+          // No predicate class to resolve against, so predClassQn is null.
+          addFamilyLinksFromArgs(pred.piArgs, instId, 'eq-fam',
+                                 null, inst.iiTyVars, eid);
+          return;
+        }
         // Render the predicate (e.g. `ShelleyBasedEra era`) as its own node,
         // not as a class node with a "ctx: era" arrow — it's morally a
         // place-holder for whichever instance satisfies it at use time.
     return null;
   }
 
+  // Parenthesise a symbolic operator name so it reads the way it would in
+  // prefix position (`~` -> `(~)`). Mirrors Render.hs's externalLabel: a
+  // name is "operator-like" when it doesn't start with a letter, `_`, `(`
+  // or `[`. Alphabetic names (e.g. `Coercible`) pass through untouched.
+  function parenthesiseOp(name) {
+    if (!name) return name;
+    return /^[A-Za-z_([]/.test(name) ? name : '(' + name + ')';
+  }
+
   // Recognise GHC's promoted-list constructors. The dumps use the
   // unprimed forms (`:`, `[]`) for the type-level cons / nil — GHC's
   // pretty-printer drops the leading apostrophe when round-tripping