]> Repositorios git - classgraph.git/commitdiff
Render instance context constraints as predicate nodes
authorJavier Sagredo <[email protected]>
Wed, 6 May 2026 22:31:10 +0000 (00:31 +0200)
committerJavier Sagredo <[email protected]>
Wed, 6 May 2026 23:51:06 +0000 (01:51 +0200)
Was: instance → class node, edge labelled "ctx: <args>".
Now: instance → predicate node ("Foo a b" rendered like a constraint),
edge labelled "instance context".

The new node kind 'predicate' deduplicates on a structural id
(class qid + JSON-stringified args), so the same constraint surfaces
once even when it appears in multiple superclass / context slots —
the next commit relies on this to merge unmatched-superclass
requirements with overlapping context predicates.

Side panel grows a renderPredicatePanel that explains the node's role
and links to the underlying class.

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

index 376c4019d0301c1695bd5e889620c6be4e96a339..69bb8525814d059a530aba28bf970ffcd818a85d 100644 (file)
@@ -139,6 +139,7 @@ body { display: flex; }
 .swatch-leaf     { border-color: #f59e0b; border-width: 2px; }
 .swatch-family   { background: #fbbf24; color: #3f2d05; border-color: #b45309; }
 .swatch-instance { background: #ecfdf5; color: #065f46; border-color: #10b981; }
+.swatch-pred     { background: #eef2ff; color: #3730a3; border-color: #6366f1; }
 .swatch-faminst  { background: #f5f3ff; color: #5b21b6; border-color: #a78bfa; font-style: italic; }
 .swatch-external { background: #e5e7eb; color: #374151; border-color: #9ca3af; border-style: dashed; }
 .swatch-ghost    { background: #dbeafe; color: #1e3a8a; border-color: #1d4ed8; border-style: dashed; opacity: 0.85; }
index e7825898061b45338fbe0ee9d0137bc8d001b563..ebdbbfb168cd2428f5ce3a620b480a67963f2a98 100644 (file)
@@ -36,6 +36,7 @@
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-class swatch-leaf">Cls</span></span><span><strong>Leaf</strong> class (no other class extends it; rendered at the top)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-family">Fam</span></span><span>Type family (open / closed / associated)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-instance">Inst</span></span><span>Class instance</span></div>
+          <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-pred">Foo a</span></span><span>Predicate — a constraint to be discharged (context or unmatched superclass)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-faminst">f a=b</span></span><span>Type family instance (<code>type instance F … = …</code>)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-external">Ext</span></span><span>External class (referenced but not defined here)</span></div>
           <div class="legend-row"><span class="swatch"><span class="swatch-node swatch-ghost">Cls</span></span><span>Ghost — one-hop neighbour in focus mode (click to add it to the focus)</span></div>
@@ -48,7 +49,7 @@
 
           <h4>Instance-view edges</h4>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge solid green"></span></span><span>Class defines this instance</span></div>
-          <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dotted purple"></span></span><span>Context constraint required by this instance</span></div>
+          <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dotted purple"></span></span><span>Instance context — points at a predicate node</span></div>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge solid amber"></span></span><span>Instance needs a superclass instance (matched locally)</span></div>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge dashed gray"></span></span><span>Needs a superclass instance (no local match)</span></div>
           <div class="legend-row"><span class="swatch swatch-edge-wrap"><span class="swatch-edge solid violet"></span></span><span>Family → one of its type family instances</span></div>
index 3379ef9685373e17a517978da1454050b71488c4..d0f0822a92cb3bc4ebd8494880292f5ec15c48be 100644 (file)
       return id;
     }
 
+    // A predicate node is a place-holder for a constraint that must be
+    // discharged *somewhere*: a context predicate ("ShelleyBasedEra
+    // era"), or — when no local instance was found — a superclass
+    // requirement. Two predicates with the same class + args structure
+    // collapse to one node, so `Foo a b` appearing in both context and
+    // superclass slots is a single node with two incoming edges.
+    //
+    // @role@ steers the styling: 'context' if the predicate appeared as
+    // a context constraint (the canonical, dark-bordered form);
+    // 'extern' for superclass requirements that *aren't* in the
+    // context (a "must exist somewhere outside this project" stub —
+    // gray). The first role observed sticks; subsequent calls with a
+    // different role are no-ops.
+    function ensurePredicateNode(classQn, args, role, boundTvs) {
+      const id = 'pred:' + qid(classQn) + ':' + JSON.stringify(args);
+      if (seenNodes.has(id)) return id;
+      seenNodes.add(id);
+      // Args render against the enclosing instance's tyvars. With
+      // multiple instances of the focused class, two different instances
+      // can structurally produce the same predicate (and therefore the
+      // same dedup id) but with different tyvar *names* — first call
+      // wins on the label.
+      const label = classQn.qnName +
+        (args && args.length > 0
+            ? ' ' + renderArgsCompact(args, boundTvs || [])
+            : '');
+      els.push({ group: 'nodes', data: {
+        id,
+        label,
+        kind: 'predicate',
+        role,
+        classQn,
+        classId: qid(classQn),
+        args,
+      }});
+      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
       inst.iiContext.forEach((pred, pi) => {
         if (pred.piIsEq) return;
         if (mutedSet.has(qid(pred.piClass))) return;
-        const cid = ensureClassNode(pred.piClass);
+        // Render the predicate (e.g. `ShelleyBasedEra era`) as its own
+        // node, not as a class node with a "ctx: era" arrow. The
+        // predicate-node label spells the constraint out in full and
+        // is shaped like an instance — which is what it morally is, a
+        // place-holder for whichever instance will satisfy this
+        // constraint at use time.
+        const pid = ensurePredicateNode(pred.piClass, pred.piArgs, 'context', inst.iiTyVars);
         els.push({ group: 'edges', data: {
           id: instId + '#ctx#' + pi,
           source: instId,
-          target: cid,
+          target: pid,
           kind: 'context',
-          label: 'ctx: ' + renderArgsCompact(pred.piArgs, inst.iiTyVars),
+          label: 'instance context',
         }});
         // Surface any type-family applications hiding inside the predicate
         // (e.g. `Eq (TxOut era)` — `Eq` itself is the class, but `TxOut`
       el.innerHTML = renderInstancePanel(data.instance);
     } else if (data.kind === 'fam-instance') {
       el.innerHTML = renderFamInstPanel(data.famInstance);
+    } else if (data.kind === 'predicate') {
+      el.innerHTML = renderPredicatePanel(data);
     }
   }
 
     return ' ' + args.map(a => renderArg(a, subTvs)).join(' ');
   }
 
+  // Side-panel content for a predicate node (a context constraint or
+  // an unmatched superclass requirement).
+  function renderPredicatePanel(d) {
+    const role = d.role === 'extern'
+      ? 'No instance was found in this program — it must be defined elsewhere.'
+      : 'Constraint required by this instance — must be discharged when the instance is used.';
+    const navClass = d.classId
+      ? `<a href="#" data-action="navigate-class" data-id="${escapeAttr(d.classId)}">` +
+        `${escape(d.classQn ? d.classQn.qnName : d.label)}</a>`
+      : escape(d.classQn ? d.classQn.qnName : d.label);
+    return `
+      <h2>${escape(d.label)}</h2>
+      <p class="pkgmod">${escape((d.classQn && d.classQn.qnPackage) || '')} · ${escape((d.classQn && d.classQn.qnModule) || '')}</p>
+      <dl>
+        <dt>Role</dt><dd>${escape(role)}</dd>
+        <dt>Class</dt><dd>${navClass}</dd>
+      </dl>`;
+  }
+
   function renderFamInstPanel(fi) {
     const head = escape(fi.fiFamily.qnName) + ' ' +
       escape(renderArgsCompact(fi.fiArgs, fi.fiTyVars));
           'font-family': 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
         },
       },
+      // Predicate node — a constraint that needs to be discharged.
+      // Same shape as an instance, but slate-toned: we know what the
+      // constraint is, we don't know which instance will satisfy it.
+      {
+        selector: 'node[kind = "predicate"]',
+        style: {
+          'background-color': '#eef2ff',
+          color: '#3730a3',
+          'border-color': '#6366f1',
+          'border-width': 1,
+          shape: 'round-rectangle',
+          'text-wrap': 'wrap',
+          'text-max-width': 280,
+          'text-justification': 'left',
+          'line-height': 1.3,
+          'font-family': 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
+        },
+      },
       // Orphan instances get a red dashed border, but only when the
       // global "Mark orphans" toggle is on. The toggle adds the
       // `.show-orphan` class to every orphan node in the graph; we only
           width: 1.6,
         },
       },
-      // Instance view: context (instance -> class it requires)
+      // Instance view: context (instance -> predicate node it requires)
       {
         selector: 'edge[kind = "context"]',
         style: {
           'target-arrow-color': '#6366f1',
           'line-style': 'dotted',
           width: 1.6,
+          'font-size': 10,
         },
       },
       // Instance view: needs (instance -> matched superclass instance)