Two compilers that walk a graph the same way walk it through one emitter (#85)
Refactoring round two: the particle and shader compilers share emitGraph.ts (cycle detection, duplicate names, emit order) and keep only their rules. FontLibrary and TextureLibrary sit on FileLibrary (base URL, loading/failed bookkeeping, listeners); SoundLibrary keeps its own contract. The three Create*Command share ImportFileCommand, the three renames share RenameCommand - class names stay. bandBounds() in framing.ts and portNameField() in portSection.ts replace in-file clones, and three comments that told history now state the rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf0be66c9d
commit
d3563897d8
16 files changed
+376
-379
No files matched your search
@@ -19,8 +19,8 @@ const MAX_STEPS_PER_FIRE = 100_000;
|
||||
|
||||
/**
|
||||
* How far exec edges may nest. The interpreter recurses along them, so this is what keeps a flow
|
||||
* feeding back into itself off the call stack – a job the step budget can no longer do now that a
|
||||
* legitimate pulse may run tens of thousands of steps.
|
||||
* feeding back into itself off the call stack – the step budget cannot, since a legitimate pulse
|
||||
* may run tens of thousands of steps.
|
||||
*/
|
||||
const MAX_DEPTH = 1000;
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { edgesInto, findNode, type Graph, type GraphNode } from './Graph';
|
||||
import { nodeDef, type NodeDef, type PortDef } from './NodeRegistry';
|
||||
|
||||
export interface EmitRules<D extends NodeDef, T> {
|
||||
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<string, T>, node: GraphNode) => Record<string, T>;
|
||||
/** 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<T> {
|
||||
/** Per node id, the first thing that keeps the graph from compiling. */
|
||||
readonly problems: Record<string, string>;
|
||||
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<D extends NodeDef, T>(graph: Graph, rules: EmitRules<D, T>): GraphEmitter<T> {
|
||||
const problems: Record<string, string> = {};
|
||||
const emitted = new Map<string, Record<string, T>>();
|
||||
const visiting = new Set<string>();
|
||||
|
||||
const problem = (node: GraphNode, text: string) => {
|
||||
if (!(node.id in problems)) problems[node.id] = text;
|
||||
};
|
||||
|
||||
const emitNode = (node: GraphNode): Record<string, T> | 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<string, T> = {};
|
||||
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<string>();
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user