<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>
<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>
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)