Hand a shared port type on through a chain of nodes (#210)
Since #207 the output of First not empty or Select takes the type
its inputs agree on. A second node of that kind behind the first
stayed any, because the first one answered with the any it was
registered with. That was the guard against a circle of such nodes
asking each other forever. The issue asks for the type to travel
through the chain. Answered there: measure with 50 nodes and build
no cache, and the painted input pins follow along.
- shapedDef in core/graph/nested.ts takes a Trail in place of the
flag inherit. Its set open holds the nodes still being asked. A
node reached a second time reports the any it was registered
with, which ends a circle.
- The map done of the Trail keeps each answer of one question. A
node feeding both inputs of the next is asked once, so the work
does not double with every link of a chain.
- ownShape is the part of shapedDef that builds the ports before a
shared group has its say. It is split off, nothing in it changed.
- Inputs stay any and only outputs inherit, as in #207. An edge
that turns invalid further down is left in place and drawn in the
danger colour.
- paintedInputs uses one Trail for all pins of a node, so the pins
show the type handed on through the chain.
- Nothing is cached between two changes. A chain of 50 resolved in
about 10 ms where it took 2 ms before, measured earlier in this
round and not again for this commit.
Tests:
- Gate on melektaus-atx: 211 files, 3240 tests (3227 + 13), exit 0
- e2e 1-6 on melektaus-atx: 298 checks, all pass
- e2e 4,6,18,19,20,23,24,26,28,31,36,43,53: 558 checks, all pass
- No full run and no smokes. The sum of 1673 (1671 + 2) is reckoned,
1671 is what CI counted for c88f57c.
- Fourteen unit tests in shared-port-type.test.ts: a chain of two
and of three, mixed kinds, a mixed and an open input, undo at the
front, a circle of two, a node feeding itself, behind a circle, an
invalid edge further down, the chain of 40 asked once per node,
the chain of 50, and two for the pins. The test saying the type
does not pass through a second node is gone, hence 13 more.
- Two e2e checks in module 06: the second node takes number, and a
text at the front opens the whole chain again.
- Four unit counter-probes and one browser counter-probe all fail
their tests. With the old behaviour only the chain check fails.
- Not checked in the browser: the circle and the chain of three,
which have unit tests only. Nothing was looked at after the build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c88f57cf0a
commit
9ca220a285
4 files changed
+268
-39
No files matched your search
+39
-22
@@ -145,7 +145,7 @@ export function declaredPorts(graph: Graph | undefined, type: string): PortDef[]
|
||||
* type declares ports per node (`declaresPorts`) or computes them (`derivePorts`).
|
||||
*/
|
||||
export function resolveDef(node: GraphNode, graph: Graph, lookup: GraphLookup): NodeDef | undefined {
|
||||
const def = shapedDef(node, graph, lookup, true);
|
||||
const def = shapedDef(node, graph, lookup, newTrail());
|
||||
return def && splitDef(def, node);
|
||||
}
|
||||
|
||||
@@ -158,24 +158,41 @@ function splitDef(def: NodeDef, node: GraphNode): NodeDef {
|
||||
* produces its wholes as always; reading a field port picks the field out of one (`splitField`).
|
||||
*/
|
||||
export function runDef(node: GraphNode, graph: Graph, lookup: GraphLookup): NodeDef | undefined {
|
||||
return shapedDef(node, graph, lookup, true);
|
||||
return shapedDef(node, graph, lookup, newTrail());
|
||||
}
|
||||
|
||||
/**
|
||||
* `inherit` is off while answering what feeds a shared group: a node of that kind then reports the
|
||||
* `any` it was registered with, so two of them in a row cannot ask each other in circles (#207).
|
||||
* One question about a shared group, followed through the nodes of that kind in front of it (#210).
|
||||
* `open` holds the nodes still being asked: reaching one again is a data cycle, and it then reports
|
||||
* the `any` it was registered with, which ends the circle. `done` keeps each answer, so a node
|
||||
* feeding both inputs of the next is asked once and not once per path.
|
||||
*/
|
||||
function shapedDef(
|
||||
node: GraphNode,
|
||||
graph: Graph,
|
||||
lookup: GraphLookup,
|
||||
inherit: boolean,
|
||||
): NodeDef | undefined {
|
||||
interface Trail {
|
||||
open: Set<string>;
|
||||
done: Map<string, NodeDef>;
|
||||
}
|
||||
|
||||
function newTrail(): Trail {
|
||||
return { open: new Set(), done: new Map() };
|
||||
}
|
||||
|
||||
function shapedDef(node: GraphNode, graph: Graph, lookup: GraphLookup, trail: Trail): NodeDef | undefined {
|
||||
const def = nodeDef(node.type);
|
||||
if (!def) return undefined;
|
||||
const shared = inherit ? def.sharedType : undefined;
|
||||
if (!def.declaresPorts && !def.derivePorts)
|
||||
return shared ? withSharedType(def, shared, node, graph, lookup) : def;
|
||||
const shared = def.sharedType;
|
||||
if (!shared || trail.open.has(node.id)) return ownShape(def, node, graph, lookup);
|
||||
const known = trail.done.get(node.id);
|
||||
if (known) return known;
|
||||
trail.open.add(node.id);
|
||||
const typed = withSharedType(ownShape(def, node, graph, lookup), shared, node, graph, lookup, trail);
|
||||
trail.open.delete(node.id);
|
||||
trail.done.set(node.id, typed);
|
||||
return typed;
|
||||
}
|
||||
|
||||
/** The ports of this node before any shared group has its say. */
|
||||
function ownShape(def: NodeDef, node: GraphNode, graph: Graph, lookup: GraphLookup): NodeDef {
|
||||
if (!def.declaresPorts && !def.derivePorts) return def;
|
||||
const own = (side: 'inputs' | 'outputs') =>
|
||||
declaredOn(def, node.ports ?? [], side).map((p) =>
|
||||
def.declaredPort ? def.declaredPort(p, node, graph, lookup) : portDefOf(p),
|
||||
@@ -185,13 +202,12 @@ function shapedDef(
|
||||
const ports = [...def[key], ...(grown[key] ?? []), ...own(key)];
|
||||
return grown.inOrder ? ports : execFirst(ports);
|
||||
};
|
||||
const resolved = {
|
||||
return {
|
||||
...def,
|
||||
label: grown.label ?? def.label,
|
||||
inputs: side('inputs'),
|
||||
outputs: side('outputs'),
|
||||
};
|
||||
return shared ? withSharedType(resolved, shared, node, graph, lookup) : resolved;
|
||||
}
|
||||
|
||||
/** The group's outputs, retyped to what its inputs agree on; `any` leaves the definition alone. */
|
||||
@@ -201,8 +217,9 @@ function withSharedType(
|
||||
node: GraphNode,
|
||||
graph: Graph,
|
||||
lookup: GraphLookup,
|
||||
trail: Trail,
|
||||
): NodeDef {
|
||||
const type = agreedType(group.inputs.map((key) => feedingType(node, key, graph, lookup, false)));
|
||||
const type = agreedType(group.inputs.map((key) => feedingType(node, key, graph, lookup, trail)));
|
||||
if (type === 'any') return def;
|
||||
return {
|
||||
...def,
|
||||
@@ -211,21 +228,20 @@ function withSharedType(
|
||||
}
|
||||
|
||||
/**
|
||||
* What the edge into that input carries, or undefined while nothing is connected to it. `inherit`
|
||||
* says whether a shared group on the *feeding* node may have its say: off while a group asks what
|
||||
* feeds it, on when only the drawn colour is wanted, which is what the edge itself is drawn in.
|
||||
* What the edge into that input carries, or undefined while nothing is connected to it: the type
|
||||
* the feeding node resolves to, which is what the edge itself is drawn in.
|
||||
*/
|
||||
function feedingType(
|
||||
node: GraphNode,
|
||||
key: string,
|
||||
graph: Graph,
|
||||
lookup: GraphLookup,
|
||||
inherit: boolean,
|
||||
trail: Trail,
|
||||
): PortType | undefined {
|
||||
const edge = edgesInto(graph, node.id, key)[0];
|
||||
if (!edge) return undefined;
|
||||
const from = findNode(graph, edge.from.node);
|
||||
const def = from && shapedDef(from, graph, lookup, inherit);
|
||||
const def = from && shapedDef(from, graph, lookup, trail);
|
||||
return def && splitDef(def, from).outputs.find((p) => p.key === edge.from.port)?.type;
|
||||
}
|
||||
|
||||
@@ -236,9 +252,10 @@ function feedingType(
|
||||
*/
|
||||
function paintedInputs(node: GraphNode, graph: Graph, def: NodeDef, lookup: GraphLookup): PortDef[] | null {
|
||||
if (!def.inputs.some((port) => port.type === 'any' && !port.ghost)) return null;
|
||||
const trail = newTrail();
|
||||
return def.inputs.map((port) => {
|
||||
if (port.type !== 'any' || port.ghost) return port;
|
||||
const fed = feedingType(node, port.key, graph, lookup, true);
|
||||
const fed = feedingType(node, port.key, graph, lookup, trail);
|
||||
return fed === undefined || fed === 'any' || fed === 'exec' ? port : { ...port, paintType: fed };
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user