Loop over a list and stop at the first match with For each (#249)
Finding the first item that matches took Count, Repeat, Item at and a Branch, and the loop could not stop once it had found something. A graph has no lambdas, so the predicate is an input that is read every round. - For each runs body once per element, with item and index set, and goes on at done. - Its break input is read anew before every round, after item is set. Once it is true the loop ends there and that round's body does not run. - done sees the matching item and index with stopped true. Without a match it sees no item, index -1 and stopped false, the OrDefault. - RepeatResult has optional items, stopWhen, stopped and exhausted. GraphInterpreter.loop reads them, Repeat runs as before. Tests: - ./check.sh: 180 files, 2623 tests, CHECK EXIT 0 - e2e GM_STEPS=1-6: ALL PASSED, 269 checks - 5 interpreter tests in graph.test.ts, 2 catalog cases - e2e 6z-iv: the palette creates the node, and a search for Name == "Gold" in Axe, Gold, Bow, Gold logs "1true" - Counter-checks: without the break check 2 unit tests fail, and with break not read over its edge 6z-iv fails - This commit alone: tsc and the three touched unit files pass (349 tests) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a5ae21481b
commit
cb8a2d029a
8 files changed
+253
-3
No files matched your search
@@ -13,7 +13,7 @@ import {
|
||||
type RepeatResult,
|
||||
} from './NodeRegistry';
|
||||
import { machineIdOf } from './stateMachine';
|
||||
import { isObjectPlace, type Value } from './values';
|
||||
import { asBoolean, isObjectPlace, type Value } from './values';
|
||||
|
||||
/** How much work one pulse may do. A Repeat over a 10×20 board is 200 rounds, so this is generous. */
|
||||
const MAX_STEPS_PER_FIRE = 100_000;
|
||||
@@ -217,20 +217,34 @@ export class GraphInterpreter {
|
||||
/**
|
||||
* One round per count, the loop node's `index` output carrying the round number. Data nodes are
|
||||
* cached for the whole pulse, so the round has to drop that cache first – otherwise every round
|
||||
* would read the values the first one computed.
|
||||
* would read the values the first one computed. The stop input is read the same way, after the
|
||||
* round's outputs are set, so a condition built on `item` asks about the element at hand.
|
||||
*/
|
||||
private loop(node: GraphNode, repeat: RepeatResult, ctx: NodeContext, depth: number): void {
|
||||
const outputs = this.frame.outputCache.get(node.id) ?? {};
|
||||
const stopPort = repeat.stopWhen
|
||||
? this.def(node)?.inputs.find((p) => p.key === repeat.stopWhen)
|
||||
: undefined;
|
||||
let stopped = false;
|
||||
for (let round = 0; round < repeat.times; round++) {
|
||||
if (++this.steps > MAX_STEPS_PER_FIRE)
|
||||
throw new Error('Behavior graph exceeded the step limit (endless loop?)');
|
||||
this.frame.dataCache.clear();
|
||||
const roundOutputs = { ...outputs, [repeat.index]: round };
|
||||
if (repeat.items) roundOutputs[repeat.items.port] = repeat.items.values[round] ?? null;
|
||||
this.frame.outputCache.set(node.id, roundOutputs);
|
||||
this.record(node.id, roundOutputs);
|
||||
if (stopPort && asBoolean(this.inputValue(node, stopPort, ctx))) {
|
||||
stopped = true;
|
||||
break;
|
||||
}
|
||||
this.follow(node, repeat.port, ctx, depth);
|
||||
}
|
||||
this.frame.dataCache.clear();
|
||||
if (!repeat.stopped) return;
|
||||
const last = this.frame.outputCache.get(node.id) ?? outputs;
|
||||
const after = stopped ? last : { ...last, ...repeat.exhausted };
|
||||
this.frame.outputCache.set(node.id, { ...after, [repeat.stopped]: stopped });
|
||||
}
|
||||
|
||||
/** Resolves every data input: connected → pulled from the source, else the inline param. */
|
||||
|
||||
Reference in new issue
Block a user