Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/fifty-cases-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@stackables/bridge-compiler": minor
"@stackables/bridge-parser": minor
"@stackables/bridge-core": minor
---

Multi-Level Control Flow (break N, continue N)

When working with deeply nested arrays (e.g., mapping categories that contain lists of products), you may want an error deep inside the inner array to skip the outer array element.

You can append a number to break or continue to specify how many loop levels the signal should pierce.
5 changes: 5 additions & 0 deletions .changeset/many-beds-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stackables/bridge-graphql": minor
---

Support optional lookahead resolver with compiler
165 changes: 122 additions & 43 deletions packages/bridge-compiler/src/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,19 +139,41 @@ function hasCatchFallback(w: Wire): boolean {
);
}

type DetectedControlFlow = {
kind: "break" | "continue" | "throw" | "panic";
levels: number;
};

/** Check if any wire in a set has a control flow instruction (break/continue/throw/panic). */
function detectControlFlow(
wires: Wire[],
): "break" | "continue" | "throw" | "panic" | null {
function detectControlFlow(wires: Wire[]): DetectedControlFlow | null {
for (const w of wires) {
if ("fallbacks" in w && w.fallbacks) {
for (const fb of w.fallbacks) {
if (fb.control)
return fb.control.kind as "break" | "continue" | "throw" | "panic";
if (fb.control) {
const kind = fb.control.kind as
| "break"
| "continue"
| "throw"
| "panic";
const levels =
kind === "break" || kind === "continue"
? Math.max(1, Number((fb.control as any).levels) || 1)
: 1;
return { kind, levels };
}
}
}
if ("catchControl" in w && w.catchControl) {
return w.catchControl.kind as "break" | "continue" | "throw" | "panic";
const kind = w.catchControl.kind as
| "break"
| "continue"
| "throw"
| "panic";
const levels =
kind === "break" || kind === "continue"
? Math.max(1, Number((w.catchControl as any).levels) || 1)
: 1;
return { kind, levels };
}
}
return null;
Expand Down Expand Up @@ -648,6 +670,12 @@ class CodegenContext {
` const __ctx = { logger: __opts?.logger ?? {}, signal: __signal };`,
);
lines.push(` const __trace = __opts?.__trace;`);
lines.push(
` const __isLoopCtrl = (v) => (v?.__bridgeControl === "break" || v?.__bridgeControl === "continue") && Number.isInteger(v?.levels) && v.levels > 0;`,
);
lines.push(
` const __nextLoopCtrl = (v) => ({ __bridgeControl: v.__bridgeControl, levels: v.levels - 1 });`,
);
lines.push(` async function __call(fn, input, toolName) {`);
lines.push(` if (__signal?.aborted) throw new __BridgeAbortError();`);
lines.push(` const start = __trace ? performance.now() : 0;`);
Expand Down Expand Up @@ -1386,6 +1414,8 @@ class CodegenContext {
// Only check control flow on direct element wires, not sub-array element wires
const directElemWires = elemWires.filter((w) => w.to.path.length === 1);
const cf = detectControlFlow(directElemWires);
const anyCf = detectControlFlow(elemWires);
const requiresLabeledLoop = !cf && !!anyCf && anyCf.levels > 1;
// Check if any element wire generates `await` (element-scoped tools or catch fallbacks)
const needsAsync = elemWires.some((w) => this.wireNeedsAwait(w));

Expand All @@ -1401,20 +1431,27 @@ class CodegenContext {
arrayIterators,
0,
4,
cf === "continue" ? "for-continue" : "break",
cf.kind === "continue" ? "for-continue" : "break",
)
: ` _result.push(${this.buildElementBody(elemWires, arrayIterators, 0, 4)});`;

lines.push(` const _result = [];`);
lines.push(` for (const _el0 of (${arrayExpr} ?? [])) {`);
lines.push(` __loop0: for (const _el0 of (${arrayExpr} ?? [])) {`);
lines.push(` try {`);
for (const pl of preambleLines) {
lines.push(` ${pl}`);
lines.push(` ${pl}`);
}
lines.push(body);
lines.push(` ${body.trimStart()}`);
lines.push(` } catch (_ctrl) {`);
lines.push(
` if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; }`,
);
lines.push(` throw _ctrl;`);
lines.push(` }`);
lines.push(` }`);
lines.push(` return _result;`);
this.elementLocalVars.clear();
} else if (cf === "continue") {
} else if (cf?.kind === "continue" && cf.levels === 1) {
// Use flatMap — skip elements that trigger continue (sync only)
const body = this.buildElementBodyWithControlFlow(
elemWires,
Expand All @@ -1426,18 +1463,35 @@ class CodegenContext {
lines.push(` return (${arrayExpr} ?? []).flatMap((_el0) => {`);
lines.push(body);
lines.push(` });`);
} else if (cf === "break") {
} else if (
cf?.kind === "break" ||
cf?.kind === "continue" ||
requiresLabeledLoop
) {
// Use an explicit loop for:
// - direct break/continue control
// - nested multilevel control (e.g. break 2 / continue 2) that must
// escape from sub-array IIFEs through throw/catch propagation.
// Use a loop with early break (sync)
const body = this.buildElementBodyWithControlFlow(
elemWires,
arrayIterators,
0,
4,
"break",
);
const body = cf
? this.buildElementBodyWithControlFlow(
elemWires,
arrayIterators,
0,
4,
cf.kind === "continue" ? "for-continue" : "break",
)
: ` _result.push(${this.buildElementBody(elemWires, arrayIterators, 0, 4)});`;
lines.push(` const _result = [];`);
lines.push(` for (const _el0 of (${arrayExpr} ?? [])) {`);
lines.push(body);
lines.push(` __loop0: for (const _el0 of (${arrayExpr} ?? [])) {`);
lines.push(` try {`);
lines.push(` ${body.trimStart()}`);
lines.push(` } catch (_ctrl) {`);
lines.push(
` if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; }`,
);
lines.push(` throw _ctrl;`);
lines.push(` }`);
lines.push(` }`);
lines.push(` return _result;`);
} else {
Expand Down Expand Up @@ -1560,6 +1614,8 @@ class CodegenContext {
// Only check control flow on direct element wires (not sub-array element wires)
const directShifted = shifted.filter((w) => w.to.path.length === 1);
const cf = detectControlFlow(directShifted);
const anyCf = detectControlFlow(shifted);
const requiresLabeledLoop = !cf && !!anyCf && anyCf.levels > 1;
// Check if any element wire generates `await` (element-scoped tools or catch fallbacks)
const needsAsync = shifted.some((w) => this.wireNeedsAwait(w));
let mapExpr: string;
Expand All @@ -1575,14 +1631,14 @@ class CodegenContext {
arrayIterators,
0,
8,
cf === "continue" ? "for-continue" : "break",
cf.kind === "continue" ? "for-continue" : "break",
)
: ` _result.push(${this.buildElementBody(shifted, arrayIterators, 0, 8)});`;

const preamble = preambleLines.map((l) => ` ${l}`).join("\n");
mapExpr = `await (async () => { const _src = ${arrayExpr}; if (_src == null) return null; const _result = []; for (const _el0 of _src) {\n${preamble}\n${asyncBody}\n } return _result; })()`;
mapExpr = `await (async () => { const _src = ${arrayExpr}; if (_src == null) return null; const _result = []; __loop0: for (const _el0 of _src) {\n try {\n${preamble}\n${asyncBody}\n } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n } return _result; })()`;
this.elementLocalVars.clear();
} else if (cf === "continue") {
} else if (cf?.kind === "continue" && cf.levels === 1) {
const cfBody = this.buildElementBodyWithControlFlow(
shifted,
arrayIterators,
Expand All @@ -1591,15 +1647,23 @@ class CodegenContext {
"continue",
);
mapExpr = `((__s) => Array.isArray(__s) ? __s.flatMap((_el0) => {\n${cfBody}\n }) ?? null : null)(${arrayExpr})`;
} else if (cf === "break") {
const cfBody = this.buildElementBodyWithControlFlow(
shifted,
arrayIterators,
0,
8,
"break",
);
mapExpr = `(() => { const _src = ${arrayExpr}; if (!Array.isArray(_src)) return null; const _result = []; for (const _el0 of _src) {\n${cfBody}\n } return _result; })()`;
} else if (
cf?.kind === "break" ||
cf?.kind === "continue" ||
requiresLabeledLoop
) {
// Same rationale as root array handling above: nested multilevel
// control requires for-loop + throw/catch propagation instead of map.
const loopBody = cf
? this.buildElementBodyWithControlFlow(
shifted,
arrayIterators,
0,
8,
cf.kind === "continue" ? "for-continue" : "break",
)
: ` _result.push(${this.buildElementBody(shifted, arrayIterators, 0, 8)});`;
mapExpr = `(() => { const _src = ${arrayExpr}; if (!Array.isArray(_src)) return null; const _result = []; __loop0: for (const _el0 of _src) {\n try {\n${loopBody}\n } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n } return _result; })()`;
} else {
const body = this.buildElementBody(shifted, arrayIterators, 0, 6);
mapExpr = `((__s) => Array.isArray(__s) ? __s.map((_el0) => (${body})) ?? null : null)(${arrayExpr})`;
Expand Down Expand Up @@ -1752,11 +1816,11 @@ class CodegenContext {
arrayIterators,
depth + 1,
indent + 4,
innerCf === "continue" ? "for-continue" : "break",
innerCf.kind === "continue" ? "for-continue" : "break",
)
: `${" ".repeat(indent + 4)}_result.push(${this.buildElementBody(shifted, arrayIterators, depth + 1, indent + 4)});`;
mapExpr = `await (async () => { const _src = ${srcExpr}; if (!Array.isArray(_src)) return null; const _result = []; for (const ${innerElVar} of _src) {\n${innerBody}\n${" ".repeat(indent + 2)}} return _result; })()`;
} else if (innerCf === "continue") {
mapExpr = `await (async () => { const _src = ${srcExpr}; if (!Array.isArray(_src)) return null; const _result = []; __loop${depth + 1}: for (const ${innerElVar} of _src) {\n${" ".repeat(indent + 4)}try {\n${innerBody}\n${" ".repeat(indent + 4)}} catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n${" ".repeat(indent + 2)}} return _result; })()`;
} else if (innerCf?.kind === "continue" && innerCf.levels === 1) {
const cfBody = this.buildElementBodyWithControlFlow(
shifted,
arrayIterators,
Expand All @@ -1765,15 +1829,15 @@ class CodegenContext {
"continue",
);
mapExpr = `((__s) => Array.isArray(__s) ? __s.flatMap((${innerElVar}) => {\n${cfBody}\n${" ".repeat(indent + 2)}}) ?? null : null)(${srcExpr})`;
} else if (innerCf === "break") {
} else if (innerCf?.kind === "break" || innerCf?.kind === "continue") {
const cfBody = this.buildElementBodyWithControlFlow(
shifted,
arrayIterators,
depth + 1,
indent + 4,
"break",
innerCf.kind === "continue" ? "for-continue" : "break",
);
mapExpr = `(() => { const _src = ${srcExpr}; if (!Array.isArray(_src)) return null; const _result = []; for (const ${innerElVar} of _src) {\n${cfBody}\n${" ".repeat(indent + 2)}} return _result; })()`;
mapExpr = `(() => { const _src = ${srcExpr}; if (!Array.isArray(_src)) return null; const _result = []; __loop${depth + 1}: for (const ${innerElVar} of _src) {\n${" ".repeat(indent + 4)}try {\n${cfBody}\n${" ".repeat(indent + 4)}} catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n${" ".repeat(indent + 2)}} return _result; })()`;
} else {
const innerBody = this.buildElementBody(
shifted,
Expand Down Expand Up @@ -1840,6 +1904,21 @@ class CodegenContext {
controlWire.fallbacks?.some(
(fb) => fb.type === "nullish" && fb.control != null,
) ?? false;
const ctrlFromFallback = controlWire.fallbacks?.find(
(fb) => fb.control != null,
)?.control;
const ctrl = ctrlFromFallback ?? controlWire.catchControl;
const controlKind = ctrl?.kind === "continue" ? "continue" : "break";
const controlLevels =
ctrl && (ctrl.kind === "continue" || ctrl.kind === "break")
? Math.max(1, Number(ctrl.levels) || 1)
: 1;
const controlStatement =
controlLevels > 1
? `throw { __bridgeControl: ${JSON.stringify(controlKind)}, levels: ${controlLevels} };`
: controlKind === "continue"
? "continue;"
: "break;";

if (mode === "continue") {
if (isNullish) {
Expand All @@ -1852,16 +1931,16 @@ class CodegenContext {
// mode === "for-continue" — same as break but uses native 'continue' keyword
if (mode === "for-continue") {
if (isNullish) {
return `${pad} if (${checkExpr} == null) continue;\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
return `${pad} if (${checkExpr} == null) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
}
return `${pad} if (!${checkExpr}) continue;\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
return `${pad} if (!${checkExpr}) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
}

// mode === "break"
if (isNullish) {
return `${pad} if (${checkExpr} == null) break;\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
return `${pad} if (${checkExpr} == null) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
}
return `${pad} if (!${checkExpr}) break;\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
return `${pad} if (!${checkExpr}) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`;
}

// ── Wire → expression ────────────────────────────────────────────────────
Expand Down
18 changes: 12 additions & 6 deletions packages/bridge-core/src/ExecutionTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "./tracing.ts";
import type {
Logger,
LoopControlSignal,
MaybePromise,
Path,
TreeContext,
Expand All @@ -27,6 +28,8 @@ import {
BridgeAbortError,
BridgePanicError,
CONTINUE_SYM,
decrementLoopControl,
isLoopControlSignal,
isPromise,
MAX_EXECUTION_DEPTH,
} from "./tree-types.ts";
Expand Down Expand Up @@ -371,8 +374,11 @@ export class ExecutionTree implements TreeContext {
if (this.signal?.aborted) {
throw new BridgeAbortError();
}
if (item === BREAK_SYM) break;
if (item === CONTINUE_SYM) continue;
if (isLoopControlSignal(item)) {
const ctrl = decrementLoopControl(item);
if (ctrl === BREAK_SYM) break;
if (ctrl === CONTINUE_SYM) continue;
}
const s = this.shadow();
s.state[this.elementTrunkKey] = item;
shadows.push(s);
Expand Down Expand Up @@ -579,7 +585,7 @@ export class ExecutionTree implements TreeContext {
const result = this.resolveWires(matches);
if (!array) return result;
const resolved = await result;
if (resolved === BREAK_SYM || resolved === CONTINUE_SYM) return [];
if (isLoopControlSignal(resolved)) return [];
return this.createShadowArray(resolved as any[]);
}

Expand Down Expand Up @@ -936,7 +942,7 @@ export class ExecutionTree implements TreeContext {
private materializeShadows(
items: ExecutionTree[],
pathPrefix: string[],
): Promise<unknown[]> {
): Promise<unknown[] | LoopControlSignal> {
return _materializeShadows(this, items, pathPrefix);
}

Expand Down Expand Up @@ -1004,7 +1010,7 @@ export class ExecutionTree implements TreeContext {

// Array: create shadow trees for per-element resolution
const resolved = await response;
if (resolved === BREAK_SYM || resolved === CONTINUE_SYM) return [];
if (isLoopControlSignal(resolved)) return [];
return this.createShadowArray(resolved as any[]);
}

Expand All @@ -1018,7 +1024,7 @@ export class ExecutionTree implements TreeContext {
const response = this.resolveWires(defineFieldWires);
if (!array) return response;
const resolved = await response;
if (resolved === BREAK_SYM || resolved === CONTINUE_SYM) return [];
if (isLoopControlSignal(resolved)) return [];
return this.createShadowArray(resolved as any[]);
}
}
Expand Down
Loading