Skip to content

Commit f1aefca

Browse files
Copilotaarne
andauthored
feat(bridge-core): add enumerateTraversalIds for bridge traversal path enumeration (#117)
* Initial plan * feat(bridge-core): add enumerateTraversalIds function Enumerates all possible traversal paths through a Bridge. Each entry represents a unique code path determined by the wire structure (fallback chains, catch gates, array scopes, ternary branches). Useful for complexity assessment and future integration into the execution engine for monitoring. Co-authored-by: aarne <82001+aarne@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aarne <82001+aarne@users.noreply.github.com>
1 parent e62f5b9 commit f1aefca

File tree

3 files changed

+529
-0
lines changed

3 files changed

+529
-0
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
/**
2+
* Enumerate all possible traversal paths through a Bridge.
3+
*
4+
* Every bridge has a finite set of execution paths ("traversals"),
5+
* determined by the wire structure alone — independent of runtime values.
6+
*
7+
* Examples:
8+
* `o <- i.a || i.b catch i.c` → 3 traversals (primary, fallback, catch)
9+
* `o <- i.arr[] as a { .data <- a.a ?? a.b }` → 3 traversals
10+
* (empty-array, primary for .data, nullish fallback for .data)
11+
*
12+
* Used for complexity assessment and will integrate into the execution
13+
* engine for monitoring.
14+
*/
15+
16+
import type { Bridge, Wire, WireFallback } from "./types.ts";
17+
18+
// ── Public types ────────────────────────────────────────────────────────────
19+
20+
/**
21+
* A single traversal path through a bridge wire.
22+
*/
23+
export interface TraversalEntry {
24+
/** Stable identifier for this traversal path. */
25+
id: string;
26+
/** Index of the originating wire in `bridge.wires` (-1 for synthetic entries like empty-array). */
27+
wireIndex: number;
28+
/** Target path segments from the wire's `to` NodeRef. */
29+
target: string[];
30+
/** Classification of this traversal path. */
31+
kind:
32+
| "primary"
33+
| "fallback"
34+
| "catch"
35+
| "empty-array"
36+
| "then"
37+
| "else"
38+
| "const";
39+
/** Fallback chain index (only when kind is `"fallback"`). */
40+
fallbackIndex?: number;
41+
/** Gate type (only when kind is `"fallback"`): `"falsy"` for `||`, `"nullish"` for `??`. */
42+
gateType?: "falsy" | "nullish";
43+
}
44+
45+
// ── Helpers ─────────────────────────────────────────────────────────────────
46+
47+
function pathKey(path: string[]): string {
48+
return path.length > 0 ? path.join(".") : "*";
49+
}
50+
51+
function hasCatch(w: Wire): boolean {
52+
if ("value" in w) return false;
53+
return (
54+
w.catchFallback != null ||
55+
w.catchFallbackRef != null ||
56+
w.catchControl != null
57+
);
58+
}
59+
60+
/**
61+
* True when the wire is an array-source wire that simply feeds an array
62+
* iteration scope without any fallback/catch choices of its own.
63+
*
64+
* Such wires always execute (to fetch the array), so they are not a
65+
* traversal "choice". The separate `empty-array` entry already covers
66+
* the "no elements" outcome.
67+
*/
68+
function isPlainArraySourceWire(
69+
w: Wire,
70+
arrayIterators: Record<string, string> | undefined,
71+
): boolean {
72+
if (!arrayIterators) return false;
73+
if (!("from" in w)) return false;
74+
if (w.from.element) return false;
75+
const targetPath = w.to.path.join(".");
76+
if (!(targetPath in arrayIterators)) return false;
77+
return !w.fallbacks?.length && !hasCatch(w);
78+
}
79+
80+
function addFallbackEntries(
81+
entries: TraversalEntry[],
82+
base: string,
83+
wireIndex: number,
84+
target: string[],
85+
fallbacks: WireFallback[] | undefined,
86+
): void {
87+
if (!fallbacks) return;
88+
for (let i = 0; i < fallbacks.length; i++) {
89+
entries.push({
90+
id: `${base}/fallback:${i}`,
91+
wireIndex,
92+
target,
93+
kind: "fallback",
94+
fallbackIndex: i,
95+
gateType: fallbacks[i].type,
96+
});
97+
}
98+
}
99+
100+
function addCatchEntry(
101+
entries: TraversalEntry[],
102+
base: string,
103+
wireIndex: number,
104+
target: string[],
105+
w: Wire,
106+
): void {
107+
if (hasCatch(w)) {
108+
entries.push({ id: `${base}/catch`, wireIndex, target, kind: "catch" });
109+
}
110+
}
111+
112+
// ── Main function ───────────────────────────────────────────────────────────
113+
114+
/**
115+
* Enumerate every possible traversal path through a bridge.
116+
*
117+
* Returns a flat list of {@link TraversalEntry} objects, one per
118+
* unique code-path through the bridge's wires. The total length
119+
* of the returned array is a useful proxy for bridge complexity.
120+
*/
121+
export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] {
122+
const entries: TraversalEntry[] = [];
123+
124+
// Track per-target occurrence counts for disambiguation when
125+
// multiple wires write to the same target (overdefinition).
126+
const targetCounts = new Map<string, number>();
127+
128+
for (let i = 0; i < bridge.wires.length; i++) {
129+
const w = bridge.wires[i];
130+
const target = w.to.path;
131+
const tKey = pathKey(target);
132+
133+
// Disambiguate overdefined targets (same target written by >1 wire).
134+
const seen = targetCounts.get(tKey) ?? 0;
135+
targetCounts.set(tKey, seen + 1);
136+
const base = seen > 0 ? `${tKey}#${seen}` : tKey;
137+
138+
// ── Constant wire ───────────────────────────────────────────────
139+
if ("value" in w) {
140+
entries.push({ id: `${base}/const`, wireIndex: i, target, kind: "const" });
141+
continue;
142+
}
143+
144+
// ── Pull wire ───────────────────────────────────────────────────
145+
if ("from" in w) {
146+
// Skip plain array source wires — they always execute and the
147+
// separate "empty-array" entry covers the "no elements" path.
148+
if (!isPlainArraySourceWire(w, bridge.arrayIterators)) {
149+
entries.push({
150+
id: `${base}/primary`,
151+
wireIndex: i,
152+
target,
153+
kind: "primary",
154+
});
155+
addFallbackEntries(entries, base, i, target, w.fallbacks);
156+
addCatchEntry(entries, base, i, target, w);
157+
}
158+
continue;
159+
}
160+
161+
// ── Conditional (ternary) wire ──────────────────────────────────
162+
if ("cond" in w) {
163+
entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then" });
164+
entries.push({ id: `${base}/else`, wireIndex: i, target, kind: "else" });
165+
addFallbackEntries(entries, base, i, target, w.fallbacks);
166+
addCatchEntry(entries, base, i, target, w);
167+
continue;
168+
}
169+
170+
// ── condAnd / condOr (logical binary) ───────────────────────────
171+
entries.push({
172+
id: `${base}/primary`,
173+
wireIndex: i,
174+
target,
175+
kind: "primary",
176+
});
177+
if ("condAnd" in w) {
178+
addFallbackEntries(entries, base, i, target, w.fallbacks);
179+
addCatchEntry(entries, base, i, target, w);
180+
} else {
181+
// condOr
182+
const wo = w as Extract<Wire, { condOr: unknown }>;
183+
addFallbackEntries(entries, base, i, target, wo.fallbacks);
184+
addCatchEntry(entries, base, i, target, w);
185+
}
186+
}
187+
188+
// ── Array iterators — each scope adds an "empty-array" path ─────
189+
if (bridge.arrayIterators) {
190+
for (const key of Object.keys(bridge.arrayIterators)) {
191+
const id = key ? `${key}/empty-array` : "*/empty-array";
192+
entries.push({
193+
id,
194+
wireIndex: -1,
195+
target: key ? key.split(".") : [],
196+
kind: "empty-array",
197+
});
198+
}
199+
}
200+
201+
return entries;
202+
}

packages/bridge-core/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ export type {
8282
WireFallback,
8383
} from "./types.ts";
8484

85+
// ── Traversal enumeration ───────────────────────────────────────────────────
86+
87+
export { enumerateTraversalIds } from "./enumerate-traversals.ts";
88+
export type { TraversalEntry } from "./enumerate-traversals.ts";
89+
8590
// ── Utilities ───────────────────────────────────────────────────────────────
8691

8792
export { parsePath } from "./utils.ts";

0 commit comments

Comments
 (0)