import { edgesInto, findNode, type Graph, type GraphNode } from './Graph'; import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry'; export interface EmitRules { isDef: (def: NodeDef) => def is D; /** Names the node kind in "Not a … node". */ noun: string; /** A node's outputs from its inputs; called once per node, in dependency order. */ emit: (def: D, inputs: Record, node: GraphNode) => Record; /** An input without an edge: the typed-in value or what the node says it means unfed. */ unfed: (node: GraphNode, port: PortDef, def: D | undefined) => T | null; /** An edge's value on its way into `port`; null (after reporting a problem) when it cannot go in. */ conform?: (node: GraphNode, port: PortDef, value: T) => T | null; } export interface GraphEmitter { /** Per node id, the first thing that keeps the graph from compiling. */ readonly problems: Record; problem(node: GraphNode, text: string): void; /** What feeds an input, emitting whatever it depends on first. */ input(node: GraphNode, port: PortDef): T | null; /** Whether the walk reached the node. */ reached(node: GraphNode): boolean; /** Reports property names that are invalid (`invalid` returns the text) or taken twice. */ checkNames( nodes: GraphNode[], nameOf: (node: GraphNode) => string, invalid: (name: string) => string, ): void; /** The problem of the first node in graph order, or null when there is none. */ firstProblem(): string | null; } /** * The walk shared by the graph compilers: every node reachable from where `input` is first asked * is emitted once, before anything that reads it, and a node feeding itself is a problem. */ export function graphEmitter(graph: Graph, rules: EmitRules): GraphEmitter { const problems: Record = {}; const emitted = new Map>(); const visiting = new Set(); const problem = (node: GraphNode, text: string) => { if (!(node.id in problems)) problems[node.id] = text; }; const emitNode = (node: GraphNode): Record | null => { const done = emitted.get(node.id); if (done) return done; if (visiting.has(node.id)) { problem(node, 'Feeds itself'); return null; } const def = nodeDef(node.type); if (!def || !rules.isDef(def)) { problem(node, `Not a ${rules.noun} node`); return null; } visiting.add(node.id); const inputs: Record = {}; let complete = true; for (const port of def.inputs) { const value = input(node, port); if (value) inputs[port.key] = value; else complete = false; } visiting.delete(node.id); if (!complete) return null; const outputs = rules.emit(def, inputs, node); emitted.set(node.id, outputs); return outputs; }; const input = (node: GraphNode, port: PortDef): T | null => { const edge = edgesInto(graph, node.id, port.key)[0]; if (edge) { const source = findNode(graph, edge.from.node); const value = (source && emitNode(source))?.[edge.from.port]; if (!value) return null; return rules.conform ? rules.conform(node, port, value) : value; } const def = nodeDef(node.type); return rules.unfed(node, port, def && rules.isDef(def) ? def : undefined); }; return { problems, problem, input, reached: (node) => emitted.has(node.id), checkNames(nodes, nameOf, invalid) { const names = new Set(); for (const node of nodes) { const name = nameOf(node); const text = invalid(name); if (text) problem(node, text); else if (names.has(name)) problem(node, `Another property is named "${name}"`); names.add(name); } }, firstProblem() { const first = graph.nodes.find((n) => n.id in problems); return first ? problems[first.id]! : null; }, }; }