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