From: Javier Sagredo Date: Wed, 6 May 2026 22:31:10 +0000 (+0200) Subject: Render instance context constraints as predicate nodes X-Git-Url: https://git.sagredo.dev/?a=commitdiff_plain;h=1dcdf4cc8b1f806b5d0c7209dbec863c3e13137a;p=classgraph.git Render instance context constraints as predicate nodes Was: instance → class node, edge labelled "ctx: ". 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) --- diff --git a/data/viewer.css b/data/viewer.css index 376c401..69bb852 100644 --- a/data/viewer.css +++ b/data/viewer.css @@ -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; } diff --git a/data/viewer.html b/data/viewer.html index e782589..ebdbbfb 100644 --- a/data/viewer.html +++ b/data/viewer.html @@ -36,6 +36,7 @@
ClsLeaf class (no other class extends it; rendered at the top)
FamType family (open / closed / associated)
InstClass instance
+
Foo aPredicate — a constraint to be discharged (context or unmatched superclass)
f a=bType family instance (type instance F … = …)
ExtExternal class (referenced but not defined here)
ClsGhost — one-hop neighbour in focus mode (click to add it to the focus)
@@ -48,7 +49,7 @@

Instance-view edges

Class defines this instance
-
Context constraint required by this instance
+
Instance context — points at a predicate node
Instance needs a superclass instance (matched locally)
Needs a superclass instance (no local match)
Family → one of its type family instances
diff --git a/data/viewer.js b/data/viewer.js index 3379ef9..d0f0822 100644 --- a/data/viewer.js +++ b/data/viewer.js @@ -607,6 +607,44 @@ 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 @@ -703,13 +741,19 @@ 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` @@ -1307,6 +1351,8 @@ 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); } } @@ -1550,6 +1596,25 @@ 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 + ? `` + + `${escape(d.classQn ? d.classQn.qnName : d.label)}` + : escape(d.classQn ? d.classQn.qnName : d.label); + return ` +

${escape(d.label)}

+

${escape((d.classQn && d.classQn.qnPackage) || '')} · ${escape((d.classQn && d.classQn.qnModule) || '')}

+
+
Role
${escape(role)}
+
Class
${navClass}
+
`; + } + function renderFamInstPanel(fi) { const head = escape(fi.fiFamily.qnName) + ' ' + escape(renderArgsCompact(fi.fiArgs, fi.fiTyVars)); @@ -1678,6 +1743,24 @@ '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 @@ -1750,7 +1833,7 @@ width: 1.6, }, }, - // Instance view: context (instance -> class it requires) + // Instance view: context (instance -> predicate node it requires) { selector: 'edge[kind = "context"]', style: { @@ -1758,6 +1841,7 @@ 'target-arrow-color': '#6366f1', 'line-style': 'dotted', width: 1.6, + 'font-size': 10, }, }, // Instance view: needs (instance -> matched superclass instance)