diff --git a/.changeset/funky-jars-accept.md b/.changeset/funky-jars-accept.md new file mode 100644 index 00000000..ff39a867 --- /dev/null +++ b/.changeset/funky-jars-accept.md @@ -0,0 +1,9 @@ +--- +"@stackables/bridge-compiler": minor +"@stackables/bridge-graphql": minor +"@stackables/bridge-parser": minor +"@stackables/bridge-core": minor +"@stackables/bridge": minor +--- + +New internal wire structure with recursive expressions diff --git a/packages/bridge-compiler/src/bridge-asserts.ts b/packages/bridge-compiler/src/bridge-asserts.ts index 4cc916ea..acff2aa5 100644 --- a/packages/bridge-compiler/src/bridge-asserts.ts +++ b/packages/bridge-compiler/src/bridge-asserts.ts @@ -2,8 +2,12 @@ import { SELF_MODULE, type Bridge, type NodeRef, + type Wire, } from "@stackables/bridge-core"; +const isPull = (w: Wire): boolean => w.sources[0]?.expr.type === "ref"; +const wRef = (w: Wire): NodeRef => (w.sources[0].expr as { ref: NodeRef }).ref; + export class BridgeCompilerIncompatibleError extends Error { constructor( public readonly operation: string, @@ -56,20 +60,22 @@ export function assertBridgeCompilerCompatible( ): void { const op = `${bridge.type}.${bridge.field}`; + const wires: Wire[] = bridge.wires; + // Pipe-handle trunk keys — block-scoped aliases inside array maps // reference these; the compiler handles them correctly. const pipeTrunkKeys = new Set((bridge.pipeHandles ?? []).map((ph) => ph.key)); - for (const w of bridge.wires) { + for (const w of wires) { // User-level alias (Shadow) wires: compiler has TDZ ordering bugs. // Block-scoped aliases inside array maps wire FROM a pipe-handle tool // instance (key is in pipeTrunkKeys) and are handled correctly. if (w.to.module === "__local" && w.to.type === "Shadow") { - if (!("from" in w)) continue; + if (!isPull(w)) continue; const fromKey = - w.from.instance != null - ? `${w.from.module}:${w.from.type}:${w.from.field}:${w.from.instance}` - : `${w.from.module}:${w.from.type}:${w.from.field}`; + wRef(w).instance != null + ? `${wRef(w).module}:${wRef(w).type}:${wRef(w).field}:${wRef(w).instance}` + : `${wRef(w).module}:${wRef(w).type}:${wRef(w).field}`; if (!pipeTrunkKeys.has(fromKey)) { throw new BridgeCompilerIncompatibleError( op, @@ -79,16 +85,12 @@ export function assertBridgeCompilerCompatible( continue; } - if (!("from" in w)) continue; + if (!isPull(w)) continue; // Catch fallback on pipe wires (expression results) — the catch must // propagate to the upstream tool, not the internal operator; codegen // does not handle this yet. - if ( - "pipe" in w && - w.pipe && - ("catchFallback" in w || "catchFallbackRef" in w || "catchControl" in w) - ) { + if (w.pipe && w.catch) { throw new BridgeCompilerIncompatibleError( op, "Catch fallback on expression (pipe) wires is not yet supported by the compiler.", @@ -97,13 +99,11 @@ export function assertBridgeCompilerCompatible( // Catch fallback that references a pipe handle — the compiler eagerly // calls all tools in the catch branch even when the main wire succeeds. - if ("catchFallbackRef" in w && w.catchFallbackRef) { - const ref = w.catchFallbackRef as NodeRef; + if (w.catch && "ref" in w.catch) { + const ref = w.catch.ref; if (ref.instance != null) { const refKey = `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`; - if ( - bridge.pipeHandles?.some((ph) => ph.key === refKey) - ) { + if (bridge.pipeHandles?.some((ph) => ph.key === refKey)) { throw new BridgeCompilerIncompatibleError( op, "Catch fallback referencing a pipe expression is not yet supported by the compiler.", @@ -115,16 +115,12 @@ export function assertBridgeCompilerCompatible( // Catch fallback on wires whose source tool has tool-backed input // dependencies — the compiler only catch-guards the direct source // tool, not its transitive dependency chain. - if ( - ("catchFallback" in w || "catchFallbackRef" in w || "catchControl" in w) && - "from" in w && - isToolRef(w.from, bridge) - ) { - const sourceTrunk = `${w.from.module}:${w.from.type}:${w.from.field}`; - for (const iw of bridge.wires) { - if (!("from" in iw)) continue; + if (w.catch && isToolRef(wRef(w), bridge)) { + const sourceTrunk = `${wRef(w).module}:${wRef(w).type}:${wRef(w).field}`; + for (const iw of wires) { + if (!isPull(iw)) continue; const iwDest = `${iw.to.module}:${iw.to.type}:${iw.to.field}`; - if (iwDest === sourceTrunk && isToolRef(iw.from, bridge)) { + if (iwDest === sourceTrunk && isToolRef(wRef(iw), bridge)) { throw new BridgeCompilerIncompatibleError( op, "Catch fallback on wires with tool chain dependencies is not yet supported by the compiler.", @@ -136,14 +132,12 @@ export function assertBridgeCompilerCompatible( // Fallback chains (|| / ??) with tool-backed refs — compiler eagerly // calls all tools via Promise.all, so short-circuit semantics are lost // and tool side effects fire unconditionally. - if (w.fallbacks) { - for (const fb of w.fallbacks) { - if (fb.ref && isToolRef(fb.ref, bridge)) { - throw new BridgeCompilerIncompatibleError( - op, - "Fallback chains (|| / ??) with tool-backed sources are not yet supported by the compiler.", - ); - } + for (const src of w.sources.slice(1)) { + if (src.expr.type === "ref" && isToolRef(src.expr.ref, bridge)) { + throw new BridgeCompilerIncompatibleError( + op, + "Fallback chains (|| / ??) with tool-backed sources are not yet supported by the compiler.", + ); } } } @@ -151,7 +145,7 @@ export function assertBridgeCompilerCompatible( // Same-cost overdefinition sourced only from tools can diverge from runtime // tracing/error behavior in current AOT codegen; compile must downgrade. const toolOnlyOverdefs = new Map(); - for (const w of bridge.wires) { + for (const w of wires) { if ( w.to.module !== SELF_MODULE || w.to.type !== bridge.type || @@ -159,7 +153,7 @@ export function assertBridgeCompilerCompatible( ) { continue; } - if (!("from" in w) || !isToolRef(w.from, bridge)) { + if (!isPull(w) || !isToolRef(wRef(w), bridge)) { continue; } @@ -196,8 +190,8 @@ export function assertBridgeCompilerCompatible( ); } - for (const w of bridge.wires) { - if (!("from" in w) || w.to.path.length === 0) continue; + for (const w of wires) { + if (!isPull(w) || w.to.path.length === 0) continue; // Build the full key for this wire target const fullKey = w.to.instance != null diff --git a/packages/bridge-compiler/src/codegen.ts b/packages/bridge-compiler/src/codegen.ts index 701f62c7..65f599e8 100644 --- a/packages/bridge-compiler/src/codegen.ts +++ b/packages/bridge-compiler/src/codegen.ts @@ -29,6 +29,8 @@ import type { Wire, NodeRef, ToolDef, + Expression, + ControlFlowInstruction, } from "@stackables/bridge-core"; import { BridgePanicError } from "@stackables/bridge-core"; import type { SourceLocation } from "@stackables/bridge-types"; @@ -39,6 +41,103 @@ import { const SELF_MODULE = "_"; +// ── Wire accessor helpers ─────────────────────────────────────────────────── +type RefExpr = Extract; +type LitExpr = Extract; +type TernExpr = Extract; +type AndOrExpr = + | Extract + | Extract; +type ControlExpr = Extract; + +function isPull(w: Wire): boolean { + return w.sources[0]!.expr.type === "ref"; +} +function isLit(w: Wire): boolean { + return w.sources[0]!.expr.type === "literal"; +} +function isTern(w: Wire): boolean { + return w.sources[0]!.expr.type === "ternary"; +} +function isAndW(w: Wire): boolean { + return w.sources[0]!.expr.type === "and"; +} +function isOrW(w: Wire): boolean { + return w.sources[0]!.expr.type === "or"; +} + +/** Primary source ref (for pull wires). */ +function wRef(w: Wire): NodeRef { + return (w.sources[0]!.expr as RefExpr).ref; +} +/** Primary source literal value (for constant wires). */ +function wVal(w: Wire): string { + return (w.sources[0]!.expr as LitExpr).value; +} +/** Safe flag on a pull wire's ref expression. */ +function wSafe(w: Wire): true | undefined { + return (w.sources[0]!.expr as RefExpr).safe; +} +/** Source ref location (for pull wires). */ +function wRefLoc(w: Wire): SourceLocation | undefined { + return (w.sources[0]!.expr as RefExpr).refLoc; +} +/** Ternary expression from a conditional wire. */ +function wTern(w: Wire): TernExpr { + return w.sources[0]!.expr as TernExpr; +} +/** And/Or expression from a logical wire. */ +function wAndOr(w: Wire): AndOrExpr { + return w.sources[0]!.expr as AndOrExpr; +} +/** Ref from an expression (for ref-type expressions). */ +function eRef(e: Expression): NodeRef { + return (e as RefExpr).ref; +} +/** Value from an expression (for literal-type expressions). */ +function eVal(e: Expression): string { + return (e as LitExpr).value; +} + +/** Whether a wire has a catch handler. */ +function hasCatchRef(w: Wire): boolean { + return w.catch != null && "ref" in w.catch; +} +function hasCatchValue(w: Wire): boolean { + return w.catch != null && "value" in w.catch; +} +function hasCatchControl(w: Wire): boolean { + return w.catch != null && "control" in w.catch; +} +/** Whether a wire has any catch fallback (ref or value). */ +function hasCatchFallback(w: Wire): boolean { + return hasCatchRef(w) || hasCatchValue(w); +} +/** Get the catch ref if present. */ +function catchRef(w: Wire): NodeRef | undefined { + return w.catch && "ref" in w.catch ? w.catch.ref : undefined; +} +/** Get the catch value if present. */ +function catchValue(w: Wire): string | undefined { + return w.catch && "value" in w.catch ? w.catch.value : undefined; +} +/** Get the catch control if present. */ +function catchControl(w: Wire): ControlFlowInstruction | undefined { + return w.catch && "control" in w.catch ? w.catch.control : undefined; +} +/** Get the catch location. */ +function catchLoc(w: Wire): SourceLocation | undefined { + return w.catch?.loc; +} +/** Get fallback source entries (everything after the primary source). */ +function fallbacks(w: Wire) { + return w.sources.slice(1); +} +/** Whether a wire has fallback entries. */ +function hasFallbacks(w: Wire): boolean { + return w.sources.length > 1; +} + function matchesRequestedFields( fieldPath: string, requestedFields: string[] | undefined, @@ -139,14 +238,6 @@ export function compileBridge( // ── Helpers ───────────────────────────────────────────────────────────────── -/** Check if a wire has catch fallback modifiers. */ -function hasCatchFallback(w: Wire): boolean { - return ( - ("catchFallback" in w && w.catchFallback != null) || - ("catchFallbackRef" in w && !!w.catchFallbackRef) - ); -} - type DetectedControlFlow = { kind: "break" | "continue" | "throw" | "panic"; levels: number; @@ -155,31 +246,23 @@ type DetectedControlFlow = { /** Check if any wire in a set has a control flow instruction (break/continue/throw/panic). */ 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) { - 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) { - const kind = w.catchControl.kind as - | "break" - | "continue" - | "throw" - | "panic"; + for (const fb of w.sources.slice(1)) { + if (fb.expr.type === "control") { + const ctrl = fb.expr.control; + const kind = ctrl.kind as "break" | "continue" | "throw" | "panic"; + const levels = + kind === "break" || kind === "continue" + ? Math.max(1, Number((ctrl as any).levels) || 1) + : 1; + return { kind, levels }; + } + } + const cc = catchControl(w); + if (cc) { + const kind = cc.kind as "break" | "continue" | "throw" | "panic"; const levels = kind === "break" || kind === "continue" - ? Math.max(1, Number((w.catchControl as any).levels) || 1) + ? Math.max(1, Number((cc as any).levels) || 1) : 1; return { kind, levels }; } @@ -187,11 +270,6 @@ function detectControlFlow(wires: Wire[]): DetectedControlFlow | null { return null; } -/** Check if a wire has a catch control flow instruction. */ -function hasCatchControl(w: Wire): boolean { - return "catchControl" in w && w.catchControl != null; -} - function splitToolName(name: string): { module: string; fieldName: string } { const dotIdx = name.lastIndexOf("."); if (dotIdx === -1) return { module: SELF_MODULE, fieldName: name }; @@ -384,7 +462,7 @@ class CodegenContext { // However, tools inlined from define blocks may use type "Define". // We detect the correct type by scanning the wires for a matching ref. let refType = module === SELF_MODULE ? "Tools" : bridge.type; - for (const w of bridge.wires) { + for (const w of this.bridge.wires) { if ( w.to.module === module && w.to.field === fieldName && @@ -394,12 +472,12 @@ class CodegenContext { break; } if ( - "from" in w && - w.from.module === module && - w.from.field === fieldName && - w.from.instance != null + isPull(w) && + wRef(w).module === module && + wRef(w).field === fieldName && + wRef(w).instance != null ) { - refType = w.from.type; + refType = wRef(w).type; break; } } @@ -451,7 +529,7 @@ class CodegenContext { // Detect alias declarations — wires targeting __local:Shadow: modules. // These act as virtual containers (like define modules). - for (const w of bridge.wires) { + for (const w of this.bridge.wires) { const toTk = refTrunkKey(w.to); if ( w.to.module === "__local" && @@ -463,11 +541,11 @@ class CodegenContext { this.defineContainers.add(toTk); } if ( - "from" in w && - w.from.module === "__local" && - w.from.type === "Shadow" + isPull(w) && + wRef(w).module === "__local" && + wRef(w).type === "Shadow" ) { - const fromTk = refTrunkKey(w.from); + const fromTk = refTrunkKey(wRef(w)); if (!this.varMap.has(fromTk)) { const vn = `_a${++this.toolCounter}`; this.varMap.set(fromTk, vn); @@ -494,13 +572,13 @@ class CodegenContext { ) instances.push(w.to.instance); if ( - "from" in w && - w.from.module === module && - w.from.type === type && - w.from.field === field && - w.from.instance != null + isPull(w) && + wRef(w).module === module && + wRef(w).type === type && + wRef(w).field === field && + wRef(w).instance != null ) - instances.push(w.from.instance); + instances.push(wRef(w).instance!); } const uniqueInstances = [...new Set(instances)].sort((a, b) => a - b); const nextIndex = this.toolInstanceCursors.get(sig) ?? 0; @@ -565,19 +643,21 @@ class CodegenContext { // 2. pullSingle guard — reject unsafe keys in wire source paths for (const w of bridge.wires) { const refs: NodeRef[] = []; - if ("from" in w) refs.push(w.from); - if ("cond" in w) { - refs.push(w.cond); - if (w.thenRef) refs.push(w.thenRef); - if (w.elseRef) refs.push(w.elseRef); - } - if ("condAnd" in w) { - refs.push(w.condAnd.leftRef); - if (w.condAnd.rightRef) refs.push(w.condAnd.rightRef); - } - if ("condOr" in w) { - refs.push(w.condOr.leftRef); - if (w.condOr.rightRef) refs.push(w.condOr.rightRef); + if (isPull(w)) refs.push(wRef(w)); + if (isTern(w)) { + refs.push(eRef(wTern(w).cond)); + if ((wTern(w).then as RefExpr).ref) + refs.push((wTern(w).then as RefExpr).ref); + if ((wTern(w).else as RefExpr).ref) + refs.push((wTern(w).else as RefExpr).ref); + } + if (isAndW(w)) { + refs.push(eRef(wAndOr(w).left)); + if (eRef(wAndOr(w).right)) refs.push(eRef(wAndOr(w).right)); + } + if (isOrW(w)) { + refs.push(eRef(wAndOr(w).left)); + if (eRef(wAndOr(w).right)) refs.push(eRef(wAndOr(w).right)); } for (const ref of refs) { for (const seg of ref.path) { @@ -663,39 +743,45 @@ class CodegenContext { const needsCatch = hasCatchFallback(w) || hasCatchControl(w) || - ("safe" in w && w.safe) || - ("condAnd" in w && (w.condAnd.safe || w.condAnd.rightSafe)) || - ("condOr" in w && (w.condOr.safe || w.condOr.rightSafe)); + wSafe(w) || + (isAndW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)) || + (isOrW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)); if (!needsCatch) continue; - if ("from" in w) { - const srcKey = refTrunkKey(w.from); + if (isPull(w)) { + const srcKey = refTrunkKey(wRef(w)); this.catchGuardedTools.add(srcKey); } - if ("condAnd" in w) { - this.catchGuardedTools.add(refTrunkKey(w.condAnd.leftRef)); - if (w.condAnd.rightRef) - this.catchGuardedTools.add(refTrunkKey(w.condAnd.rightRef)); + if (isAndW(w)) { + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).right))); } - if ("condOr" in w) { - this.catchGuardedTools.add(refTrunkKey(w.condOr.leftRef)); - if (w.condOr.rightRef) - this.catchGuardedTools.add(refTrunkKey(w.condOr.rightRef)); + if (isOrW(w)) { + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).right))); } } // Also mark tools catch-guarded if referenced by catch-guarded or safe define wires for (const [, dwires] of defineWires) { for (const w of dwires) { const needsCatch = - hasCatchFallback(w) || hasCatchControl(w) || ("safe" in w && w.safe); + hasCatchFallback(w) || hasCatchControl(w) || wSafe(w); if (!needsCatch) continue; - if ("from" in w) { - const srcKey = refTrunkKey(w.from); + if (isPull(w)) { + const srcKey = refTrunkKey(wRef(w)); this.catchGuardedTools.add(srcKey); } - if ("cond" in w) { - this.catchGuardedTools.add(refTrunkKey(w.cond)); - if (w.thenRef) this.catchGuardedTools.add(refTrunkKey(w.thenRef)); - if (w.elseRef) this.catchGuardedTools.add(refTrunkKey(w.elseRef)); + if (isTern(w)) { + this.catchGuardedTools.add(refTrunkKey(eRef(wTern(w).cond))); + if ((wTern(w).then as RefExpr).ref) + this.catchGuardedTools.add( + refTrunkKey((wTern(w).then as RefExpr).ref), + ); + if ((wTern(w).else as RefExpr).ref) + this.catchGuardedTools.add( + refTrunkKey((wTern(w).else as RefExpr).ref), + ); } } } @@ -704,22 +790,22 @@ class CodegenContext { for (const [, twires] of toolWires) { for (const w of twires) { const isSafe = - ("safe" in w && w.safe) || - ("condAnd" in w && (w.condAnd.safe || w.condAnd.rightSafe)) || - ("condOr" in w && (w.condOr.safe || w.condOr.rightSafe)); + wSafe(w) || + (isAndW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)) || + (isOrW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)); if (!isSafe) continue; - if ("from" in w) { - this.catchGuardedTools.add(refTrunkKey(w.from)); + if (isPull(w)) { + this.catchGuardedTools.add(refTrunkKey(wRef(w))); } - if ("condAnd" in w) { - this.catchGuardedTools.add(refTrunkKey(w.condAnd.leftRef)); - if (w.condAnd.rightRef) - this.catchGuardedTools.add(refTrunkKey(w.condAnd.rightRef)); + if (isAndW(w)) { + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).right))); } - if ("condOr" in w) { - this.catchGuardedTools.add(refTrunkKey(w.condOr.leftRef)); - if (w.condOr.rightRef) - this.catchGuardedTools.add(refTrunkKey(w.condOr.rightRef)); + if (isOrW(w)) { + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).right))); } } } @@ -737,7 +823,7 @@ class CodegenContext { for (const [tk, wires] of elementScopeEntries) { if (this.elementScopedTools.has(tk)) continue; for (const w of wires) { - if ("from" in w && w.from.element) { + if (isPull(w) && wRef(w).element) { this.elementScopedTools.add(tk); changed = true; break; @@ -1156,27 +1242,31 @@ class CodegenContext { const collectSourceKeys = (wires: Wire[]): Set => { const keys = new Set(); for (const w of wires) { - if ("from" in w) keys.add(refTrunkKey(w.from)); - if ("cond" in w) { - keys.add(refTrunkKey(w.cond)); - if (w.thenRef) keys.add(refTrunkKey(w.thenRef)); - if (w.elseRef) keys.add(refTrunkKey(w.elseRef)); + if (isPull(w)) keys.add(refTrunkKey(wRef(w))); + if (isTern(w)) { + keys.add(refTrunkKey(eRef(wTern(w).cond))); + if ((wTern(w).then as RefExpr).ref) + keys.add(refTrunkKey((wTern(w).then as RefExpr).ref)); + if ((wTern(w).else as RefExpr).ref) + keys.add(refTrunkKey((wTern(w).else as RefExpr).ref)); } - if ("condAnd" in w) { - keys.add(refTrunkKey(w.condAnd.leftRef)); - if (w.condAnd.rightRef) keys.add(refTrunkKey(w.condAnd.rightRef)); + if (isAndW(w)) { + keys.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + keys.add(refTrunkKey(eRef(wAndOr(w).right))); } - if ("condOr" in w) { - keys.add(refTrunkKey(w.condOr.leftRef)); - if (w.condOr.rightRef) keys.add(refTrunkKey(w.condOr.rightRef)); + if (isOrW(w)) { + keys.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + keys.add(refTrunkKey(eRef(wAndOr(w).right))); } - if ("fallbacks" in w && w.fallbacks) { - for (const fb of w.fallbacks) { - if (fb.ref) keys.add(refTrunkKey(fb.ref)); + if (hasFallbacks(w)) { + for (const fb of fallbacks(w)) { + if (eRef(fb.expr)) keys.add(refTrunkKey(eRef(fb.expr))); } } - if ("catchFallbackRef" in w && w.catchFallbackRef) { - keys.add(refTrunkKey(w.catchFallbackRef)); + if (hasCatchRef(w)) { + keys.add(refTrunkKey(catchRef(w)!)); } } return keys; @@ -1208,7 +1298,11 @@ class CodegenContext { // can execute in parallel — we emit them as a single Promise.all(). for (const layer of toolLayers) { // Classify tools in this layer - const parallelBatch: { tk: string; tool: ToolInfo; wires: Wire[] }[] = []; + const parallelBatch: { + tk: string; + tool: ToolInfo; + wires: Wire[]; + }[] = []; const sequentialKeys: string[] = []; for (const tk of layer) { @@ -1257,22 +1351,26 @@ class CodegenContext { } else if (wires.length === 1 && wires[0]!.to.path.length === 0) { const w = wires[0]!; let expr = this.wireToExpr(w); - if ("safe" in w && w.safe) { + if (wSafe(w)) { const errFlags: string[] = []; - const wAny = w as any; - if (wAny.from) { + if (isPull(w)) { const ef = this.getSourceErrorFlag(w); if (ef) errFlags.push(ef); } - if (wAny.cond) { - const condEf = this.getErrorFlagForRef(wAny.cond); + if (isTern(w)) { + const tern = wTern(w); + const condEf = this.getErrorFlagForRef(eRef(tern.cond)); if (condEf) errFlags.push(condEf); - if (wAny.thenRef) { - const ef = this.getErrorFlagForRef(wAny.thenRef); + if (tern.then.type === "ref") { + const ef = this.getErrorFlagForRef( + (tern.then as RefExpr).ref, + ); if (ef) errFlags.push(ef); } - if (wAny.elseRef) { - const ef = this.getErrorFlagForRef(wAny.elseRef); + if (tern.else.type === "ref") { + const ef = this.getErrorFlagForRef( + (tern.else as RefExpr).ref, + ); if (ef) errFlags.push(ef); } } @@ -1491,27 +1589,18 @@ class CodegenContext { for (const tw of toolDef.wires) { if (refTrunkKey(tw.to) !== forkKey) continue; const path = tw.to.path.join("."); - if ("value" in tw && !("cond" in tw)) { - forkInputs.set( - path, - emitCoerced((tw as Wire & { value: string }).value), - ); - } else if ("from" in tw) { - const fromKey = refTrunkKey((tw as Wire & { from: NodeRef }).from); + if (isLit(tw) && !isTern(tw)) { + forkInputs.set(path, emitCoerced(wVal(tw))); + } else if (isPull(tw)) { + const fromKey = refTrunkKey(wRef(tw)); if (forkExprs.has(fromKey)) { let expr = forkExprs.get(fromKey)!; - for (const p of (tw as Wire & { from: NodeRef }).from.path) { + for (const p of wRef(tw).path) { expr += `[${JSON.stringify(p)}]`; } forkInputs.set(path, expr); } else { - forkInputs.set( - path, - this.resolveToolWireSource( - tw as Wire & { from: NodeRef }, - toolDef, - ), - ); + forkInputs.set(path, this.resolveToolWireSource(tw, toolDef)); } } } @@ -1531,10 +1620,10 @@ class CodegenContext { // ToolDef constant wires (skip fork-targeted wires) for (const tw of toolDef.wires) { - if ("value" in tw && !("cond" in tw)) { + if (isLit(tw) && !isTern(tw)) { if (forkKeys.has(refTrunkKey(tw.to))) continue; const path = tw.to.path; - const expr = emitCoerced((tw as Wire & { value: string }).value); + const expr = emitCoerced(wVal(tw)); if (path.length > 1) { addNestedEntry(path, expr); } else { @@ -1545,24 +1634,21 @@ class CodegenContext { // ToolDef pull wires — resolved from tool handles (skip fork-targeted wires) for (const tw of toolDef.wires) { - if (!("from" in tw)) continue; + if (!isPull(tw)) continue; if (forkKeys.has(refTrunkKey(tw.to))) continue; // Skip wires with fallbacks — handled below - if ("fallbacks" in tw && (tw as any).fallbacks?.length > 0) continue; + if (hasFallbacks(tw)) continue; const path = tw.to.path; - const fromKey = refTrunkKey((tw as Wire & { from: NodeRef }).from); + const fromKey = refTrunkKey(wRef(tw)); let expr: string; if (forkExprs.has(fromKey)) { // Source is a fork result expr = forkExprs.get(fromKey)!; - for (const p of (tw as Wire & { from: NodeRef }).from.path) { + for (const p of wRef(tw).path) { expr = `(${expr})[${JSON.stringify(p)}]`; } } else { - expr = this.resolveToolWireSource( - tw as Wire & { from: NodeRef }, - toolDef, - ); + expr = this.resolveToolWireSource(tw, toolDef); } if (path.length > 1) { addNestedEntry(path, expr); @@ -1573,24 +1659,35 @@ class CodegenContext { // ToolDef ternary wires for (const tw of toolDef.wires) { - if (!("cond" in tw)) continue; + if (!isTern(tw)) continue; if (forkKeys.has(refTrunkKey(tw.to))) continue; const path = tw.to.path; + const tern = wTern(tw); const condExpr = this.resolveToolDefRef( - (tw as any).cond, + eRef(tern.cond), toolDef, forkExprs, ); - const thenExpr = (tw as any).thenRef - ? this.resolveToolDefRef((tw as any).thenRef, toolDef, forkExprs) - : (tw as any).thenValue !== undefined - ? emitCoerced((tw as any).thenValue) - : "undefined"; - const elseExpr = (tw as any).elseRef - ? this.resolveToolDefRef((tw as any).elseRef, toolDef, forkExprs) - : (tw as any).elseValue !== undefined - ? emitCoerced((tw as any).elseValue) - : "undefined"; + const thenExpr = + tern.then.type === "ref" + ? this.resolveToolDefRef( + (tern.then as RefExpr).ref, + toolDef, + forkExprs, + ) + : tern.then.type === "literal" + ? emitCoerced((tern.then as LitExpr).value) + : "undefined"; + const elseExpr = + tern.else.type === "ref" + ? this.resolveToolDefRef( + (tern.else as RefExpr).ref, + toolDef, + forkExprs, + ) + : tern.else.type === "literal" + ? emitCoerced((tern.else as LitExpr).value) + : "undefined"; const expr = `(${condExpr} ? ${thenExpr} : ${elseExpr})`; if (path.length > 1) { addNestedEntry(path, expr); @@ -1601,18 +1698,22 @@ class CodegenContext { // ToolDef fallback/coalesce wires (pull wires with fallbacks array) for (const tw of toolDef.wires) { - if (!("from" in tw)) continue; - if (!("fallbacks" in tw) || !(tw as any).fallbacks?.length) continue; + if (!isPull(tw)) continue; + if (!hasFallbacks(tw) || !fallbacks(tw).length) continue; if (forkKeys.has(refTrunkKey(tw.to))) continue; const path = tw.to.path; - const pullWire = tw as Wire & { from: NodeRef; fallbacks: any[] }; - let expr = this.resolveToolDefRef(pullWire.from, toolDef, forkExprs); - for (const fb of pullWire.fallbacks) { - const op = fb.type === "nullish" ? "??" : "||"; - if (fb.value !== undefined) { - expr = `(${expr} ${op} ${emitCoerced(fb.value)})`; - } else if (fb.ref) { - const refExpr = this.resolveToolDefRef(fb.ref, toolDef, forkExprs); + const pullWire = tw; + let expr = this.resolveToolDefRef(wRef(pullWire), toolDef, forkExprs); + for (const fb of fallbacks(pullWire)) { + const op = fb.gate === "nullish" ? "??" : "||"; + if (eVal(fb.expr) !== undefined) { + expr = `(${expr} ${op} ${emitCoerced(eVal(fb.expr))})`; + } else if (eRef(fb.expr)) { + const refExpr = this.resolveToolDefRef( + eRef(fb.expr), + toolDef, + forkExprs, + ); expr = `(${expr} ${op} ${refExpr})`; } } @@ -1849,7 +1950,7 @@ class CodegenContext { ); } for (const tw of depToolDef.wires) { - if (("value" in tw || "from" in tw) && !("cond" in tw)) { + if ((isLit(tw) || isPull(tw)) && !isTern(tw)) { if (tw.to.path.length > 1) { throw new BridgeCompilerIncompatibleError( `${this.bridge.type}.${this.bridge.field}`, @@ -1884,20 +1985,17 @@ class CodegenContext { // Constant wires for (const tw of depToolDef.wires) { - if ("value" in tw && !("cond" in tw)) { + if (isLit(tw) && !isTern(tw)) { inputParts.push( - ` ${JSON.stringify(tw.to.path.join("."))}: ${emitCoerced((tw as Wire & { value: string }).value)}`, + ` ${JSON.stringify(tw.to.path.join("."))}: ${emitCoerced(wVal(tw))}`, ); } } // Pull wires — resolved from the dep's own handles for (const tw of depToolDef.wires) { - if ("from" in tw) { - const source = this.resolveToolWireSource( - tw as Wire & { from: NodeRef }, - depToolDef, - ); + if (isPull(tw)) { + const source = this.resolveToolWireSource(tw, depToolDef); inputParts.push( ` ${JSON.stringify(tw.to.path.join("."))}: ${source}`, ); @@ -1939,11 +2037,8 @@ class CodegenContext { * Resolve a Wire's source NodeRef to a JS expression in the context of a ToolDef. * Handles context, const, and tool handle types. */ - private resolveToolWireSource( - wire: Wire & { from: NodeRef }, - toolDef: ToolDef, - ): string { - const ref = wire.from; + private resolveToolWireSource(wire: Wire, toolDef: ToolDef): string { + const ref = wRef(wire); // Match the ref against tool handles const h = toolDef.handles.find((handle) => { if (handle.kind === "context") { @@ -1996,7 +2091,7 @@ class CodegenContext { } // Delegate to resolveToolWireSource via a synthetic wire return this.resolveToolWireSource( - { from: ref, to: ref } as Wire & { from: NodeRef }, + { to: ref, sources: [{ expr: { type: "ref" as const, ref: ref } }] }, toolDef, ); } @@ -2117,12 +2212,8 @@ class CodegenContext { if (depToolDef) { const pathKey = restPath.join("."); for (const tw of depToolDef.wires) { - if ( - "value" in tw && - !("cond" in tw) && - tw.to.path.join(".") === pathKey - ) { - expr = `(${expr} ?? ${emitCoerced((tw as Wire & { value: string }).value)})`; + if (isLit(tw) && !isTern(tw) && tw.to.path.join(".") === pathKey) { + expr = `(${expr} ?? ${emitCoerced(wVal(tw))})`; break; } } @@ -2220,10 +2311,10 @@ class CodegenContext { // Separate root wires into passthrough vs spread const rootWires = outputWires.filter((w) => w.to.path.length === 0); const spreadRootWires = rootWires.filter( - (w) => "from" in w && "spread" in w && w.spread, + (w) => isPull(w) && w.spread && w.spread, ); const passthroughRootWire = rootWires.find( - (w) => !("from" in w && "spread" in w && w.spread), + (w) => !(isPull(w) && w.spread && w.spread), ); // Passthrough (non-spread root wire) — return directly @@ -2242,8 +2333,9 @@ class CodegenContext { ); let arrayExpr = this.wireToExpr(rootWire); // Check for catch control on root wire (e.g., `catch continue` returns []) - const rootCatchCtrl = - "catchControl" in rootWire ? rootWire.catchControl : undefined; + const rootCatchCtrl = hasCatchControl(rootWire) + ? catchControl(rootWire) + : undefined; if ( rootCatchCtrl && (rootCatchCtrl.kind === "continue" || rootCatchCtrl.kind === "break") @@ -2421,17 +2513,17 @@ class CodegenContext { for (const w of outputWires) { const topField = w.to.path[0]!; const isElementWire = - ("from" in w && - (w.from.element || + (isPull(w) && + (wRef(w).element || w.to.element || - this.elementScopedTools.has(refTrunkKey(w.from)) || + this.elementScopedTools.has(refTrunkKey(wRef(w))) || // Wires from bridge-level refs targeting inside an array mapping (arrayFields.has(topField) && w.to.path.length > 1))) || - (w.to.element && ("value" in w || "cond" in w)) || + (w.to.element && (isLit(w) || isTern(w))) || // Cond wires targeting a field inside an array mapping are element wires - ("cond" in w && arrayFields.has(topField) && w.to.path.length > 1) || + (isTern(w) && arrayFields.has(topField) && w.to.path.length > 1) || // Const wires targeting a field inside an array mapping are element wires - ("value" in w && arrayFields.has(topField) && w.to.path.length > 1); + (isLit(w) && arrayFields.has(topField) && w.to.path.length > 1); if (isElementWire) { // Element wire — belongs to an array mapping const arr = elementWires.get(topField) ?? []; @@ -2440,12 +2532,7 @@ class CodegenContext { } else if (arrayFields.has(topField) && w.to.path.length === 1) { // Root wire for an array field arraySourceWires.set(topField, w); - } else if ( - "from" in w && - "spread" in w && - w.spread && - w.to.path.length === 0 - ) { + } else if (isPull(w) && w.spread && w.spread && w.to.path.length === 0) { // Spread root wire — handled separately via spreadRootWires } else { scalarWires.push(w); @@ -2463,10 +2550,10 @@ class CodegenContext { // First pass: handle nested spread wires (spread with path.length > 0) const nestedSpreadWires = scalarWires.filter( - (w) => "from" in w && "spread" in w && w.spread && w.to.path.length > 0, + (w) => isPull(w) && w.spread && w.spread && w.to.path.length > 0, ); const normalScalarWires = scalarWires.filter( - (w) => !("from" in w && "spread" in w && w.spread), + (w) => !(isPull(w) && w.spread && w.spread), ); // Add nested spread expressions to tree nodes @@ -2771,83 +2858,89 @@ class CodegenContext { wire: Wire, visited = new Set(), ): boolean { - if ("value" in wire) return true; + if (isLit(wire)) return true; - if ("from" in wire) { - if (!this.refIsZeroCost(wire.from, visited)) return false; - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) { + if (isPull(wire)) { + if (!this.refIsZeroCost(wRef(wire), visited)) return false; + for (const fallback of fallbacks(wire) ?? []) { + if ( + eRef(fallback.expr) && + !this.refIsZeroCost(eRef(fallback.expr), visited) + ) { return false; } } - if ( - wire.catchFallbackRef && - !this.refIsZeroCost(wire.catchFallbackRef, visited) - ) { + if (catchRef(wire) && !this.refIsZeroCost(catchRef(wire)!, visited)) { return false; } return true; } - if ("cond" in wire) { - if (!this.refIsZeroCost(wire.cond, visited)) return false; - if (wire.thenRef && !this.refIsZeroCost(wire.thenRef, visited)) + if (isTern(wire)) { + if (!this.refIsZeroCost(eRef(wTern(wire).cond), visited)) return false; + if ( + (wTern(wire).then as RefExpr).ref && + !this.refIsZeroCost((wTern(wire).then as RefExpr).ref, visited) + ) return false; - if (wire.elseRef && !this.refIsZeroCost(wire.elseRef, visited)) + if ( + (wTern(wire).else as RefExpr).ref && + !this.refIsZeroCost((wTern(wire).else as RefExpr).ref, visited) + ) return false; - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) { + for (const fallback of fallbacks(wire) ?? []) { + if ( + eRef(fallback.expr) && + !this.refIsZeroCost(eRef(fallback.expr), visited) + ) { return false; } } - if ( - wire.catchFallbackRef && - !this.refIsZeroCost(wire.catchFallbackRef, visited) - ) { + if (catchRef(wire) && !this.refIsZeroCost(catchRef(wire)!, visited)) { return false; } return true; } - if ("condAnd" in wire) { - if (!this.refIsZeroCost(wire.condAnd.leftRef, visited)) return false; + if (isAndW(wire)) { + if (!this.refIsZeroCost(eRef(wAndOr(wire).left), visited)) return false; if ( - wire.condAnd.rightRef && - !this.refIsZeroCost(wire.condAnd.rightRef, visited) + eRef(wAndOr(wire).right) && + !this.refIsZeroCost(eRef(wAndOr(wire).right), visited) ) { return false; } - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) { + for (const fallback of fallbacks(wire) ?? []) { + if ( + eRef(fallback.expr) && + !this.refIsZeroCost(eRef(fallback.expr), visited) + ) { return false; } } - if ( - wire.catchFallbackRef && - !this.refIsZeroCost(wire.catchFallbackRef, visited) - ) { + if (catchRef(wire) && !this.refIsZeroCost(catchRef(wire)!, visited)) { return false; } return true; } - if ("condOr" in wire) { - if (!this.refIsZeroCost(wire.condOr.leftRef, visited)) return false; + if (isOrW(wire)) { + if (!this.refIsZeroCost(eRef(wAndOr(wire).left), visited)) return false; if ( - wire.condOr.rightRef && - !this.refIsZeroCost(wire.condOr.rightRef, visited) + eRef(wAndOr(wire).right) && + !this.refIsZeroCost(eRef(wAndOr(wire).right), visited) ) { return false; } - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref && !this.refIsZeroCost(fallback.ref, visited)) { + for (const fallback of fallbacks(wire) ?? []) { + if ( + eRef(fallback.expr) && + !this.refIsZeroCost(eRef(fallback.expr), visited) + ) { return false; } } - if ( - wire.catchFallbackRef && - !this.refIsZeroCost(wire.catchFallbackRef, visited) - ) { + if (catchRef(wire) && !this.refIsZeroCost(catchRef(wire)!, visited)) { return false; } return true; @@ -3046,11 +3139,11 @@ class CodegenContext { const controlWire = elemWires.find( (w) => w.to.path.length === 1 && - (("fallbacks" in w && w.fallbacks?.some((fb) => fb.control != null)) || - ("catchControl" in w && w.catchControl != null)), + (fallbacks(w).some((fb) => fb.expr.type === "control") || + hasCatchControl(w)), ); - if (!controlWire || !("from" in controlWire)) { + if (!controlWire || !isPull(controlWire)) { // No control flow found — fall back to simple body const body = this.buildElementBody( elemWires, @@ -3068,14 +3161,16 @@ class CodegenContext { const checkExpr = this.elementWireToExpr(controlWire, elVar); // Determine the check type - const isNullish = - 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 isNullish = fallbacks(controlWire).some( + (fb) => fb.gate === "nullish" && fb.expr.type === "control", + ); + const ctrlFb = fallbacks(controlWire).find( + (fb) => fb.expr.type === "control", + ); + const ctrlFromFallback = ctrlFb + ? (ctrlFb.expr as ControlExpr).control + : undefined; + const ctrl = ctrlFromFallback ?? catchControl(controlWire); const controlKind = ctrl?.kind === "continue" ? "continue" : "break"; const controlLevels = ctrl && (ctrl.kind === "continue" || ctrl.kind === "break") @@ -3116,32 +3211,38 @@ class CodegenContext { /** Convert a wire to a JavaScript expression string. */ wireToExpr(w: Wire): string { // Constant wire - if ("value" in w) return emitCoerced(w.value); + if (isLit(w)) return emitCoerced(wVal(w)); // Pull wire - if ("from" in w) { - let expr = this.wrapExprWithLoc(this.refToExpr(w.from), w.fromLoc); + if (isPull(w)) { + let expr = this.wrapExprWithLoc(this.refToExpr(wRef(w)), wRefLoc(w)); expr = this.applyFallbacks(w, expr); return this.wrapWireExpr(w, expr); } // Conditional wire (ternary) - if ("cond" in w) { + if (isTern(w)) { const condExpr = this.wrapExprWithLoc( - this.refToExpr(w.cond), - w.condLoc ?? w.loc, + this.refToExpr(eRef(wTern(w).cond)), + wTern(w).condLoc ?? w.loc, ); const thenExpr = - w.thenRef !== undefined - ? this.wrapExprWithLoc(this.lazyRefToExpr(w.thenRef), w.thenLoc) - : w.thenValue !== undefined - ? emitCoerced(w.thenValue) + (wTern(w).then as RefExpr).ref !== undefined + ? this.wrapExprWithLoc( + this.lazyRefToExpr((wTern(w).then as RefExpr).ref), + wTern(w).thenLoc, + ) + : (wTern(w).then as LitExpr).value !== undefined + ? emitCoerced((wTern(w).then as LitExpr).value) : "undefined"; const elseExpr = - w.elseRef !== undefined - ? this.wrapExprWithLoc(this.lazyRefToExpr(w.elseRef), w.elseLoc) - : w.elseValue !== undefined - ? emitCoerced(w.elseValue) + (wTern(w).else as RefExpr).ref !== undefined + ? this.wrapExprWithLoc( + this.lazyRefToExpr((wTern(w).else as RefExpr).ref), + wTern(w).elseLoc, + ) + : (wTern(w).else as LitExpr).value !== undefined + ? emitCoerced((wTern(w).else as LitExpr).value) : "undefined"; let expr = `(${condExpr} ? ${thenExpr} : ${elseExpr})`; expr = this.applyFallbacks(w, expr); @@ -3149,13 +3250,17 @@ class CodegenContext { } // Logical AND - if ("condAnd" in w) { - const { leftRef, rightRef, rightValue, rightSafe } = w.condAnd; + if (isAndW(w)) { + const ao = wAndOr(w); + const leftRef = eRef(ao.left); + const rightRef = ao.right.type === "ref" ? eRef(ao.right) : undefined; + const rightValue = + ao.right.type === "literal" ? eVal(ao.right) : undefined; const left = this.refToExpr(leftRef); let expr: string; if (rightRef) { let rightExpr = this.lazyRefToExpr(rightRef); - if (rightSafe && this.ternaryOnlyTools.has(refTrunkKey(rightRef))) { + if (ao.rightSafe && this.ternaryOnlyTools.has(refTrunkKey(rightRef))) { rightExpr = `await (async () => { try { return ${rightExpr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; return undefined; } })()`; } expr = `(Boolean(${left}) && Boolean(${rightExpr}))`; @@ -3167,19 +3272,26 @@ class CodegenContext { } // Logical OR - if ("condOr" in w) { - const { leftRef, rightRef, rightValue, rightSafe } = w.condOr; - const left = this.refToExpr(leftRef); + if (isOrW(w)) { + const ao2 = wAndOr(w); + const leftRef2 = eRef(ao2.left); + const rightRef2 = ao2.right.type === "ref" ? eRef(ao2.right) : undefined; + const rightValue2 = + ao2.right.type === "literal" ? eVal(ao2.right) : undefined; + const left2 = this.refToExpr(leftRef2); let expr: string; - if (rightRef) { - let rightExpr = this.lazyRefToExpr(rightRef); - if (rightSafe && this.ternaryOnlyTools.has(refTrunkKey(rightRef))) { + if (rightRef2) { + let rightExpr = this.lazyRefToExpr(rightRef2); + if ( + ao2.rightSafe && + this.ternaryOnlyTools.has(refTrunkKey(rightRef2)) + ) { rightExpr = `await (async () => { try { return ${rightExpr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; return undefined; } })()`; } - expr = `(Boolean(${left}) || Boolean(${rightExpr}))`; - } else if (rightValue !== undefined) - expr = `(Boolean(${left}) || Boolean(${emitCoerced(rightValue)}))`; - else expr = `Boolean(${left})`; + expr = `(Boolean(${left2}) || Boolean(${rightExpr}))`; + } else if (rightValue2 !== undefined) + expr = `(Boolean(${left2}) || Boolean(${emitCoerced(rightValue2)}))`; + else expr = `Boolean(${left2})`; expr = this.applyFallbacks(w, expr); return this.wrapWireExpr(w, expr); } @@ -3214,16 +3326,23 @@ class CodegenContext { */ private findPullingWireLoc(trunkKey: string): SourceLocation | undefined { for (const w of this.bridge.wires) { - if ("from" in w) { - const srcKey = refTrunkKey(w.from); - if (srcKey === trunkKey) return w.fromLoc ?? w.loc; + if (isPull(w)) { + const srcKey = refTrunkKey(wRef(w)); + if (srcKey === trunkKey) return wRefLoc(w) ?? w.loc; } - if ("cond" in w) { - if (refTrunkKey(w.cond) === trunkKey) return w.condLoc ?? w.loc; - if (w.thenRef && refTrunkKey(w.thenRef) === trunkKey) - return w.thenLoc ?? w.loc; - if (w.elseRef && refTrunkKey(w.elseRef) === trunkKey) - return w.elseLoc ?? w.loc; + if (isTern(w)) { + if (refTrunkKey(eRef(wTern(w).cond)) === trunkKey) + return wTern(w).condLoc ?? w.loc; + if ( + (wTern(w).then as RefExpr).ref && + refTrunkKey((wTern(w).then as RefExpr).ref) === trunkKey + ) + return wTern(w).thenLoc ?? w.loc; + if ( + (wTern(w).else as RefExpr).ref && + refTrunkKey((wTern(w).else as RefExpr).ref) === trunkKey + ) + return wTern(w).elseLoc ?? w.loc; } } return undefined; @@ -3255,11 +3374,11 @@ class CodegenContext { } private _elementWireToExprInner(w: Wire, elVar: string): string { - if ("value" in w) return emitCoerced(w.value); + if (isLit(w)) return emitCoerced(wVal(w)); // Handle ternary (conditional) wires inside array mapping - if ("cond" in w) { - const condRef = w.cond; + if (isTern(w)) { + const condRef = eRef(wTern(w).cond); let condExpr: string; if (condRef.element) { condExpr = this.refToElementExpr(condRef); @@ -3274,7 +3393,7 @@ class CodegenContext { condExpr = this.refToExpr(condRef); } } - condExpr = this.wrapExprWithLoc(condExpr, w.condLoc ?? w.loc); + condExpr = this.wrapExprWithLoc(condExpr, wTern(w).condLoc ?? w.loc); const resolveBranch = ( ref: NodeRef | undefined, val: string | undefined, @@ -3294,41 +3413,49 @@ class CodegenContext { } return val !== undefined ? emitCoerced(val) : "undefined"; }; - const thenExpr = resolveBranch(w.thenRef, w.thenValue, w.thenLoc); - const elseExpr = resolveBranch(w.elseRef, w.elseValue, w.elseLoc); + const thenExpr = resolveBranch( + (wTern(w).then as RefExpr).ref, + (wTern(w).then as LitExpr).value, + wTern(w).thenLoc, + ); + const elseExpr = resolveBranch( + (wTern(w).else as RefExpr).ref, + (wTern(w).else as LitExpr).value, + wTern(w).elseLoc, + ); let expr = `(${condExpr} ? ${thenExpr} : ${elseExpr})`; expr = this.applyFallbacks(w, expr); return expr; } - if ("from" in w) { + if (isPull(w)) { // Check if the source is an element-scoped tool (needs inline computation) - if (!w.from.element) { - const srcKey = refTrunkKey(w.from); + if (!wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if (this.elementScopedTools.has(srcKey)) { let expr = this.buildInlineToolExpr(srcKey, elVar); - if (w.from.path.length > 0) { - expr = this.appendPathExpr(`(${expr})`, w.from); + if (wRef(w).path.length > 0) { + expr = this.appendPathExpr(`(${expr})`, wRef(w)); } - expr = this.wrapExprWithLoc(expr, w.fromLoc); + expr = this.wrapExprWithLoc(expr, wRefLoc(w)); expr = this.applyFallbacks(w, expr); return expr; } // Non-element ref inside array mapping — use normal refToExpr - let expr = this.wrapExprWithLoc(this.refToExpr(w.from), w.fromLoc); + let expr = this.wrapExprWithLoc(this.refToExpr(wRef(w)), wRefLoc(w)); expr = this.applyFallbacks(w, expr); return expr; } // Element refs: from.element === true, path = ["srcField"] // Resolve elementDepth to find the correct enclosing element variable - const elemDepth = w.from.elementDepth ?? 0; + const elemDepth = wRef(w).elementDepth ?? 0; let targetVar = elVar; if (elemDepth > 0) { const currentDepth = parseInt(elVar.slice(3), 10); targetVar = `_el${currentDepth - elemDepth}`; } - let expr = this.appendPathExpr(targetVar, w.from, true); - expr = this.wrapExprWithLoc(expr, w.fromLoc); + let expr = this.appendPathExpr(targetVar, wRef(w), true); + expr = this.wrapExprWithLoc(expr, wRefLoc(w)); expr = this.applyFallbacks(w, expr); return expr; } @@ -3356,18 +3483,18 @@ class CodegenContext { if (wires.length === 1 && wires[0]!.to.path.length === 0) { const w = wires[0]!; // Check if the wire itself is element-scoped - if ("from" in w && w.from.element) { + if (isPull(w) && wRef(w).element) { return this.elementWireToExpr(w, elVar); } - if ("from" in w && !w.from.element) { + if (isPull(w) && !wRef(w).element) { // Check if the source is another element-scoped tool - const srcKey = refTrunkKey(w.from); + const srcKey = refTrunkKey(wRef(w)); if (this.elementScopedTools.has(srcKey)) { return this.elementWireToExpr(w, elVar); } } // Check if this is a pipe tool call (alias tool:source as name) - if ("from" in w && w.pipe) { + if (isPull(w) && w.pipe) { return this.elementWireToExpr(w, elVar); } return this.wireToExpr(w); @@ -3453,8 +3580,8 @@ class CodegenContext { */ private wireNeedsAwait(w: Wire): boolean { // Element-scoped non-internal tool reference generates await __call() - if ("from" in w && !w.from.element) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3499,8 +3626,8 @@ class CodegenContext { (w) => refTrunkKey(w.to) === trunkKey, ); for (const w of wires) { - if ("from" in w && !w.from.element) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) && @@ -3514,8 +3641,8 @@ class CodegenContext { return this.hasAsyncElementDeps(srcKey); } } - if ("from" in w && w.pipe) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && w.pipe) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3549,8 +3676,8 @@ class CodegenContext { (w) => refTrunkKey(w.to) === tk, ); for (const w of depWires) { - if ("from" in w && !w.from.element) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3558,8 +3685,8 @@ class CodegenContext { collectDeps(srcKey); } } - if ("from" in w && w.pipe) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && w.pipe) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3571,8 +3698,8 @@ class CodegenContext { }; for (const w of elemWires) { - if ("from" in w && !w.from.element) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3592,7 +3719,7 @@ class CodegenContext { if (wires.length === 1 && wires[0]!.to.path.length === 0) { const w = wires[0]!; const hasCatch = hasCatchFallback(w) || hasCatchControl(w); - const hasSafe = "from" in w && w.safe; + const hasSafe = isPull(w) && wSafe(w); const expr = this.elementWireToExpr(w, elVar); if (hasCatch || hasSafe) { lines.push( @@ -3671,7 +3798,7 @@ class CodegenContext { const err = new BridgePanicError( `Circular dependency detected: "${key}" depends on itself`, ); - (err as any).bridgeLoc = "fromLoc" in w ? w.fromLoc : w.loc; + (err as any).bridgeLoc = isPull(w) ? wRefLoc(w) : w.loc; throw err; } if (!needed.has(src)) continue; @@ -3756,8 +3883,8 @@ class CodegenContext { (w) => refTrunkKey(w.to) === tk, ); for (const w of depWires) { - if ("from" in w && !w.from.element) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3765,8 +3892,8 @@ class CodegenContext { collectDeps(srcKey); } } - if ("from" in w && w.pipe) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && w.pipe) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3777,8 +3904,8 @@ class CodegenContext { } }; for (const w of elemWires) { - if ("from" in w && !w.from.element) { - const srcKey = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcKey = refTrunkKey(wRef(w)); if ( this.elementScopedTools.has(srcKey) && !this.internalToolKeys.has(srcKey) @@ -3865,14 +3992,13 @@ class CodegenContext { // Top-level safe flag indicates the wire wants error → undefined conversion. // condAnd/condOr wires carry safe INSIDE (condAnd.safe) — those refs already // have rootSafe/pathSafe so __get handles null bases; no extra wrapping needed. - const wireSafe = "safe" in w && w.safe; + const wireSafe = wSafe(w); // When safe (?.) has fallbacks (?? / ||), convert tool error → undefined // BEFORE the fallback chain so that `a?.name ?? panic "msg"` triggers // the panic when the tool errors (safe makes it undefined, then ?? fires). - const hasFallbacks = - "fallbacks" in w && w.fallbacks && w.fallbacks.length > 0; + const wireHasFallbacks = hasFallbacks(w); if ( - hasFallbacks && + wireHasFallbacks && wireSafe && !hasCatchFallback(w) && !hasCatchControl(w) @@ -3883,15 +4009,15 @@ class CodegenContext { } } - if ("fallbacks" in w && w.fallbacks) { - for (const fb of w.fallbacks) { - if (fb.type === "falsy") { - if (fb.ref) { - expr = `(${expr} || ${this.wrapExprWithLoc(this.lazyRefToExpr(fb.ref), fb.loc)})`; // lgtm [js/code-injection] - } else if (fb.value != null) { - expr = `(${expr} || ${emitCoerced(fb.value)})`; // lgtm [js/code-injection] - } else if (fb.control) { - const ctrl = fb.control; + if (hasFallbacks(w)) { + for (const fb of fallbacks(w)) { + if (fb.gate === "falsy") { + if (eRef(fb.expr)) { + expr = `(${expr} || ${this.wrapExprWithLoc(this.lazyRefToExpr(eRef(fb.expr)), fb.loc)})`; // lgtm [js/code-injection] + } else if (eVal(fb.expr) != null) { + expr = `(${expr} || ${emitCoerced(eVal(fb.expr))})`; // lgtm [js/code-injection] + } else if ((fb.expr as ControlExpr).control) { + const ctrl = (fb.expr as ControlExpr).control; const fbLoc = this.serializeLoc(fb.loc); if (ctrl.kind === "throw") { expr = `(${expr} || (() => { throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${fbLoc} }); })())`; // lgtm [js/code-injection] @@ -3901,12 +4027,12 @@ class CodegenContext { } } else { // nullish - if (fb.ref) { - expr = `((__v) => (__v == null ? undefined : __v))((${expr} ?? ${this.wrapExprWithLoc(this.lazyRefToExpr(fb.ref), fb.loc)}))`; // lgtm [js/code-injection] - } else if (fb.value != null) { - expr = `((__v) => (__v == null ? undefined : __v))((${expr} ?? ${emitCoerced(fb.value)}))`; // lgtm [js/code-injection] - } else if (fb.control) { - const ctrl = fb.control; + if (eRef(fb.expr)) { + expr = `((__v) => (__v == null ? undefined : __v))((${expr} ?? ${this.wrapExprWithLoc(this.lazyRefToExpr(eRef(fb.expr)), fb.loc)}))`; // lgtm [js/code-injection] + } else if (eVal(fb.expr) != null) { + expr = `((__v) => (__v == null ? undefined : __v))((${expr} ?? ${emitCoerced(eVal(fb.expr))}))`; // lgtm [js/code-injection] + } else if ((fb.expr as ControlExpr).control) { + const ctrl = (fb.expr as ControlExpr).control; const fbLoc = this.serializeLoc(fb.loc); if (ctrl.kind === "throw") { expr = `(${expr} ?? (() => { throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${fbLoc} }); })())`; // lgtm [js/code-injection] @@ -3923,13 +4049,13 @@ class CodegenContext { if (hasCatchFallback(w)) { let catchExpr: string; - if ("catchFallbackRef" in w && w.catchFallbackRef) { + if (hasCatchRef(w)) { catchExpr = this.wrapExprWithLoc( - this.refToExpr(w.catchFallbackRef), - "catchLoc" in w ? w.catchLoc : undefined, + this.refToExpr(catchRef(w)!), + catchLoc(w), ); - } else if ("catchFallback" in w && w.catchFallback != null) { - catchExpr = emitCoerced(w.catchFallback); + } else if (hasCatchValue(w)) { + catchExpr = emitCoerced(catchValue(w)!); } else { catchExpr = "undefined"; } @@ -3943,7 +4069,7 @@ class CodegenContext { } else if (wireSafe && !hasCatchControl(w)) { // Safe navigation (?.) without catch — return undefined on error. // When fallbacks are present, the early conversion already happened above. - if (!hasFallbacks) { + if (!wireHasFallbacks) { if (errFlag) { expr = `(${errFlag} !== undefined ? undefined : ${expr})`; // lgtm [js/code-injection] } else { @@ -3955,8 +4081,8 @@ class CodegenContext { // so __get handles null bases gracefully. Don't re-throw; the natural Boolean() // evaluation produces the correct result (e.g. Boolean(undefined) → false). const isCondSafe = - ("condAnd" in w && (w.condAnd.safe || w.condAnd.rightSafe)) || - ("condOr" in w && (w.condOr.safe || w.condOr.rightSafe)); + (isAndW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)) || + (isOrW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)); if (!isCondSafe) { // This wire has NO catch fallback but its source tool is catch-guarded by another // wire. If the tool failed, re-throw the stored error rather than silently @@ -3966,23 +4092,21 @@ class CodegenContext { } // Catch control flow (throw/panic on catch gate) - if ("catchControl" in w && w.catchControl) { - const ctrl = w.catchControl; - const catchLoc = this.serializeLoc( - "catchLoc" in w ? w.catchLoc : undefined, - ); + if (hasCatchControl(w)) { + const ctrl = catchControl(w)!; + const cLoc = this.serializeLoc(catchLoc(w)); if (ctrl.kind === "throw") { // Wrap in catch IIFE — on error, throw the custom message if (errFlag) { - expr = `(${errFlag} !== undefined ? (() => { throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${catchLoc} }); })() : ${expr})`; // lgtm [js/code-injection] + expr = `(${errFlag} !== undefined ? (() => { throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${cLoc} }); })() : ${expr})`; // lgtm [js/code-injection] } else { - expr = `await (async () => { try { return ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${catchLoc} }); } })()`; // lgtm [js/code-injection] + expr = `await (async () => { try { return ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${cLoc} }); } })()`; // lgtm [js/code-injection] } } else if (ctrl.kind === "panic") { if (errFlag) { - expr = `(${errFlag} !== undefined ? (() => { const _e = new __BridgePanicError(${JSON.stringify(ctrl.message)}); _e.bridgeLoc = ${catchLoc}; throw _e; })() : ${expr})`; // lgtm [js/code-injection] + expr = `(${errFlag} !== undefined ? (() => { const _e = new __BridgePanicError(${JSON.stringify(ctrl.message)}); _e.bridgeLoc = ${cLoc}; throw _e; })() : ${expr})`; // lgtm [js/code-injection] } else { - expr = `await (async () => { try { return ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; const _pe = new __BridgePanicError(${JSON.stringify(ctrl.message)}); _pe.bridgeLoc = ${catchLoc}; throw _pe; } })()`; // lgtm [js/code-injection] + expr = `await (async () => { try { return ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; const _pe = new __BridgePanicError(${JSON.stringify(ctrl.message)}); _pe.bridgeLoc = ${cLoc}; throw _pe; } })()`; // lgtm [js/code-injection] } } } @@ -3993,41 +4117,41 @@ class CodegenContext { /** Get the error flag variable name for a wire's source tool, but ONLY if * that tool was compiled in catch-guarded mode (i.e. the `_err` variable exists). */ private getSourceErrorFlag(w: Wire): string | undefined { - if ("from" in w) { - return this.getErrorFlagForRef(w.from); + if (isPull(w)) { + return this.getErrorFlagForRef(wRef(w)); } // For ternary wires, check all referenced tools - if ("cond" in w) { + if (isTern(w)) { const flags: string[] = []; - const cf = this.getErrorFlagForRef(w.cond); + const cf = this.getErrorFlagForRef(eRef(wTern(w).cond)); if (cf) flags.push(cf); - if (w.thenRef) { - const f = this.getErrorFlagForRef(w.thenRef); + if ((wTern(w).then as RefExpr).ref) { + const f = this.getErrorFlagForRef((wTern(w).then as RefExpr).ref); if (f && !flags.includes(f)) flags.push(f); } - if (w.elseRef) { - const f = this.getErrorFlagForRef(w.elseRef); + if ((wTern(w).else as RefExpr).ref) { + const f = this.getErrorFlagForRef((wTern(w).else as RefExpr).ref); if (f && !flags.includes(f)) flags.push(f); } if (flags.length > 0) return flags.join(" ?? "); // Combine error flags } // For condAnd/condOr wires, check leftRef and rightRef - if ("condAnd" in w) { + if (isAndW(w)) { const flags: string[] = []; - const lf = this.getErrorFlagForRef(w.condAnd.leftRef); + const lf = this.getErrorFlagForRef(eRef(wAndOr(w).left)); if (lf) flags.push(lf); - if (w.condAnd.rightRef) { - const rf = this.getErrorFlagForRef(w.condAnd.rightRef); + if (eRef(wAndOr(w).right)) { + const rf = this.getErrorFlagForRef(eRef(wAndOr(w).right)); if (rf && !flags.includes(rf)) flags.push(rf); } if (flags.length > 0) return flags.join(" ?? "); } - if ("condOr" in w) { + if (isOrW(w)) { const flags: string[] = []; - const lf = this.getErrorFlagForRef(w.condOr.leftRef); + const lf = this.getErrorFlagForRef(eRef(wAndOr(w).left)); if (lf) flags.push(lf); - if (w.condOr.rightRef) { - const rf = this.getErrorFlagForRef(w.condOr.rightRef); + if (eRef(wAndOr(w).right)) { + const rf = this.getErrorFlagForRef(eRef(wAndOr(w).right)); if (rf && !flags.includes(rf)) flags.push(rf); } if (flags.length > 0) return flags.join(" ?? "); @@ -4147,21 +4271,18 @@ class CodegenContext { if (toolDef) { const inputEntries = new Map(); for (const tw of toolDef.wires) { - if ("value" in tw && !("cond" in tw)) { + if (isLit(tw) && !isTern(tw)) { const target = tw.to.path.join("."); inputEntries.set( target, - `${JSON.stringify(target)}: ${emitCoerced((tw as Wire & { value: string }).value)}`, + `${JSON.stringify(target)}: ${emitCoerced(wVal(tw))}`, ); } } for (const tw of toolDef.wires) { - if ("from" in tw) { + if (isPull(tw)) { const target = tw.to.path.join("."); - const expr = this.resolveToolWireSource( - tw as Wire & { from: NodeRef }, - toolDef, - ); + const expr = this.resolveToolWireSource(tw, toolDef); inputEntries.set(target, `${JSON.stringify(target)}: ${expr}`); } } @@ -4209,33 +4330,34 @@ class CodegenContext { const ternaryBranchRefs = new Set(); const processWire = (w: Wire) => { - if ("from" in w && !w.from.element) { - allRefs.add(refTrunkKey(w.from)); - } - if ("cond" in w) { - allRefs.add(refTrunkKey(w.cond)); - if (w.thenRef) ternaryBranchRefs.add(refTrunkKey(w.thenRef)); - if (w.elseRef) ternaryBranchRefs.add(refTrunkKey(w.elseRef)); - } - if ("condAnd" in w) { - allRefs.add(refTrunkKey(w.condAnd.leftRef)); - if (w.condAnd.rightRef) - ternaryBranchRefs.add(refTrunkKey(w.condAnd.rightRef)); - } - if ("condOr" in w) { - allRefs.add(refTrunkKey(w.condOr.leftRef)); - if (w.condOr.rightRef) - ternaryBranchRefs.add(refTrunkKey(w.condOr.rightRef)); + if (isPull(w) && !wRef(w).element) { + allRefs.add(refTrunkKey(wRef(w))); + } + if (isTern(w)) { + allRefs.add(refTrunkKey(eRef(wTern(w).cond))); + if ((wTern(w).then as RefExpr).ref) + ternaryBranchRefs.add(refTrunkKey((wTern(w).then as RefExpr).ref)); + if ((wTern(w).else as RefExpr).ref) + ternaryBranchRefs.add(refTrunkKey((wTern(w).else as RefExpr).ref)); + } + if (isAndW(w)) { + allRefs.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + ternaryBranchRefs.add(refTrunkKey(eRef(wAndOr(w).right))); + } + if (isOrW(w)) { + allRefs.add(refTrunkKey(eRef(wAndOr(w).left))); + if (eRef(wAndOr(w).right)) + ternaryBranchRefs.add(refTrunkKey(eRef(wAndOr(w).right))); } // Fallback refs — on ternary wires, treat as lazy (ternary-branch-like) - if ("fallbacks" in w && w.fallbacks) { - const refSet = "cond" in w ? ternaryBranchRefs : allRefs; - for (const fb of w.fallbacks) { - if (fb.ref) refSet.add(refTrunkKey(fb.ref)); + if (hasFallbacks(w)) { + const refSet = isTern(w) ? ternaryBranchRefs : allRefs; + for (const fb of fallbacks(w)) { + if (eRef(fb.expr)) refSet.add(refTrunkKey(eRef(fb.expr))); } } - if ("catchFallbackRef" in w && w.catchFallbackRef) - allRefs.add(refTrunkKey(w.catchFallbackRef)); + if (hasCatchRef(w)) allRefs.add(refTrunkKey(catchRef(w)!)); }; for (const w of outputWires) processWire(w); @@ -4268,7 +4390,7 @@ class CodegenContext { wire: Wire, ): void { const nextExpr = this.wireToExpr(wire); - const nextIsConstant = "value" in wire; + const nextIsConstant = isLit(wire); if (node.expr == null) { node.expr = nextExpr; @@ -4406,7 +4528,7 @@ class CodegenContext { const outputByPath = new Map(); for (const w of outputWires) { if (w.to.path.length === 0) continue; - if ("from" in w && w.from.element) continue; + if (isPull(w) && wRef(w).element) continue; const pathKey = w.to.path.join("."); const arr = outputByPath.get(pathKey) ?? []; arr.push(w); @@ -4438,8 +4560,8 @@ class CodegenContext { const wireExpr = this.wireToExpr(w); // Check if this wire pulls from a tool - if ("from" in w && !w.from.element) { - const srcTk = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const srcTk = refTrunkKey(wRef(w)); if (this.tools.has(srcTk) && !this.defineContainers.has(srcTk)) { if (!toolInfo.has(srcTk)) { toolInfo.set(srcTk, { secondaryPaths: [], hasPrimary: false }); @@ -4464,8 +4586,8 @@ class CodegenContext { } // Track tools referenced in this wire (for cascading conditionals) - if ("from" in w && !w.from.element) { - const refTk = refTrunkKey(w.from); + if (isPull(w) && !wRef(w).element) { + const refTk = refTrunkKey(wRef(w)); if (this.tools.has(refTk)) priorToolsForPath.add(refTk); } @@ -4520,9 +4642,9 @@ class CodegenContext { info.secondaryPaths.map((sp) => sp.pathKey), ); for (const w of outputWires) { - if (!("from" in w)) continue; - if (w.from.element) continue; - const srcTk = refTrunkKey(w.from); + if (!isPull(w)) continue; + if (wRef(w).element) continue; + const srcTk = refTrunkKey(wRef(w)); if (srcTk !== toolTk) continue; if (w.to.path.length === 0) { hasUncaptured = true; @@ -4552,28 +4674,29 @@ class CodegenContext { const trunks: string[] = []; const collectTrunk = (ref: NodeRef) => trunks.push(refTrunkKey(ref)); - if ("from" in w) { - collectTrunk(w.from); - if (w.fallbacks) { - for (const fb of w.fallbacks) { - if (fb.ref) collectTrunk(fb.ref); + if (isPull(w)) { + collectTrunk(wRef(w)); + if (fallbacks(w)) { + for (const fb of fallbacks(w)) { + if (eRef(fb.expr)) collectTrunk(eRef(fb.expr)); } } - if ("catchFallbackRef" in w && w.catchFallbackRef) - collectTrunk(w.catchFallbackRef); + if (hasCatchRef(w)) collectTrunk(catchRef(w)!); } - if ("cond" in w) { - collectTrunk(w.cond); - if (w.thenRef) collectTrunk(w.thenRef); - if (w.elseRef) collectTrunk(w.elseRef); + if (isTern(w)) { + collectTrunk(eRef(wTern(w).cond)); + if ((wTern(w).then as RefExpr).ref) + collectTrunk((wTern(w).then as RefExpr).ref); + if ((wTern(w).else as RefExpr).ref) + collectTrunk((wTern(w).else as RefExpr).ref); } - if ("condAnd" in w) { - collectTrunk(w.condAnd.leftRef); - if (w.condAnd.rightRef) collectTrunk(w.condAnd.rightRef); + if (isAndW(w)) { + collectTrunk(eRef(wAndOr(w).left)); + if (eRef(wAndOr(w).right)) collectTrunk(eRef(wAndOr(w).right)); } - if ("condOr" in w) { - collectTrunk(w.condOr.leftRef); - if (w.condOr.rightRef) collectTrunk(w.condOr.rightRef); + if (isOrW(w)) { + collectTrunk(eRef(wAndOr(w).left)); + if (eRef(wAndOr(w).right)) collectTrunk(eRef(wAndOr(w).right)); } return trunks; } @@ -4622,21 +4745,18 @@ class CodegenContext { const fnName = toolDef.fn ?? tool.toolName; const inputEntries = new Map(); for (const tw of toolDef.wires) { - if ("value" in tw && !("cond" in tw)) { + if (isLit(tw) && !isTern(tw)) { const target = tw.to.path.join("."); inputEntries.set( target, - ` ${JSON.stringify(target)}: ${emitCoerced((tw as Wire & { value: string }).value)}`, + ` ${JSON.stringify(target)}: ${emitCoerced(wVal(tw))}`, ); } } for (const tw of toolDef.wires) { - if ("from" in tw) { + if (isPull(tw)) { const target = tw.to.path.join("."); - const expr = this.resolveToolWireSource( - tw as Wire & { from: NodeRef }, - toolDef, - ); + const expr = this.resolveToolWireSource(tw, toolDef); inputEntries.set(target, ` ${JSON.stringify(target)}: ${expr}`); } } @@ -4678,7 +4798,7 @@ class CodegenContext { const err = new BridgePanicError( `Circular dependency detected: "${key}" depends on itself`, ); - (err as any).bridgeLoc = "fromLoc" in w ? w.fromLoc : w.loc; + (err as any).bridgeLoc = isPull(w) ? wRefLoc(w) : w.loc; throw err; } if (adj.has(src)) { @@ -4734,7 +4854,7 @@ class CodegenContext { const err = new BridgePanicError( `Circular dependency detected: "${key}" depends on itself`, ); - (err as any).bridgeLoc = "fromLoc" in w ? w.fromLoc : w.loc; + (err as any).bridgeLoc = isPull(w) ? wRefLoc(w) : w.loc; throw err; } if (adj.has(src)) { diff --git a/packages/bridge-compiler/test/codegen.test.ts b/packages/bridge-compiler/test/codegen.test.ts index 2dde1b4d..7886853e 100644 --- a/packages/bridge-compiler/test/codegen.test.ts +++ b/packages/bridge-compiler/test/codegen.test.ts @@ -334,19 +334,29 @@ describe("AOT codegen: fallback operators", () => { ], wires: [ { - from: { - module: "_", - type: "Query", - field: "nullishProbe", - path: ["m"], - }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "_", + type: "Query", + field: "nullishProbe", + path: ["m"], + }, + }, + }, + { + expr: { type: "literal", value: "null" }, + gate: "nullish", + }, + ], to: { module: "_", type: "Query", field: "nullishProbe", path: ["k"], }, - fallbacks: [{ type: "nullish", value: "null" }], }, ], }, @@ -602,15 +612,26 @@ describe("AOT codegen: conditional wires", () => { ], wires: [ { - condAnd: { - leftRef: { - module: "_", - type: "Query", - field: "probe", - path: ["m"], + sources: [ + { + expr: { + type: "and", + left: { + type: "ref", + ref: { + module: "_", + type: "Query", + field: "probe", + path: ["m"], + }, + }, + right: { + type: "literal", + value: "null", + }, + }, }, - rightValue: "null", - }, + ], to: { module: "_", type: "Query", @@ -653,15 +674,26 @@ describe("AOT codegen: conditional wires", () => { ], wires: [ { - condOr: { - leftRef: { - module: "_", - type: "Query", - field: "probeOr", - path: ["m"], + sources: [ + { + expr: { + type: "or", + left: { + type: "ref", + ref: { + module: "_", + type: "Query", + field: "probeOr", + path: ["m"], + }, + }, + right: { + type: "literal", + value: "null", + }, + }, }, - rightValue: "null", - }, + ], to: { module: "_", type: "Query", @@ -1641,7 +1673,7 @@ bridge Query.test { handles: [{ kind: "output", handle: "o" }], wires: [ { - value: "null", + sources: [{ expr: { type: "literal", value: "null" } }], to: { module: "_", type: "Query", @@ -1650,7 +1682,7 @@ bridge Query.test { }, }, { - value: "false", + sources: [{ expr: { type: "literal", value: "false" } }], to: { module: "_", type: "Query", diff --git a/packages/bridge-compiler/test/fuzz-compile.fuzz.ts b/packages/bridge-compiler/test/fuzz-compile.fuzz.ts index 3f2fa8f8..d1ea7caf 100644 --- a/packages/bridge-compiler/test/fuzz-compile.fuzz.ts +++ b/packages/bridge-compiler/test/fuzz-compile.fuzz.ts @@ -7,7 +7,7 @@ import type { BridgeDocument, NodeRef, Wire, - WireFallback, + WireSourceEntry, } from "@stackables/bridge-core"; import { executeBridge as executeRuntime } from "@stackables/bridge-core"; import { @@ -76,70 +76,121 @@ const wireArb = (type: string, field: string): fc.Arbitrary => { const toArb = pathArb.map((path) => outputRef(type, field, path)); const fromArb = pathArb.map((path) => inputRef(type, field, path)); + const fallbackSourceArb: fc.Arbitrary = fc.oneof( + fc.record({ + gate: fc.constant<"falsy">("falsy"), + expr: constantValueArb.map((v) => ({ + type: "literal" as const, + value: v, + })), + }), + fc.record({ + gate: fc.constant<"nullish">("nullish"), + expr: constantValueArb.map((v) => ({ + type: "literal" as const, + value: v, + })), + }), + ); + return fc.oneof( // 1. Constant Wires - fc.record({ - value: constantValueArb, - to: toArb, - }), + fc.tuple(toArb, constantValueArb).map( + ([to, value]): Wire => ({ + to, + sources: [{ expr: { type: "literal", value } }], + }), + ), // 2. Complex Pull Wires (Randomly injecting fallbacks) - fc.record( - { - from: fromArb, - to: toArb, - fallbacks: fc.array( - fc.oneof( - fc.record({ - type: fc.constant<"falsy">("falsy"), - value: constantValueArb, - }), - fc.record({ - type: fc.constant<"nullish">("nullish"), - value: constantValueArb, - }), - ) as fc.Arbitrary, - { minLength: 0, maxLength: 2 }, - ), - catchFallback: constantValueArb, - }, - { requiredKeys: ["from", "to"] }, // Fallbacks are randomly omitted - ), + fc + .tuple( + fromArb, + toArb, + fc.array(fallbackSourceArb, { minLength: 0, maxLength: 2 }), + fc.option(constantValueArb, { nil: undefined }), + ) + .map(([from, to, fallbacks, catchVal]): Wire => { + const sources: WireSourceEntry[] = [ + { expr: { type: "ref", ref: from } }, + ...fallbacks, + ]; + const wire: Wire = { to, sources }; + if (catchVal !== undefined) wire.catch = { value: catchVal }; + return wire; + }), // 3. Ternary Conditional Wires - fc.record( - { - cond: fromArb, - to: toArb, - thenValue: constantValueArb, - elseValue: constantValueArb, - }, - { requiredKeys: ["cond", "to"] }, // then/else are randomly omitted - ), + fc + .tuple( + fromArb, + toArb, + fc.option(constantValueArb, { nil: undefined }), + fc.option(constantValueArb, { nil: undefined }), + ) + .map( + ([cond, to, thenVal, elseVal]): Wire => ({ + to, + sources: [ + { + expr: { + type: "ternary", + cond: { type: "ref", ref: cond }, + then: + thenVal !== undefined + ? { type: "literal", value: thenVal } + : { type: "literal", value: "null" }, + else: + elseVal !== undefined + ? { type: "literal", value: elseVal } + : { type: "literal", value: "null" }, + }, + }, + ], + }), + ), // 4. Logical AND - fc.record({ - condAnd: fc.record( - { - leftRef: fromArb, - rightValue: constantValueArb, - }, - { requiredKeys: ["leftRef"] }, + fc + .tuple(fromArb, toArb, fc.option(constantValueArb, { nil: undefined })) + .map( + ([left, to, rightVal]): Wire => ({ + to, + sources: [ + { + expr: { + type: "and", + left: { type: "ref", ref: left }, + right: + rightVal !== undefined + ? { type: "literal", value: rightVal } + : { type: "literal", value: "null" }, + }, + }, + ], + }), ), - to: toArb, - }), // 5. Logical OR - fc.record({ - condOr: fc.record( - { - leftRef: fromArb, - rightValue: constantValueArb, - }, - { requiredKeys: ["leftRef"] }, + fc + .tuple(fromArb, toArb, fc.option(constantValueArb, { nil: undefined })) + .map( + ([left, to, rightVal]): Wire => ({ + to, + sources: [ + { + expr: { + type: "or", + left: { type: "ref", ref: left }, + right: + rightVal !== undefined + ? { type: "literal", value: rightVal } + : { type: "literal", value: "null" }, + }, + }, + ], + }), ), - to: toArb, - }), ); }; @@ -148,14 +199,18 @@ const flatWireArb = (type: string, field: string): fc.Arbitrary => { const fromArb = flatPathArb.map((path) => inputRef(type, field, path)); return fc.oneof( - fc.record({ - value: constantValueArb, - to: toArb, - }), - fc.record({ - from: fromArb, - to: toArb, - }), + fc.tuple(toArb, constantValueArb).map( + ([to, value]): Wire => ({ + to, + sources: [{ expr: { type: "literal", value } }], + }), + ), + fc.tuple(fromArb, toArb).map( + ([from, to]): Wire => ({ + to, + sources: [{ expr: { type: "ref", ref: from } }], + }), + ), ); }; @@ -363,24 +418,38 @@ const fallbackHeavyBridgeArb: fc.Arbitrary = fc { kind: "output", handle: "o" } as const, ]), wires: fc.uniqueArray( - fc.record({ - from: flatPathArb.map((path) => inputRef(type, field, path)), - to: flatPathArb.map((path) => outputRef(type, field, path)), - fallbacks: fc.array( - fc.oneof( - fc.record({ - type: fc.constant<"falsy">("falsy"), - value: constantValueArb, - }), - fc.record({ - type: fc.constant<"nullish">("nullish"), - value: constantValueArb, - }), - ) as fc.Arbitrary, - { minLength: 0, maxLength: 2 }, + fc + .tuple( + flatPathArb.map((path) => inputRef(type, field, path)), + flatPathArb.map((path) => outputRef(type, field, path)), + fc.array( + fc.oneof( + fc.record({ + gate: fc.constant<"falsy">("falsy"), + expr: constantValueArb.map((v) => ({ + type: "literal" as const, + value: v, + })), + }), + fc.record({ + gate: fc.constant<"nullish">("nullish"), + expr: constantValueArb.map((v) => ({ + type: "literal" as const, + value: v, + })), + }), + ), + { minLength: 0, maxLength: 2 }, + ), + constantValueArb, + ) + .map( + ([from, to, fallbacks, catchVal]): Wire => ({ + to, + sources: [{ expr: { type: "ref", ref: from } }, ...fallbacks], + catch: { value: catchVal }, + }), ), - catchFallback: constantValueArb, - }), { minLength: 1, maxLength: 20, @@ -409,35 +478,83 @@ const logicalBridgeArb: fc.Arbitrary = fc ]), wires: fc.uniqueArray( fc.oneof( - fc.record( - { - cond: fromArb, - to: toArb, - thenValue: constantValueArb, - elseValue: constantValueArb, - }, - { requiredKeys: ["cond", "to"] }, - ), - fc.record({ - condAnd: fc.record( - { - leftRef: fromArb, - rightValue: constantValueArb, - }, - { requiredKeys: ["leftRef"] }, + // Ternary + fc + .tuple( + fromArb, + toArb, + fc.option(constantValueArb, { nil: undefined }), + fc.option(constantValueArb, { nil: undefined }), + ) + .map( + ([cond, to, thenVal, elseVal]): Wire => ({ + to, + sources: [ + { + expr: { + type: "ternary", + cond: { type: "ref", ref: cond }, + then: + thenVal !== undefined + ? { type: "literal", value: thenVal } + : { type: "literal", value: "null" }, + else: + elseVal !== undefined + ? { type: "literal", value: elseVal } + : { type: "literal", value: "null" }, + }, + }, + ], + }), ), - to: toArb, - }), - fc.record({ - condOr: fc.record( - { - leftRef: fromArb, - rightValue: constantValueArb, - }, - { requiredKeys: ["leftRef"] }, + // Logical AND + fc + .tuple( + fromArb, + toArb, + fc.option(constantValueArb, { nil: undefined }), + ) + .map( + ([left, to, rightVal]): Wire => ({ + to, + sources: [ + { + expr: { + type: "and", + left: { type: "ref", ref: left }, + right: + rightVal !== undefined + ? { type: "literal", value: rightVal } + : { type: "literal", value: "null" }, + }, + }, + ], + }), + ), + // Logical OR + fc + .tuple( + fromArb, + toArb, + fc.option(constantValueArb, { nil: undefined }), + ) + .map( + ([left, to, rightVal]): Wire => ({ + to, + sources: [ + { + expr: { + type: "or", + left: { type: "ref", ref: left }, + right: + rightVal !== undefined + ? { type: "literal", value: rightVal } + : { type: "literal", value: "null" }, + }, + }, + ], + }), ), - to: toArb, - }), ), { minLength: 1, diff --git a/packages/bridge-compiler/test/fuzz-runtime-parity.fuzz.ts b/packages/bridge-compiler/test/fuzz-runtime-parity.fuzz.ts index 4e815a04..a5673c93 100644 --- a/packages/bridge-compiler/test/fuzz-runtime-parity.fuzz.ts +++ b/packages/bridge-compiler/test/fuzz-runtime-parity.fuzz.ts @@ -89,11 +89,13 @@ const deepWireArb = (type: string, field: string): fc.Arbitrary => { return fc.oneof( fc.record({ - value: constantValueArb, + sources: constantValueArb.map((v) => [ + { expr: { type: "literal" as const, value: v } }, + ]), to: toArb, }), fc.record({ - from: fromArb, + sources: fromArb.map((r) => [{ expr: { type: "ref" as const, ref: r } }]), to: toArb, }), ); diff --git a/packages/bridge-core/src/ExecutionTree.ts b/packages/bridge-core/src/ExecutionTree.ts index 24b3ff95..bb01321b 100644 --- a/packages/bridge-core/src/ExecutionTree.ts +++ b/packages/bridge-core/src/ExecutionTree.ts @@ -42,6 +42,8 @@ import { } from "./tree-types.ts"; import { pathEquals, + getPrimaryRef, + isPullWire, roundMs, sameTrunk, TRUNK_KEY_CACHE, @@ -51,6 +53,7 @@ import { import type { Bridge, BridgeDocument, + Expression, Instruction, NodeRef, ToolContext, @@ -1099,116 +1102,44 @@ export class ExecutionTree implements TreeContext { wire: Wire, visited = new Set(), ): boolean { - if ("value" in wire) return true; - - if ("from" in wire) { - if (!this.canResolveRefWithoutScheduling(wire.from, visited)) { - return false; - } - for (const fallback of wire.fallbacks ?? []) { - if ( - fallback.ref && - !this.canResolveRefWithoutScheduling(fallback.ref, visited) - ) { - return false; - } - } - if ( - wire.catchFallbackRef && - !this.canResolveRefWithoutScheduling(wire.catchFallbackRef, visited) - ) { + // Check all source expressions + for (const source of wire.sources) { + if (!this.canResolveExprWithoutScheduling(source.expr, visited)) { return false; } - return true; } - - if ("cond" in wire) { - if (!this.canResolveRefWithoutScheduling(wire.cond, visited)) - return false; - if ( - wire.thenRef && - !this.canResolveRefWithoutScheduling(wire.thenRef, visited) - ) { - return false; - } - if ( - wire.elseRef && - !this.canResolveRefWithoutScheduling(wire.elseRef, visited) - ) { - return false; - } - for (const fallback of wire.fallbacks ?? []) { - if ( - fallback.ref && - !this.canResolveRefWithoutScheduling(fallback.ref, visited) - ) { - return false; - } - } - if ( - wire.catchFallbackRef && - !this.canResolveRefWithoutScheduling(wire.catchFallbackRef, visited) - ) { - return false; - } - return true; - } - - if ("condAnd" in wire) { - if (!this.canResolveRefWithoutScheduling(wire.condAnd.leftRef, visited)) { - return false; - } - if ( - wire.condAnd.rightRef && - !this.canResolveRefWithoutScheduling(wire.condAnd.rightRef, visited) - ) { - return false; - } - for (const fallback of wire.fallbacks ?? []) { - if ( - fallback.ref && - !this.canResolveRefWithoutScheduling(fallback.ref, visited) - ) { - return false; - } - } - if ( - wire.catchFallbackRef && - !this.canResolveRefWithoutScheduling(wire.catchFallbackRef, visited) - ) { + // Check catch handler ref + if (wire.catch && "ref" in wire.catch) { + if (!this.canResolveRefWithoutScheduling(wire.catch.ref, visited)) { return false; } - return true; } + return true; + } - if ("condOr" in wire) { - if (!this.canResolveRefWithoutScheduling(wire.condOr.leftRef, visited)) { - return false; - } - if ( - wire.condOr.rightRef && - !this.canResolveRefWithoutScheduling(wire.condOr.rightRef, visited) - ) { - return false; - } - for (const fallback of wire.fallbacks ?? []) { - if ( - fallback.ref && - !this.canResolveRefWithoutScheduling(fallback.ref, visited) - ) { - return false; - } - } - if ( - wire.catchFallbackRef && - !this.canResolveRefWithoutScheduling(wire.catchFallbackRef, visited) - ) { - return false; - } - return true; + private canResolveExprWithoutScheduling( + expr: Expression, + visited: Set, + ): boolean { + switch (expr.type) { + case "literal": + case "control": + return true; + case "ref": + return this.canResolveRefWithoutScheduling(expr.ref, visited); + case "ternary": + return ( + this.canResolveExprWithoutScheduling(expr.cond, visited) && + this.canResolveExprWithoutScheduling(expr.then, visited) && + this.canResolveExprWithoutScheduling(expr.else, visited) + ); + case "and": + case "or": + return ( + this.canResolveExprWithoutScheduling(expr.left, visited) && + this.canResolveExprWithoutScheduling(expr.right, visited) + ); } - - return false; } private canResolveRefWithoutScheduling( @@ -1329,30 +1260,28 @@ export class ExecutionTree implements TreeContext { ); // Separate spread wires from regular wires - const spreadWires = exactWires.filter( - (w) => "from" in w && "spread" in w && w.spread, - ); - const regularWires = exactWires.filter( - (w) => !("from" in w && "spread" in w && w.spread), - ); + const spreadWires = exactWires.filter((w) => isPullWire(w) && w.spread); + const regularWires = exactWires.filter((w) => !(isPullWire(w) && w.spread)); if (regularWires.length > 0) { // Check for array mapping: exact wires (the array source) PLUS // element-level wires deeper than prefix (the field mappings). // E.g. `o.entries <- src[] as x { .id <- x.item_id }` produces // an exact wire at ["entries"] and element wires at ["entries","id"]. - const hasElementWires = bridge.wires.some( - (w) => - "from" in w && - ((w.from as NodeRef).element === true || - this.isElementScopedTrunk(w.from as NodeRef) || + const hasElementWires = bridge.wires.some((w) => { + const ref = getPrimaryRef(w); + return ( + ref != null && + (ref.element === true || + this.isElementScopedTrunk(ref) || w.to.element === true) && w.to.module === SELF_MODULE && w.to.type === type && w.to.field === field && w.to.path.length > prefix.length && - prefix.every((seg, i) => w.to.path[i] === seg), - ); + prefix.every((seg, i) => w.to.path[i] === seg) + ); + }); if (hasElementWires) { // Array mapping on a sub-field: resolve the array source, @@ -1487,7 +1416,7 @@ export class ExecutionTree implements TreeContext { // Root wire (`o <- src`) — whole-object passthrough const hasRootWire = bridge.wires.some( (w) => - "from" in w && + isPullWire(w) && w.to.module === SELF_MODULE && w.to.type === type && w.to.field === field && @@ -1555,7 +1484,7 @@ export class ExecutionTree implements TreeContext { // Separate root-level wires into passthrough vs spread const rootWires = bridge.wires.filter( (w) => - "from" in w && + isPullWire(w) && w.to.module === SELF_MODULE && w.to.type === type && w.to.field === field && @@ -1564,13 +1493,11 @@ export class ExecutionTree implements TreeContext { // Passthrough wire: root wire without spread flag const hasPassthroughWire = rootWires.some( - (w) => "from" in w && !("spread" in w && w.spread), + (w) => isPullWire(w) && !w.spread, ); // Spread wires: root wires with spread flag - const spreadWires = rootWires.filter( - (w) => "from" in w && "spread" in w && w.spread, - ); + const spreadWires = rootWires.filter((w) => isPullWire(w) && !!w.spread); const hasRootWire = rootWires.length > 0; @@ -1579,16 +1506,18 @@ export class ExecutionTree implements TreeContext { // (`o <- api.user`) only has the root wire. // Pipe fork output wires in element context (e.g. concat template strings) // may have to.element === true instead. - const hasElementWires = bridge.wires.some( - (w) => - "from" in w && - ((w.from as NodeRef).element === true || - this.isElementScopedTrunk(w.from as NodeRef) || + const hasElementWires = bridge.wires.some((w) => { + const ref = getPrimaryRef(w); + return ( + ref != null && + (ref.element === true || + this.isElementScopedTrunk(ref) || w.to.element === true) && w.to.module === SELF_MODULE && w.to.type === type && - w.to.field === field, - ); + w.to.field === field + ); + }); if (hasRootWire && hasElementWires) { const [shadows] = await Promise.all([ @@ -1714,9 +1643,10 @@ export class ExecutionTree implements TreeContext { !array && matches.every( (w): boolean => - "from" in w && - w.from.module.startsWith("__define_out_") && - w.from.path.length === 0, + w.sources.length === 1 && + w.sources[0]!.expr.type === "ref" && + w.sources[0]!.expr.ref.module.startsWith("__define_out_") && + w.sources[0]!.expr.ref.path.length === 0, ) ) { return this; @@ -1728,16 +1658,17 @@ export class ExecutionTree implements TreeContext { // can pick up both spread properties and explicit wires. if ( !array && - matches.every( - (w): boolean => "from" in w && "spread" in w && !!w.spread, - ) + matches.every((w): boolean => isPullWire(w) && !!w.spread) ) { const spreadData = await this.resolveWires(matches); if (spreadData != null && typeof spreadData === "object") { const prefix = cleanPath.join("."); this.spreadCache ??= {}; if (prefix === "") { - Object.assign(this.spreadCache, spreadData as Record); + Object.assign( + this.spreadCache, + spreadData as Record, + ); } else { (this.spreadCache as Record)[prefix] = spreadData; } @@ -1758,18 +1689,20 @@ export class ExecutionTree implements TreeContext { // unnecessary — return the plain resolved array directly. if (scalar) { const { type, field } = this.trunk; - const hasElementWires = this.bridge?.wires.some( - (w) => - "from" in w && - ((w.from as NodeRef).element === true || - this.isElementScopedTrunk(w.from as NodeRef) || + const hasElementWires = this.bridge?.wires.some((w) => { + const ref = getPrimaryRef(w); + return ( + ref != null && + (ref.element === true || + this.isElementScopedTrunk(ref) || w.to.element === true) && w.to.module === SELF_MODULE && w.to.type === type && w.to.field === field && w.to.path.length > cleanPath.length && - cleanPath.every((seg, i) => w.to.path[i] === seg), - ); + cleanPath.every((seg, i) => w.to.path[i] === seg) + ); + }); if (!hasElementWires) { return response; } @@ -1818,7 +1751,9 @@ export class ExecutionTree implements TreeContext { const parentSpread = parentPrefix === "" ? this.spreadCache - : (this.spreadCache[parentPrefix] as Record | undefined); + : (this.spreadCache[parentPrefix] as + | Record + | undefined); if ( parentSpread != null && typeof parentSpread === "object" && @@ -1883,19 +1818,22 @@ export class ExecutionTree implements TreeContext { private findDefineFieldWires(cleanPath: string[]): Wire[] { const forwards = this.bridge?.wires.filter( - (w): w is Extract => - "from" in w && + (w): boolean => + w.sources.length === 1 && + w.sources[0]!.expr.type === "ref" && sameTrunk(w.to, this.trunk) && w.to.path.length === 0 && - w.from.module.startsWith("__define_out_") && - w.from.path.length === 0, + w.sources[0]!.expr.ref.module.startsWith("__define_out_") && + w.sources[0]!.expr.ref.path.length === 0, ) ?? []; if (forwards.length === 0) return []; const result: Wire[] = []; for (const fw of forwards) { - const defOutTrunk = fw.from; + const defOutTrunk = ( + fw.sources[0]!.expr as Extract + ).ref; const fieldWires = this.bridge?.wires.filter( (w) => diff --git a/packages/bridge-core/src/enumerate-traversals.ts b/packages/bridge-core/src/enumerate-traversals.ts index 78fa6067..274e301a 100644 --- a/packages/bridge-core/src/enumerate-traversals.ts +++ b/packages/bridge-core/src/enumerate-traversals.ts @@ -18,7 +18,7 @@ import type { Bridge, Wire, - WireFallback, + WireSourceEntry, NodeRef, ControlFlowInstruction, SourceLocation, @@ -79,15 +79,6 @@ function pathKey(path: string[]): string { return path.length > 0 ? path.join(".") : "*"; } -function hasCatch(w: Wire): boolean { - if ("value" in w) return false; - return ( - w.catchFallback != null || - w.catchFallbackRef != null || - w.catchControl != null - ); -} - /** * True when a NodeRef can throw at runtime — i.e. it targets a tool (or * pipe) call and is NOT root-safe (`?.`). @@ -123,11 +114,12 @@ function isPlainArraySourceWire( arrayIterators: Record | undefined, ): boolean { if (!arrayIterators) return false; - if (!("from" in w)) return false; - if (w.from.element) return false; + if (w.sources.length !== 1 || w.catch) return false; + const primary = w.sources[0]!.expr; + if (primary.type !== "ref" || primary.ref.element) return false; const targetPath = w.to.path.join("."); if (!(targetPath in arrayIterators)) return false; - return !w.fallbacks?.length && !hasCatch(w); + return true; } // ── Description helpers ──────────────────────────────────────────────────── @@ -190,22 +182,24 @@ function controlLabel(ctrl: ControlFlowInstruction): string { return `${ctrl.kind}${n}`; } -function fallbackDescription( - fb: WireFallback, +/** Generate a description string for a fallback source entry. */ +function sourceEntryDescription( + entry: WireSourceEntry, hmap: Map, ): string { - const gate = fb.type === "falsy" ? "||" : "??"; - if (fb.value != null) return `${gate} ${fb.value}`; - if (fb.ref) return `${gate} ${refLabel(fb.ref, hmap)}`; - if (fb.control) return `${gate} ${controlLabel(fb.control)}`; + const gate = entry.gate === "falsy" ? "||" : "??"; + const expr = entry.expr; + if (expr.type === "ref") return `${gate} ${refLabel(expr.ref, hmap)}`; + if (expr.type === "literal") return `${gate} ${expr.value}`; + if (expr.type === "control") return `${gate} ${controlLabel(expr.control)}`; return gate; } function catchDescription(w: Wire, hmap: Map): string { - if ("value" in w) return "catch"; - if (w.catchFallback != null) return `catch ${w.catchFallback}`; - if (w.catchFallbackRef) return `catch ${refLabel(w.catchFallbackRef, hmap)}`; - if (w.catchControl) return `catch ${controlLabel(w.catchControl)}`; + if (!w.catch) return "catch"; + if ("value" in w.catch) return `catch ${w.catch.value}`; + if ("ref" in w.catch) return `catch ${refLabel(w.catch.ref, hmap)}`; + if ("control" in w.catch) return `catch ${controlLabel(w.catch.control)}`; return "catch"; } @@ -221,9 +215,12 @@ function effectiveTarget(w: Wire): string[] { return w.to.path; } +/** Source location of the primary expression. */ function primaryLoc(w: Wire): SourceLocation | undefined { - if ("value" in w) return w.loc; - if ("from" in w) return w.fromLoc ?? w.loc; + const primary = w.sources[0]; + if (!primary) return w.loc; + const expr = primary.expr; + if (expr.type === "ref") return expr.refLoc ?? w.loc; return w.loc; } @@ -235,20 +232,19 @@ function addFallbackEntries( w: Wire, hmap: Map, ): void { - const fallbacks = "fallbacks" in w ? w.fallbacks : undefined; - if (!fallbacks) return; - for (let i = 0; i < fallbacks.length; i++) { + for (let i = 1; i < w.sources.length; i++) { + const entry = w.sources[i]!; entries.push({ - id: `${base}/fallback:${i}`, + id: `${base}/fallback:${i - 1}`, wireIndex, target, kind: "fallback", - fallbackIndex: i, - gateType: fallbacks[i].type, - bitIndex: -1, // assigned after enumeration - loc: fallbacks[i].loc, + fallbackIndex: i - 1, + gateType: entry.gate, + bitIndex: -1, + loc: entry.loc, wireLoc: w.loc, - description: fallbackDescription(fallbacks[i], hmap), + description: sourceEntryDescription(entry, hmap), }); } } @@ -261,14 +257,14 @@ function addCatchEntry( w: Wire, hmap: Map, ): void { - if (hasCatch(w)) { + if (w.catch) { entries.push({ id: `${base}/catch`, wireIndex, target, kind: "catch", bitIndex: -1, - loc: "catchLoc" in w ? w.catchLoc : undefined, + loc: w.catch.loc, wireLoc: w.loc, description: catchDescription(w, hmap), }); @@ -298,16 +294,10 @@ function addErrorEntries( wireSafe: boolean, elseRef?: NodeRef | undefined, ): void { - const wHasCatch = hasCatch(w); - - if (wHasCatch) { + if (w.catch) { // Catch absorbs source errors — only check if the catch source itself // can throw. - if ( - "catchFallbackRef" in w && - w.catchFallbackRef && - canRefError(w.catchFallbackRef) - ) { + if ("ref" in w.catch && canRefError(w.catch.ref)) { entries.push({ id: `${base}/catch/error`, wireIndex, @@ -315,7 +305,7 @@ function addErrorEntries( kind: "catch", error: true, bitIndex: -1, - loc: "catchLoc" in w ? w.catchLoc : undefined, + loc: w.catch.loc, wireLoc: w.loc, description: `${catchDescription(w, hmap)} error`, }); @@ -343,6 +333,9 @@ function addErrorEntries( // Else source (conditionals only) if (elseRef && canRefError(elseRef)) { + const primary = w.sources[0]?.expr; + const elseLoc = + primary?.type === "ternary" ? (primary.elseLoc ?? w.loc) : w.loc; entries.push({ id: `${base}/else/error`, wireIndex, @@ -350,31 +343,30 @@ function addErrorEntries( kind: "else", error: true, bitIndex: -1, - loc: "elseLoc" in w ? (w.elseLoc ?? w.loc) : w.loc, + loc: elseLoc, wireLoc: w.loc, description: `${refLabel(elseRef, hmap)} error`, }); } // Fallback sources - const fallbacks = "fallbacks" in w ? w.fallbacks : undefined; - if (fallbacks) { - for (let i = 0; i < fallbacks.length; i++) { - if (canRefError(fallbacks[i].ref)) { - entries.push({ - id: `${base}/fallback:${i}/error`, - wireIndex, - target, - kind: "fallback", - error: true, - fallbackIndex: i, - gateType: fallbacks[i].type, - bitIndex: -1, - loc: fallbacks[i].loc, - wireLoc: w.loc, - description: `${fallbackDescription(fallbacks[i], hmap)} error`, - }); - } + for (let i = 1; i < w.sources.length; i++) { + const entry = w.sources[i]!; + const fbRef = entry.expr.type === "ref" ? entry.expr.ref : undefined; + if (canRefError(fbRef)) { + entries.push({ + id: `${base}/fallback:${i - 1}/error`, + wireIndex, + target, + kind: "fallback", + error: true, + fallbackIndex: i - 1, + gateType: entry.gate, + bitIndex: -1, + loc: entry.loc, + wireLoc: w.loc, + description: `${sourceEntryDescription(entry, hmap)} error`, + }); } } } @@ -410,8 +402,12 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { targetCounts.set(tKey, seen + 1); const base = seen > 0 ? `${tKey}#${seen}` : tKey; + // ── Classify by primary expression type ──────────────────────── + const primary = w.sources[0]?.expr; + if (!primary) continue; + // ── Constant wire ─────────────────────────────────────────────── - if ("value" in w) { + if (primary.type === "literal" && w.sources.length === 1 && !w.catch) { entries.push({ id: `${base}/const`, wireIndex: i, @@ -420,13 +416,13 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { bitIndex: -1, loc: w.loc, wireLoc: w.loc, - description: `= ${w.value}`, + description: `= ${primary.value}`, }); continue; } - // ── Pull wire ─────────────────────────────────────────────────── - if ("from" in w) { + // ── Pull wire (ref primary) ───────────────────────────────────── + if (primary.type === "ref") { // Skip plain array source wires — they always execute and the // separate "empty-array" entry covers the "no elements" path. if (!isPlainArraySourceWire(w, bridge.arrayIterators)) { @@ -438,34 +434,47 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { bitIndex: -1, loc: primaryLoc(w), wireLoc: w.loc, - description: refLabel(w.from, hmap), + description: refLabel(primary.ref, hmap), }); addFallbackEntries(entries, base, i, target, w, hmap); addCatchEntry(entries, base, i, target, w, hmap); - addErrorEntries(entries, base, i, target, w, hmap, w.from, !!w.safe); + addErrorEntries( + entries, + base, + i, + target, + w, + hmap, + primary.ref, + !!primary.safe, + ); } continue; } // ── Conditional (ternary) wire ────────────────────────────────── - if ("cond" in w) { - const thenDesc = w.thenRef - ? `? ${refLabel(w.thenRef, hmap)}` - : w.thenValue != null - ? `? ${w.thenValue}` - : "then"; - const elseDesc = w.elseRef - ? `: ${refLabel(w.elseRef, hmap)}` - : w.elseValue != null - ? `: ${w.elseValue}` - : "else"; + if (primary.type === "ternary") { + const thenExpr = primary.then; + const elseExpr = primary.else; + const thenDesc = + thenExpr.type === "ref" + ? `? ${refLabel(thenExpr.ref, hmap)}` + : thenExpr.type === "literal" + ? `? ${thenExpr.value}` + : "then"; + const elseDesc = + elseExpr.type === "ref" + ? `: ${refLabel(elseExpr.ref, hmap)}` + : elseExpr.type === "literal" + ? `: ${elseExpr.value}` + : "else"; entries.push({ id: `${base}/then`, wireIndex: i, target, kind: "then", bitIndex: -1, - loc: w.thenLoc ?? w.loc, + loc: primary.thenLoc ?? w.loc, wireLoc: w.loc, description: thenDesc, }); @@ -475,12 +484,14 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { target, kind: "else", bitIndex: -1, - loc: w.elseLoc ?? w.loc, + loc: primary.elseLoc ?? w.loc, wireLoc: w.loc, description: elseDesc, }); addFallbackEntries(entries, base, i, target, w, hmap); addCatchEntry(entries, base, i, target, w, hmap); + const thenRef = thenExpr.type === "ref" ? thenExpr.ref : undefined; + const elseRef = elseExpr.type === "ref" ? elseExpr.ref : undefined; addErrorEntries( entries, base, @@ -488,20 +499,27 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { target, w, hmap, - w.thenRef, + thenRef, false, - w.elseRef, + elseRef, ); continue; } // ── condAnd / condOr (logical binary) ─────────────────────────── - if ("condAnd" in w) { - const desc = w.condAnd.rightRef - ? `${refLabel(w.condAnd.leftRef, hmap)} && ${refLabel(w.condAnd.rightRef, hmap)}` - : w.condAnd.rightValue != null - ? `${refLabel(w.condAnd.leftRef, hmap)} && ${w.condAnd.rightValue}` - : refLabel(w.condAnd.leftRef, hmap); + if (primary.type === "and" || primary.type === "or") { + const leftRef = + primary.left.type === "ref" ? primary.left.ref : undefined; + const rightExpr = primary.right; + const op = primary.type === "and" ? "&&" : "||"; + const leftLabel = leftRef ? refLabel(leftRef, hmap) : "?"; + const rightLabel = + rightExpr.type === "ref" + ? refLabel(rightExpr.ref, hmap) + : rightExpr.type === "literal" && rightExpr.value !== "true" + ? rightExpr.value + : undefined; + const desc = rightLabel ? `${leftLabel} ${op} ${rightLabel}` : leftLabel; entries.push({ id: `${base}/primary`, wireIndex: i, @@ -521,40 +539,24 @@ export function enumerateTraversalIds(bridge: Bridge): TraversalEntry[] { target, w, hmap, - w.condAnd.leftRef, - !!w.condAnd.safe, - ); - } else { - // condOr - const wo = w as Extract; - const desc = wo.condOr.rightRef - ? `${refLabel(wo.condOr.leftRef, hmap)} || ${refLabel(wo.condOr.rightRef, hmap)}` - : wo.condOr.rightValue != null - ? `${refLabel(wo.condOr.leftRef, hmap)} || ${wo.condOr.rightValue}` - : refLabel(wo.condOr.leftRef, hmap); - entries.push({ - id: `${base}/primary`, - wireIndex: i, - target, - kind: "primary", - bitIndex: -1, - loc: primaryLoc(wo), - wireLoc: wo.loc, - description: desc, - }); - addFallbackEntries(entries, base, i, target, wo, hmap); - addCatchEntry(entries, base, i, target, w, hmap); - addErrorEntries( - entries, - base, - i, - target, - w, - hmap, - wo.condOr.leftRef, - !!wo.condOr.safe, + leftRef, + !!primary.leftSafe, ); + continue; } + + // ── Other expression types (control, literal with catch/fallbacks) ── + entries.push({ + id: `${base}/primary`, + wireIndex: i, + target, + kind: "primary", + bitIndex: -1, + loc: w.loc, + wireLoc: w.loc, + }); + addFallbackEntries(entries, base, i, target, w, hmap); + addCatchEntry(entries, base, i, target, w, hmap); } // ── Array iterators — each scope adds an "empty-array" path ───── diff --git a/packages/bridge-core/src/index.ts b/packages/bridge-core/src/index.ts index 57d62ed6..d6402f7d 100644 --- a/packages/bridge-core/src/index.ts +++ b/packages/bridge-core/src/index.ts @@ -69,6 +69,7 @@ export type { ConstDef, ControlFlowInstruction, DefineDef, + Expression, HandleBinding, Instruction, NodeRef, @@ -82,9 +83,19 @@ export type { ToolMetadata, VersionDecl, Wire, - WireFallback, + WireCatch, + WireSourceEntry, } from "./types.ts"; +// ── Wire resolution ───────────────────────────────────────────────────────── + +export { + evaluateExpression, + resolveSourceEntries, + applyFallbackGates as applyFallbackGatesV2, + applyCatch as applyCatchV2, +} from "./resolveWiresSources.ts"; + // ── Traversal enumeration ─────────────────────────────────────────────────── export { diff --git a/packages/bridge-core/src/resolveWires.ts b/packages/bridge-core/src/resolveWires.ts index 6ec11f26..dfab5d2e 100644 --- a/packages/bridge-core/src/resolveWires.ts +++ b/packages/bridge-core/src/resolveWires.ts @@ -9,33 +9,12 @@ * the full `ExecutionTree` class. */ -import type { ControlFlowInstruction, NodeRef, Wire } from "./types.ts"; -import type { - LoopControlSignal, - MaybePromise, - TreeContext, -} from "./tree-types.ts"; -import { - attachBridgeErrorMetadata, - isFatalError, - isPromise, - applyControlFlow, - BridgeAbortError, - BridgePanicError, - wrapBridgeRuntimeError, -} from "./tree-types.ts"; +import type { Wire } from "./types.ts"; +import type { MaybePromise, TreeContext } from "./tree-types.ts"; +import { isFatalError, BridgeAbortError } from "./tree-types.ts"; import { coerceConstant, getSimplePullRef } from "./tree-utils.ts"; import type { TraceWireBits } from "./enumerate-traversals.ts"; - -// ── Wire type helpers ──────────────────────────────────────────────────────── - -/** - * A non-constant wire — any Wire variant that carries gate modifiers - * (`fallbacks`, `catchFallback`, etc.). - * Excludes the `{ value: string; to: NodeRef }` constant wire which has no - * modifier slots. - */ -type WireWithGates = Exclude; +import { resolveSourceEntries } from "./resolveWiresSources.ts"; // ── Public entry point ────────────────────────────────────────────────────── @@ -44,7 +23,7 @@ type WireWithGates = Exclude; * * Architecture: two distinct resolution axes — * - * **Fallback Gates** (`||` / `??`, within a wire): unified `fallbacks` array + * **Fallback Gates** (`||` / `??`, within a wire): ordered source entries * → falsy gates trigger on falsy values (0, "", false, null, undefined) * → nullish gates trigger only on null/undefined * → gates are processed left-to-right, allowing mixed `||` and `??` chains @@ -52,13 +31,8 @@ type WireWithGates = Exclude; * **Overdefinition** (across wires): multiple wires target the same path * → nullish check — only null/undefined falls through to the next wire. * - * Per-wire layers: - * Layer 1 — Execution (pullSingle + safe modifier) - * Layer 2 — Fallback Gates (unified fallbacks array: || and ?? in order) - * Layer 3 — Catch (catchFallbackRef / catchFallback / catchControl) - * - * After layers 1–2, the overdefinition boundary (`!= null`) decides whether - * to return or continue to the next wire. + * Resolution is handled by `resolveSourceEntries()` from resolveWiresSources.ts, + * which evaluates source entries in order with their gates and catch handler. * * --- * @@ -77,9 +51,14 @@ export function resolveWires( if (wires.length === 1) { const w = wires[0]!; - if ("value" in w) { + // Constant wire — single literal source, no catch + if ( + w.sources.length === 1 && + w.sources[0]!.expr.type === "literal" && + !w.catch + ) { recordPrimary(ctx, w); - return coerceConstant(w.value); + return coerceConstant(w.sources[0]!.expr.value); } const ref = getSimplePullRef(w); if ( @@ -87,11 +66,9 @@ export function resolveWires( (ctx.traceBits?.get(w) as TraceWireBits | undefined)?.primaryError == null ) { recordPrimary(ctx, w); - return ctx.pullSingle( - ref, - pullChain, - "from" in w ? (w.fromLoc ?? w.loc) : w.loc, - ); + const expr = w.sources[0]!.expr; + const refLoc = expr.type === "ref" ? (expr.refLoc ?? w.loc) : w.loc; + return ctx.pullSingle(ref, pullChain, refLoc); } } const orderedWires = orderOverdefinedWires(ctx, wires); @@ -132,31 +109,27 @@ async function resolveWiresAsync( // Abort discipline — yield immediately if client disconnected if (ctx.signal?.aborted) throw new BridgeAbortError(); - // Constant wire — always wins, no modifiers - if ("value" in w) { + // Constant wire — single literal source, no catch + if ( + w.sources.length === 1 && + w.sources[0]!.expr.type === "literal" && + !w.catch + ) { recordPrimary(ctx, w); - return coerceConstant(w.value); + return coerceConstant(w.sources[0]!.expr.value); } - try { - // Layer 1: Execution - let value = await evaluateWireSource(ctx, w, pullChain); + // Delegate to the unified source-loop resolver + const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - // Layer 2: Fallback Gates (unified || and ?? chain) - value = await applyFallbackGates(ctx, w, value, pullChain); + try { + const value = await resolveSourceEntries(ctx, w, pullChain, bits); // Overdefinition Boundary if (value != null) return value; } catch (err: unknown) { - // Layer 3: Catch Gate if (isFatalError(err)) throw err; - - const recoveredValue = await applyCatchGate(ctx, w, pullChain); - if (recoveredValue !== undefined) return recoveredValue; - - lastError = wrapBridgeRuntimeError(err, { - bridgeLoc: w.loc, - }); + lastError = err; } } @@ -164,261 +137,6 @@ async function resolveWiresAsync( return undefined; } -// ── Layer 2: Fallback Gates (unified || and ??) ───────────────────────────── - -/** - * Apply the unified Fallback Gates (Layer 2) to a resolved value. - * - * Walks the `fallbacks` array in order. Each entry is either a falsy gate - * (`||`) or a nullish gate (`??`). A falsy gate opens when `!value`; - * a nullish gate opens when `value == null`. When a gate is open, the - * fallback is applied (control flow, ref pull, or constant coercion) and - * the result replaces `value` for subsequent gates. - */ -export async function applyFallbackGates( - ctx: TreeContext, - w: WireWithGates, - value: unknown, - pullChain?: Set, -): Promise { - if (!w.fallbacks?.length) return value; - - for ( - let fallbackIndex = 0; - fallbackIndex < w.fallbacks.length; - fallbackIndex++ - ) { - const fallback = w.fallbacks[fallbackIndex]; - const isFalsyGateOpen = fallback.type === "falsy" && !value; - const isNullishGateOpen = fallback.type === "nullish" && value == null; - - if (isFalsyGateOpen || isNullishGateOpen) { - recordFallback(ctx, w, fallbackIndex); - if (fallback.control) { - return applyControlFlowWithLoc(fallback.control, fallback.loc ?? w.loc); - } - if (fallback.ref) { - try { - value = await ctx.pullSingle( - fallback.ref, - pullChain, - fallback.loc ?? w.loc, - ); - } catch (err: any) { - recordFallbackError(ctx, w, fallbackIndex); - throw err; - } - } else if (fallback.value !== undefined) { - value = coerceConstant(fallback.value); - } - } - } - - return value; -} - -// ── Layer 3: Catch Gate ────────────────────────────────────────────────────── - -/** - * Apply the Catch Gate (Layer 3) after an error has been thrown by the - * execution layer. - * - * Returns the recovered value if the wire supplies a catch handler, or - * `undefined` if the error should be stored as `lastError` so the loop can - * continue to the next wire. - */ -export async function applyCatchGate( - ctx: TreeContext, - w: WireWithGates, - pullChain?: Set, -): Promise { - if (w.catchControl) { - recordCatch(ctx, w); - return applyControlFlowWithLoc(w.catchControl, w.catchLoc ?? w.loc); - } - if (w.catchFallbackRef) { - recordCatch(ctx, w); - try { - return await ctx.pullSingle( - w.catchFallbackRef, - pullChain, - w.catchLoc ?? w.loc, - ); - } catch (err: any) { - recordCatchError(ctx, w); - throw err; - } - } - if (w.catchFallback != null) { - recordCatch(ctx, w); - return coerceConstant(w.catchFallback); - } - return undefined; -} - -function applyControlFlowWithLoc( - control: ControlFlowInstruction, - bridgeLoc: Wire["loc"], -): symbol | LoopControlSignal { - try { - return applyControlFlow(control); - } catch (err) { - if (err instanceof BridgePanicError) { - throw attachBridgeErrorMetadata(err, { - bridgeLoc, - }); - } - if (isFatalError(err)) throw err; - throw wrapBridgeRuntimeError(err, { - bridgeLoc, - }); - } -} - -// ── Layer 1: Wire source evaluation ───────────────────────────────────────── - -/** - * Evaluate the primary value of a wire (Layer 1) — the `from`, `cond`, - * `condAnd`, or `condOr` portion, before any fallback gates are applied. - * - * Returns the raw resolved value (or `undefined` if the wire variant is - * unrecognised). - */ -async function evaluateWireSource( - ctx: TreeContext, - w: Wire, - pullChain?: Set, -): Promise { - if ("cond" in w) { - const condValue = await ctx.pullSingle( - w.cond, - pullChain, - w.condLoc ?? w.loc, - ); - if (condValue) { - recordPrimary(ctx, w); // "then" branch → primary bit - if (w.thenRef !== undefined) { - try { - return await ctx.pullSingle(w.thenRef, pullChain, w.thenLoc ?? w.loc); - } catch (err: any) { - recordPrimaryError(ctx, w); - throw err; - } - } - if (w.thenValue !== undefined) return coerceConstant(w.thenValue); - } else { - recordElse(ctx, w); // "else" branch - if (w.elseRef !== undefined) { - try { - return await ctx.pullSingle(w.elseRef, pullChain, w.elseLoc ?? w.loc); - } catch (err: any) { - recordElseError(ctx, w); - throw err; - } - } - if (w.elseValue !== undefined) return coerceConstant(w.elseValue); - } - return undefined; - } - - if ("condAnd" in w) { - recordPrimary(ctx, w); - const { leftRef, rightRef, rightValue, safe, rightSafe } = w.condAnd; - try { - const leftVal = await pullSafe(ctx, leftRef, safe, pullChain, w.loc); - if (!leftVal) return false; - if (rightRef !== undefined) - return Boolean( - await pullSafe(ctx, rightRef, rightSafe, pullChain, w.loc), - ); - if (rightValue !== undefined) return Boolean(coerceConstant(rightValue)); - return Boolean(leftVal); - } catch (err: any) { - recordPrimaryError(ctx, w); - throw err; - } - } - - if ("condOr" in w) { - recordPrimary(ctx, w); - const { leftRef, rightRef, rightValue, safe, rightSafe } = w.condOr; - try { - const leftVal = await pullSafe(ctx, leftRef, safe, pullChain, w.loc); - if (leftVal) return true; - if (rightRef !== undefined) - return Boolean( - await pullSafe(ctx, rightRef, rightSafe, pullChain, w.loc), - ); - if (rightValue !== undefined) return Boolean(coerceConstant(rightValue)); - return Boolean(leftVal); - } catch (err: any) { - recordPrimaryError(ctx, w); - throw err; - } - } - - if ("from" in w) { - recordPrimary(ctx, w); - if (w.safe) { - try { - return await ctx.pullSingle(w.from, pullChain, w.fromLoc ?? w.loc); - } catch (err: any) { - if (isFatalError(err)) throw err; - return undefined; - } - } - try { - return await ctx.pullSingle(w.from, pullChain, w.fromLoc ?? w.loc); - } catch (err: any) { - recordPrimaryError(ctx, w); - throw err; - } - } - - return undefined; -} - -// ── Safe-navigation helper ────────────────────────────────────────────────── - -/** - * Pull a ref with optional safe-navigation: catches non-fatal errors and - * returns `undefined` instead. Used by condAnd / condOr evaluation. - * Returns `MaybePromise` so synchronous pulls skip microtask scheduling. - */ -function pullSafe( - ctx: TreeContext, - ref: NodeRef, - safe: boolean | undefined, - pullChain?: Set, - bridgeLoc?: Wire["loc"], -): MaybePromise { - // FAST PATH: Unsafe wires bypass the try/catch overhead entirely - if (!safe) { - return ctx.pullSingle(ref, pullChain, bridgeLoc); - } - - // SAFE PATH: We must catch synchronous throws during the invocation - let pull: any; - try { - pull = ctx.pullSingle(ref, pullChain, bridgeLoc); - } catch (e: any) { - // Caught a synchronous error! - if (isFatalError(e)) throw e; - return undefined; - } - - // If the result was synchronous and didn't throw, we just return it - if (!isPromise(pull)) { - return pull; - } - - // If the result is a Promise, we must catch asynchronous rejections - return pull.catch((e: any) => { - if (isFatalError(e)) throw e; - return undefined; - }); -} - // ── Trace recording helpers ───────────────────────────────────────────────── // These are designed for minimal overhead: when `traceBits` is not set on the // context (tracing disabled), the functions return immediately after a single @@ -431,43 +149,3 @@ function recordPrimary(ctx: TreeContext, w: Wire): void { const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; if (bits?.primary != null) ctx.traceMask![0] |= 1n << BigInt(bits.primary); } - -function recordElse(ctx: TreeContext, w: Wire): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.else != null) ctx.traceMask![0] |= 1n << BigInt(bits.else); -} - -function recordFallback(ctx: TreeContext, w: Wire, index: number): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - const fb = bits?.fallbacks; - if (fb && fb[index] != null) ctx.traceMask![0] |= 1n << BigInt(fb[index]); -} - -function recordCatch(ctx: TreeContext, w: Wire): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.catch != null) ctx.traceMask![0] |= 1n << BigInt(bits.catch); -} - -function recordPrimaryError(ctx: TreeContext, w: Wire): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.primaryError != null) - ctx.traceMask![0] |= 1n << BigInt(bits.primaryError); -} - -function recordElseError(ctx: TreeContext, w: Wire): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.elseError != null) - ctx.traceMask![0] |= 1n << BigInt(bits.elseError); -} - -function recordFallbackError(ctx: TreeContext, w: Wire, index: number): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - const fb = bits?.fallbackErrors; - if (fb && fb[index] != null) ctx.traceMask![0] |= 1n << BigInt(fb[index]); -} - -function recordCatchError(ctx: TreeContext, w: Wire): void { - const bits = ctx.traceBits?.get(w) as TraceWireBits | undefined; - if (bits?.catchError != null) - ctx.traceMask![0] |= 1n << BigInt(bits.catchError); -} diff --git a/packages/bridge-core/src/resolveWiresSources.ts b/packages/bridge-core/src/resolveWiresSources.ts new file mode 100644 index 00000000..52d41174 --- /dev/null +++ b/packages/bridge-core/src/resolveWiresSources.ts @@ -0,0 +1,491 @@ +/** + * Wire resolution — unified source-loop evaluation. + * + * Evaluates `Wire.sources[]` in order with their fallback gates and + * optional catch handler. Called from `resolveWires.ts` for the + * async resolution path and overdefinition handling. + */ + +import type { + ControlFlowInstruction, + Expression, + NodeRef, + WireCatch, + Wire, +} from "./types.ts"; +import type { + LoopControlSignal, + MaybePromise, + TreeContext, +} from "./tree-types.ts"; +import { + attachBridgeErrorMetadata, + isFatalError, + isPromise, + applyControlFlow, + BridgeAbortError, + BridgePanicError, + wrapBridgeRuntimeError, +} from "./tree-types.ts"; +import { coerceConstant } from "./tree-utils.ts"; +import type { TraceWireBits } from "./enumerate-traversals.ts"; +import type { SourceLocation } from "@stackables/bridge-types"; + +// ── Public entry points ───────────────────────────────────────────────────── + +/** + * Evaluate a recursive Expression tree to a single value. + * + * Any expression that can appear in a source entry (ref, literal, ternary, + * and/or, control) is recursively resolved here. + */ +export function evaluateExpression( + ctx: TreeContext, + expr: Expression, + pullChain?: Set, +): MaybePromise { + switch (expr.type) { + case "ref": + if (expr.safe) { + return pullSafe(ctx, expr.ref, pullChain, expr.refLoc ?? expr.loc); + } + return ctx.pullSingle(expr.ref, pullChain, expr.refLoc ?? expr.loc); + + case "literal": + return coerceConstant(expr.value); + + case "control": + return applyControlFlowWithLoc(expr.control, expr.loc); + + case "ternary": + return evaluateTernary(ctx, expr, pullChain); + + case "and": + return evaluateAnd(ctx, expr, pullChain); + + case "or": + return evaluateOr(ctx, expr, pullChain); + } +} + +/** + * Resolve a single Wire — evaluate its ordered source entries with + * gate semantics, then apply the catch handler on error. + * + * Returns the resolved value, or throws if all sources fail and no catch + * handler recovers. + * + * @param bits — Optional pre-resolved trace bits for this wire. + */ +export async function resolveSourceEntries( + ctx: TreeContext, + w: Wire, + pullChain?: Set, + bits?: TraceWireBits, +): Promise { + if (ctx.signal?.aborted) throw new BridgeAbortError(); + + try { + let value: unknown; + for (let i = 0; i < w.sources.length; i++) { + const entry = w.sources[i]!; + + // Gate check: skip this entry if its gate is not open + if (i > 0 && entry.gate) { + const gateOpen = entry.gate === "falsy" ? !value : value == null; + if (!gateOpen) continue; + } + + // Evaluate the expression — ternary at primary position needs + // branch-specific trace recording (then → primary, else → else) + if (i === 0 && entry.expr.type === "ternary" && bits?.else != null) { + try { + value = await evaluateTernaryWithTrace( + ctx, + entry.expr, + pullChain, + bits, + ); + } catch (err: unknown) { + if (isFatalError(err)) throw err; + // Error bit was already recorded by evaluateTernaryWithTrace + throw err; + } + } else { + // Record which source was evaluated + recordSourceBit(ctx, bits, i); + + // Evaluate the expression + try { + value = await evaluateExpression(ctx, entry.expr, pullChain); + } catch (err: unknown) { + if (isFatalError(err)) throw err; + recordSourceErrorBit(ctx, bits, i); + throw err; + } + } + } + + return value; + } catch (err: unknown) { + if (isFatalError(err)) throw err; + + // Try catch handler + if (w.catch) { + const recovered = await applyCatchHandler(ctx, w.catch, pullChain, bits); + if (recovered !== undefined) return recovered; + } + + throw wrapBridgeRuntimeError(err, { bridgeLoc: w.loc }); + } +} + +/** + * Apply fallback gates to a pre-evaluated value. + * + * Iterates over `w.sources[1..]`, applying gate checks (falsy `||` or + * nullish `??`). Falls through to the next source entry when the gate opens. + */ +export async function applyFallbackGates( + ctx: TreeContext, + w: Wire, + value: unknown, + pullChain?: Set, + bits?: TraceWireBits, +): Promise { + if (w.sources.length <= 1) return value; + + for (let i = 1; i < w.sources.length; i++) { + const entry = w.sources[i]!; + + // Gate check + const gateOpen = entry.gate === "falsy" ? !value : value == null; + if (!gateOpen) continue; + + // Record fallback — uses the "fallback" index (i - 1) for backward + // compatibility with TraceWireBits.fallbacks[] + const fallbackIndex = i - 1; + recordFallbackBit(ctx, bits, fallbackIndex); + + // Evaluate the expression + if (entry.expr.type === "control") { + return applyControlFlowWithLoc(entry.expr.control, entry.loc ?? w.loc); + } + + if (entry.expr.type === "ref") { + try { + value = await ctx.pullSingle( + entry.expr.ref, + pullChain, + entry.loc ?? w.loc, + ); + } catch (err: any) { + recordFallbackErrorBit(ctx, bits, fallbackIndex); + throw err; + } + } else if (entry.expr.type === "literal") { + value = coerceConstant(entry.expr.value); + } else { + // Complex expression in fallback position + try { + value = await evaluateExpression(ctx, entry.expr, pullChain); + } catch (err: any) { + recordFallbackErrorBit(ctx, bits, fallbackIndex); + throw err; + } + } + } + + return value; +} + +/** + * Apply the wire's catch handler. + * + * Returns the recovered value, or `undefined` if no catch handler is + * configured (indicating the error should propagate). + */ +export async function applyCatch( + ctx: TreeContext, + w: Wire, + pullChain?: Set, + bits?: TraceWireBits, +): Promise { + if (!w.catch) return undefined; + return applyCatchHandler(ctx, w.catch, pullChain, bits); +} + +// ── Internal helpers ──────────────────────────────────────────────────────── + +async function applyCatchHandler( + ctx: TreeContext, + c: WireCatch, + pullChain?: Set, + bits?: TraceWireBits, +): Promise { + recordCatchBit(ctx, bits); + if ("control" in c) { + return applyControlFlowWithLoc(c.control, c.loc); + } + if ("ref" in c) { + try { + return await ctx.pullSingle(c.ref, pullChain, c.loc); + } catch (err: any) { + recordCatchErrorBit(ctx, bits); + throw err; + } + } + return coerceConstant(c.value); +} + +async function evaluateTernary( + ctx: TreeContext, + expr: Extract, + pullChain?: Set, +): Promise { + const condValue = await evaluateExpression(ctx, expr.cond, pullChain); + if (condValue) { + return evaluateExpression(ctx, expr.then, pullChain); + } + return evaluateExpression(ctx, expr.else, pullChain); +} + +/** + * Evaluate a ternary expression with branch-specific trace recording. + * + * Used by `resolveSourceEntries` when the primary source is a ternary and + * the trace bits distinguish then/else branches. + */ +async function evaluateTernaryWithTrace( + ctx: TreeContext, + expr: Extract, + pullChain: Set | undefined, + bits: TraceWireBits, +): Promise { + const condValue = await evaluateExpression(ctx, expr.cond, pullChain); + if (condValue) { + recordSourceBit(ctx, bits, 0); // "then" → primary bit + try { + return await evaluateExpression(ctx, expr.then, pullChain); + } catch (err: unknown) { + if (isFatalError(err)) throw err; + recordSourceErrorBit(ctx, bits, 0); + throw err; + } + } else { + recordElseBit(ctx, bits); + try { + return await evaluateExpression(ctx, expr.else, pullChain); + } catch (err: unknown) { + if (isFatalError(err)) throw err; + recordElseErrorBit(ctx, bits); + throw err; + } + } +} + +async function evaluateAnd( + ctx: TreeContext, + expr: Extract, + pullChain?: Set, +): Promise { + const leftVal = await evaluateExprSafe( + ctx, + expr.left, + expr.leftSafe, + pullChain, + ); + if (!leftVal) return false; + if (expr.right.type === "literal" && expr.right.value === "true") { + return Boolean(leftVal); + } + const rightVal = await evaluateExprSafe( + ctx, + expr.right, + expr.rightSafe, + pullChain, + ); + return Boolean(rightVal); +} + +async function evaluateOr( + ctx: TreeContext, + expr: Extract, + pullChain?: Set, +): Promise { + const leftVal = await evaluateExprSafe( + ctx, + expr.left, + expr.leftSafe, + pullChain, + ); + if (leftVal) return true; + if (expr.right.type === "literal" && expr.right.value === "true") { + return Boolean(leftVal); + } + const rightVal = await evaluateExprSafe( + ctx, + expr.right, + expr.rightSafe, + pullChain, + ); + return Boolean(rightVal); +} + +/** + * Evaluate an expression with optional safe navigation — catches non-fatal + * errors and returns `undefined`. + */ +function evaluateExprSafe( + ctx: TreeContext, + expr: Expression, + safe: boolean | undefined, + pullChain?: Set, +): MaybePromise { + if (!safe) return evaluateExpression(ctx, expr, pullChain); + + let result: any; + try { + result = evaluateExpression(ctx, expr, pullChain); + } catch (e: any) { + if (isFatalError(e)) throw e; + return undefined; + } + if (!isPromise(result)) return result; + return result.catch((e: any) => { + if (isFatalError(e)) throw e; + return undefined; + }); +} + +function applyControlFlowWithLoc( + control: ControlFlowInstruction, + bridgeLoc: SourceLocation | undefined, +): symbol | LoopControlSignal { + try { + return applyControlFlow(control); + } catch (err) { + if (err instanceof BridgePanicError) { + throw attachBridgeErrorMetadata(err, { bridgeLoc }); + } + if (isFatalError(err)) throw err; + throw wrapBridgeRuntimeError(err, { bridgeLoc }); + } +} + +/** + * Pull a ref with optional safe-navigation. + */ +function pullSafe( + ctx: TreeContext, + ref: NodeRef, + pullChain?: Set, + bridgeLoc?: SourceLocation, +): MaybePromise { + let pull: any; + try { + pull = ctx.pullSingle(ref, pullChain, bridgeLoc); + } catch (e: any) { + if (isFatalError(e)) throw e; + return undefined; + } + if (!isPromise(pull)) return pull; + return pull.catch((e: any) => { + if (isFatalError(e)) throw e; + return undefined; + }); +} + +// ── Trace recording helpers ───────────────────────────────────────────────── +// +// Operate on TraceWireBits passed directly by the caller. Minimal overhead: +// when `bits` is undefined (tracing disabled), the functions return after +// a single falsy check. + +function recordSourceBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, + index: number, +): void { + if (!bits || !ctx.traceMask) return; + if (index === 0) { + if (bits.primary != null) ctx.traceMask[0] |= 1n << BigInt(bits.primary); + } else { + const fb = bits.fallbacks; + const fbIndex = index - 1; + if (fb && fb[fbIndex] != null) + ctx.traceMask[0] |= 1n << BigInt(fb[fbIndex]); + } +} + +function recordSourceErrorBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, + index: number, +): void { + if (!bits || !ctx.traceMask) return; + if (index === 0) { + if (bits.primaryError != null) + ctx.traceMask[0] |= 1n << BigInt(bits.primaryError); + } else { + const fb = bits.fallbackErrors; + const fbIndex = index - 1; + if (fb && fb[fbIndex] != null) + ctx.traceMask[0] |= 1n << BigInt(fb[fbIndex]); + } +} + +function recordFallbackBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, + fallbackIndex: number, +): void { + if (!bits || !ctx.traceMask) return; + const fb = bits.fallbacks; + if (fb && fb[fallbackIndex] != null) + ctx.traceMask[0] |= 1n << BigInt(fb[fallbackIndex]); +} + +function recordFallbackErrorBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, + fallbackIndex: number, +): void { + if (!bits || !ctx.traceMask) return; + const fb = bits.fallbackErrors; + if (fb && fb[fallbackIndex] != null) + ctx.traceMask[0] |= 1n << BigInt(fb[fallbackIndex]); +} + +function recordCatchBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, +): void { + if (!bits || !ctx.traceMask) return; + if (bits.catch != null) ctx.traceMask[0] |= 1n << BigInt(bits.catch); +} + +function recordCatchErrorBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, +): void { + if (!bits || !ctx.traceMask) return; + if (bits.catchError != null) + ctx.traceMask[0] |= 1n << BigInt(bits.catchError); +} + +function recordElseBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, +): void { + if (!bits || !ctx.traceMask) return; + if (bits.else != null) ctx.traceMask[0] |= 1n << BigInt(bits.else); +} + +function recordElseErrorBit( + ctx: TreeContext, + bits: TraceWireBits | undefined, +): void { + if (!bits || !ctx.traceMask) return; + if (bits.elseError != null) ctx.traceMask[0] |= 1n << BigInt(bits.elseError); +} diff --git a/packages/bridge-core/src/scheduleTools.ts b/packages/bridge-core/src/scheduleTools.ts index 697e19e8..6953e712 100644 --- a/packages/bridge-core/src/scheduleTools.ts +++ b/packages/bridge-core/src/scheduleTools.ts @@ -8,7 +8,7 @@ * keeping the dependency surface explicit. */ -import type { Bridge, NodeRef, ToolDef, Wire } from "./types.ts"; +import type { Bridge, Expression, NodeRef, ToolDef, Wire } from "./types.ts"; import { SELF_MODULE } from "./types.ts"; import { isPromise, wrapBridgeRuntimeError } from "./tree-types.ts"; import type { MaybePromise, Trunk } from "./tree-types.ts"; @@ -72,47 +72,34 @@ function getToolName(target: Trunk): string { function refsInWire(wire: Wire): NodeRef[] { const refs: NodeRef[] = []; - - if ("from" in wire) { - refs.push(wire.from); - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref) refs.push(fallback.ref); - } - if (wire.catchFallbackRef) refs.push(wire.catchFallbackRef); - return refs; + // Collect refs from all source expressions + for (const source of wire.sources) { + collectExprRefs(source.expr, refs); } - - if ("cond" in wire) { - refs.push(wire.cond); - if (wire.thenRef) refs.push(wire.thenRef); - if (wire.elseRef) refs.push(wire.elseRef); - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref) refs.push(fallback.ref); - } - if (wire.catchFallbackRef) refs.push(wire.catchFallbackRef); - return refs; - } - - if ("condAnd" in wire) { - refs.push(wire.condAnd.leftRef); - if (wire.condAnd.rightRef) refs.push(wire.condAnd.rightRef); - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref) refs.push(fallback.ref); - } - if (wire.catchFallbackRef) refs.push(wire.catchFallbackRef); - return refs; + // Collect ref from catch handler + if (wire.catch && "ref" in wire.catch) { + refs.push(wire.catch.ref); } + return refs; +} - if ("condOr" in wire) { - refs.push(wire.condOr.leftRef); - if (wire.condOr.rightRef) refs.push(wire.condOr.rightRef); - for (const fallback of wire.fallbacks ?? []) { - if (fallback.ref) refs.push(fallback.ref); - } - if (wire.catchFallbackRef) refs.push(wire.catchFallbackRef); +function collectExprRefs(expr: Expression, refs: NodeRef[]): void { + switch (expr.type) { + case "ref": + refs.push(expr.ref); + break; + case "ternary": + collectExprRefs(expr.cond, refs); + collectExprRefs(expr.then, refs); + collectExprRefs(expr.else, refs); + break; + case "and": + case "or": + collectExprRefs(expr.left, refs); + collectExprRefs(expr.right, refs); + break; + // literal, control — no refs } - - return refs; } export function trunkDependsOnElement( @@ -392,7 +379,13 @@ export async function scheduleToolDef( const memoizeKey = ctx.memoizedToolKeys.has(trunkKey(target)) ? trunkKey(target) : undefined; - const raw = await ctx.callTool(toolName, toolDef.fn!, fn, input, memoizeKey); + const raw = await ctx.callTool( + toolName, + toolDef.fn!, + fn, + input, + memoizeKey, + ); return mergeToolDefConstants(toolDef, raw); } catch (err) { if (!toolDef.onError) throw err; diff --git a/packages/bridge-core/src/toolLookup.ts b/packages/bridge-core/src/toolLookup.ts index 201ee947..97908b8d 100644 --- a/packages/bridge-core/src/toolLookup.ts +++ b/packages/bridge-core/src/toolLookup.ts @@ -206,37 +206,33 @@ export function resolveToolDefByName( /** * Resolve a tool definition's wires into a nested input object. - * Wires use the unified Wire type — constant wires set fixed values, - * pull wires resolve sources from handles (context, const, tool deps). + * Wires use the unified Wire type with sources[] and catch. */ export async function resolveToolWires( ctx: ToolLookupContext, toolDef: ToolDef, input: Record, ): Promise { - // Build pipe-fork lookup: key → pipeHandle entry const forkKeys = new Set(); if (toolDef.pipeHandles) { - for (const ph of toolDef.pipeHandles) { - forkKeys.add(ph.key); - } + for (const ph of toolDef.pipeHandles) forkKeys.add(ph.key); } - // Determine whether a wire targets a pipe fork or the main tool const isForkTarget = (w: Wire): boolean => { - if (!("to" in w)) return false; const key = trunkKey(w.to); return forkKeys.has(key); }; - // Separate wires: main tool wires vs fork wires const mainConstantWires: Wire[] = []; const mainPullWires: Wire[] = []; const mainTernaryWires: Wire[] = []; - // Fork wires grouped by trunk key, sorted by instance for chain ordering + const mainComplexWires: Wire[] = []; const forkWireMap = new Map(); for (const wire of toolDef.wires) { + const primary = wire.sources[0]?.expr; + if (!primary) continue; + if (isForkTarget(wire)) { const key = trunkKey(wire.to); let group = forkWireMap.get(key); @@ -244,29 +240,25 @@ export async function resolveToolWires( group = { constants: [], pulls: [] }; forkWireMap.set(key, group); } - if ("value" in wire && !("cond" in wire)) { + if (primary.type === "literal" && wire.sources.length === 1) { group.constants.push(wire); - } else if ("from" in wire) { + } else if (primary.type === "ref") { group.pulls.push(wire); } - } else if ("cond" in wire) { + } else if (wire.sources.length > 1 || wire.catch) { + mainComplexWires.push(wire); + } else if (primary.type === "ternary") { mainTernaryWires.push(wire); - } else if ("value" in wire) { + } else if (primary.type === "literal") { mainConstantWires.push(wire); - } else if ("from" in wire) { - // Pull wires with fallbacks/catch are processed separately below - if ("fallbacks" in wire || "catchFallback" in wire) { - // handled by fallback loop - } else { - mainPullWires.push(wire); - } + } else if (primary.type === "ref") { + mainPullWires.push(wire); } } - // Execute pipe forks in instance order (lower instance first, chains depend on prior results) + // Execute pipe forks in instance order const forkResults = new Map(); if (forkWireMap.size > 0) { - // Sort fork keys by instance number to respect chain ordering const sortedForkKeys = [...forkWireMap.keys()].sort((a, b) => { const instA = parseInt(a.split(":").pop() ?? "0", 10); const instB = parseInt(b.split(":").pop() ?? "0", 10); @@ -277,62 +269,51 @@ export async function resolveToolWires( const group = forkWireMap.get(forkKey)!; const forkInput: Record = {}; - // Apply constants for (const wire of group.constants) { - if ("value" in wire && "to" in wire) { - setNested(forkInput, wire.to.path, coerceConstant(wire.value)); + const expr = wire.sources[0]!.expr; + if (expr.type === "literal") { + setNested(forkInput, wire.to.path, coerceConstant(expr.value)); } } - // Resolve pull wires (sources may be handles or prior fork results) for (const wire of group.pulls) { - if (!("from" in wire)) continue; - const fromKey = trunkKey(wire.from); - let value: any; - if (forkResults.has(fromKey)) { - // Source is a prior fork's result - value = forkResults.get(fromKey); - for (const seg of wire.from.path) { - value = value?.[seg]; - } - } else { - value = await resolveToolNodeRef(ctx, wire.from, toolDef); - } + const expr = wire.sources[0]!.expr; + if (expr.type !== "ref") continue; + const value = await resolveToolExprRef( + ctx, + expr.ref, + toolDef, + forkResults, + ); setNested(forkInput, wire.to.path, value); } - // Look up and execute the fork tool function const forkToolName = forkKey.split(":")[2] ?? ""; const fn = lookupToolFn(ctx, forkToolName); - if (fn) { - forkResults.set(forkKey, await fn(forkInput)); - } + if (fn) forkResults.set(forkKey, await fn(forkInput)); } } // Constants applied synchronously for (const wire of mainConstantWires) { - if ("value" in wire && "to" in wire) { - setNested(input, wire.to.path, coerceConstant(wire.value)); + const expr = wire.sources[0]!.expr; + if (expr.type === "literal") { + setNested(input, wire.to.path, coerceConstant(expr.value)); } } - // Pull wires resolved in parallel (independent deps shouldn't wait on each other) + // Pull wires resolved in parallel if (mainPullWires.length > 0) { const resolved = await Promise.all( mainPullWires.map(async (wire) => { - if (!("from" in wire)) return null; - const fromKey = trunkKey(wire.from); - let value: any; - if (forkResults.has(fromKey)) { - // Source is a fork result (e.g., expression chain output) - value = forkResults.get(fromKey); - for (const seg of wire.from.path) { - value = value?.[seg]; - } - } else { - value = await resolveToolNodeRef(ctx, wire.from, toolDef); - } + const expr = wire.sources[0]!.expr; + if (expr.type !== "ref") return null; + const value = await resolveToolExprRef( + ctx, + expr.ref, + toolDef, + forkResults, + ); return { path: wire.to.path, value }; }), ); @@ -341,82 +322,77 @@ export async function resolveToolWires( } } - // Ternary wires: evaluate condition and pick branch + // Ternary wires for (const wire of mainTernaryWires) { - if (!("cond" in wire)) continue; - const condValue = await resolveToolNodeRef(ctx, wire.cond, toolDef); + const expr = wire.sources[0]!.expr; + if (expr.type !== "ternary") continue; + const condRef = expr.cond.type === "ref" ? expr.cond.ref : undefined; + if (!condRef) continue; + const condValue = await resolveToolExprRef( + ctx, + condRef, + toolDef, + forkResults, + ); + const branchExpr = condValue ? expr.then : expr.else; let value: any; - if (condValue) { - if ("thenRef" in wire && wire.thenRef) { - const fromKey = trunkKey(wire.thenRef); - if (forkResults.has(fromKey)) { - value = forkResults.get(fromKey); - for (const seg of wire.thenRef.path) value = value?.[seg]; - } else { - value = await resolveToolNodeRef(ctx, wire.thenRef, toolDef); - } - } else if ("thenValue" in wire && wire.thenValue !== undefined) { - value = coerceConstant(wire.thenValue); - } - } else { - if ("elseRef" in wire && wire.elseRef) { - const fromKey = trunkKey(wire.elseRef); - if (forkResults.has(fromKey)) { - value = forkResults.get(fromKey); - for (const seg of wire.elseRef.path) value = value?.[seg]; - } else { - value = await resolveToolNodeRef(ctx, wire.elseRef, toolDef); - } - } else if ("elseValue" in wire && wire.elseValue !== undefined) { - value = coerceConstant(wire.elseValue); - } + if (branchExpr.type === "ref") { + value = await resolveToolExprRef( + ctx, + branchExpr.ref, + toolDef, + forkResults, + ); + } else if (branchExpr.type === "literal") { + value = coerceConstant(branchExpr.value); } if (value !== undefined) setNested(input, wire.to.path, value); } - // Handle fallback wires (coalesce/catch) on main pull wires - for (const wire of toolDef.wires) { + // Complex wires (with fallbacks and/or catch) + for (const wire of mainComplexWires) { if (isForkTarget(wire)) continue; - if (!("from" in wire) || !("fallbacks" in wire)) continue; - // The value was already set by the pull wire resolution above. - // Check if it needs fallback processing. - const fromKey = trunkKey(wire.from); + const primary = wire.sources[0]!.expr; let value: any; - if (forkResults.has(fromKey)) { - value = forkResults.get(fromKey); - for (const seg of wire.from.path) value = value?.[seg]; - } else { + if (primary.type === "ref") { try { - value = await resolveToolNodeRef(ctx, wire.from, toolDef); + value = await resolveToolExprRef( + ctx, + primary.ref, + toolDef, + forkResults, + ); } catch { value = undefined; } + } else if (primary.type === "literal") { + value = coerceConstant(primary.value); } - // Apply fallback chain - if (wire.fallbacks) { - for (const fb of wire.fallbacks) { - const shouldFallback = fb.type === "nullish" ? value == null : !value; - if (shouldFallback) { - if (fb.value !== undefined) { - value = coerceConstant(fb.value); - } else if (fb.ref) { - const fbKey = trunkKey(fb.ref); - if (forkResults.has(fbKey)) { - value = forkResults.get(fbKey); - for (const seg of fb.ref.path) value = value?.[seg]; - } else { - value = await resolveToolNodeRef(ctx, fb.ref, toolDef); - } - } + // Apply fallback gates + for (let j = 1; j < wire.sources.length; j++) { + const fb = wire.sources[j]!; + const shouldFallback = fb.gate === "nullish" ? value == null : !value; + if (shouldFallback) { + if (fb.expr.type === "literal") { + value = coerceConstant(fb.expr.value); + } else if (fb.expr.type === "ref") { + value = await resolveToolExprRef( + ctx, + fb.expr.ref, + toolDef, + forkResults, + ); } } } - // Apply catch fallback - if ("catchFallback" in wire && wire.catchFallback !== undefined) { - if (value == null) { - value = coerceConstant(wire.catchFallback); + // Apply catch + if (wire.catch && value == null) { + if ("value" in wire.catch) { + value = coerceConstant(wire.catch.value); + } else if ("ref" in wire.catch) { + value = await resolveToolNodeRef(ctx, wire.catch.ref, toolDef); } } @@ -424,6 +400,22 @@ export async function resolveToolWires( } } +/** Resolve a NodeRef, checking fork results first. */ +async function resolveToolExprRef( + ctx: ToolLookupContext, + ref: NodeRef, + toolDef: ToolDef, + forkResults: Map, +): Promise { + const fromKey = trunkKey(ref); + if (forkResults.has(fromKey)) { + let value = forkResults.get(fromKey); + for (const seg of ref.path) value = value?.[seg]; + return value; + } + return resolveToolNodeRef(ctx, ref, toolDef); +} + // ── Tool NodeRef resolution ───────────────────────────────────────────────── /** @@ -573,7 +565,15 @@ export function mergeToolDefConstants(toolDef: ToolDef, result: any): any { } for (const wire of toolDef.wires) { - if (!("value" in wire) || "cond" in wire || !("to" in wire)) continue; + // Only simple constant wires: single literal source, no catch + const primary = wire.sources[0]?.expr; + if ( + !primary || + primary.type !== "literal" || + wire.sources.length > 1 || + wire.catch + ) + continue; if (forkKeys.size > 0 && forkKeys.has(trunkKey(wire.to))) continue; const path = wire.to.path; @@ -581,7 +581,7 @@ export function mergeToolDefConstants(toolDef: ToolDef, result: any): any { // Only fill in fields the tool didn't already produce if (!(path[0] in result)) { - setNested(result, path, coerceConstant(wire.value)); + setNested(result, path, coerceConstant(primary.value)); } } diff --git a/packages/bridge-core/src/tree-utils.ts b/packages/bridge-core/src/tree-utils.ts index a6ad45c7..dba042a5 100644 --- a/packages/bridge-core/src/tree-utils.ts +++ b/packages/bridge-core/src/tree-utils.ts @@ -171,28 +171,37 @@ export const SIMPLE_PULL_CACHE = Symbol.for("bridge.simplePull"); // ── Wire helpers ──────────────────────────────────────────────────────────── /** - * Returns the `from` NodeRef when a wire qualifies for the simple-pull fast - * path (single `from` wire, no safe/fallbacks/catch modifiers). Returns + * Get the primary NodeRef from a wire's first source expression, if it's a ref. + * Unlike `getSimplePullRef`, this works for any wire (including those with + * fallbacks, catch, or safe access). + */ +export function getPrimaryRef(w: Wire): NodeRef | undefined { + const expr = w.sources[0]?.expr; + return expr?.type === "ref" ? expr.ref : undefined; +} + +/** Return true if the wire's primary source is a ref expression. */ +export function isPullWire(w: Wire): boolean { + return w.sources[0]?.expr.type === "ref"; +} + +/** + * Returns the source NodeRef when a wire qualifies for the simple-pull fast + * path: single ref source, not safe, no fallbacks, no catch. Returns * `null` otherwise. The result is cached on the wire via a Symbol key so * subsequent calls are a single property read without affecting V8 shapes. * See packages/bridge-core/performance.md (#11). */ export function getSimplePullRef(w: Wire): NodeRef | null { - if ("from" in w) { - const cached = (w as any)[SIMPLE_PULL_CACHE]; - if (cached !== undefined) return cached; - const ref = - !w.safe && - !w.fallbacks?.length && - !w.catchControl && - !w.catchFallbackRef && - w.catchFallback == null - ? w.from - : null; - (w as any)[SIMPLE_PULL_CACHE] = ref; - return ref; + const cached = (w as any)[SIMPLE_PULL_CACHE]; + if (cached !== undefined) return cached; + let ref: NodeRef | null = null; + if (w.sources.length === 1 && !w.catch) { + const expr = w.sources[0]!.expr; + if (expr.type === "ref" && !expr.safe) ref = expr.ref; } - return null; + (w as any)[SIMPLE_PULL_CACHE] = ref; + return ref; } // ── Misc ──────────────────────────────────────────────────────────────────── diff --git a/packages/bridge-core/src/types.ts b/packages/bridge-core/src/types.ts index 7a2cdf4c..72b570cb 100644 --- a/packages/bridge-core/src/types.ts +++ b/packages/bridge-core/src/types.ts @@ -28,103 +28,27 @@ export type NodeRef = { pathSafe?: boolean[]; }; -/** - * A single entry in a wire's fallback chain. - * - * Each entry is either a falsy gate (`||`) or a nullish gate (`??`). - * The unified array allows mixing `||` and `??` in any order: - * - * `o.x <- a.x || b.x ?? "default" || c.x` - * - * Exactly one of `ref`, `value`, or `control` should be set. - */ -export interface WireFallback { - type: "falsy" | "nullish"; - ref?: NodeRef; - value?: string; - control?: ControlFlowInstruction; - loc?: SourceLocation; -} - /** * A wire connects a data source (from) to a data sink (to). - * Execution is pull-based: when "to" is demanded, "from" is resolved. * - * Constant wires (`=`) set a fixed value on the target. - * Pull wires (`<-`) resolve the source at runtime. - * Pipe wires (`pipe: true`) are generated by the `<- h1:h2:source` shorthand - * and route data through declared tool handles; the serializer collapses them - * back to pipe notation. + * Unified shape: every wire has an ordered list of source entries and an + * optional catch handler. The first source entry is always evaluated; subsequent + * entries have a gate (`||` for falsy, `??` for nullish) that determines whether + * to fall through to them. + * + * Constant wires have a single literal source entry. + * Ternary/boolean wires have a single ternary/and/or expression entry. + * Pipe wires (`pipe: true`) route data through declared tool handles. * Spread wires (`spread: true`) merge source object properties into the target. */ -export type Wire = - | { - from: NodeRef; - to: NodeRef; - loc?: SourceLocation; - fromLoc?: SourceLocation; - pipe?: true; - /** When true, this wire merges source properties into target (from `...source` syntax). */ - spread?: true; - safe?: true; - fallbacks?: WireFallback[]; - catchLoc?: SourceLocation; - catchFallback?: string; - catchFallbackRef?: NodeRef; - catchControl?: ControlFlowInstruction; - } - | { value: string; to: NodeRef; loc?: SourceLocation } - | { - cond: NodeRef; - condLoc?: SourceLocation; - thenRef?: NodeRef; - thenValue?: string; - thenLoc?: SourceLocation; - elseRef?: NodeRef; - elseValue?: string; - elseLoc?: SourceLocation; - to: NodeRef; - loc?: SourceLocation; - fallbacks?: WireFallback[]; - catchLoc?: SourceLocation; - catchFallback?: string; - catchFallbackRef?: NodeRef; - catchControl?: ControlFlowInstruction; - } - | { - /** Short-circuit logical AND: evaluate left first, only evaluate right if left is truthy */ - condAnd: { - leftRef: NodeRef; - rightRef?: NodeRef; - rightValue?: string; - safe?: true; - rightSafe?: true; - }; - to: NodeRef; - loc?: SourceLocation; - fallbacks?: WireFallback[]; - catchLoc?: SourceLocation; - catchFallback?: string; - catchFallbackRef?: NodeRef; - catchControl?: ControlFlowInstruction; - } - | { - /** Short-circuit logical OR: evaluate left first, only evaluate right if left is falsy */ - condOr: { - leftRef: NodeRef; - rightRef?: NodeRef; - rightValue?: string; - safe?: true; - rightSafe?: true; - }; - to: NodeRef; - loc?: SourceLocation; - fallbacks?: WireFallback[]; - catchLoc?: SourceLocation; - catchFallback?: string; - catchFallbackRef?: NodeRef; - catchControl?: ControlFlowInstruction; - }; +export type Wire = { + to: NodeRef; + sources: WireSourceEntry[]; + catch?: WireCatch; + pipe?: true; + spread?: true; + loc?: SourceLocation; +}; /** * Bridge definition — wires one GraphQL field to its data sources. @@ -261,6 +185,105 @@ export type ControlFlowInstruction = | { kind: "continue"; levels?: number } | { kind: "break"; levels?: number }; +// ── Wire Expression Model ─────────────────────────────────────────────────── +// +// Every wire is an ordered list of source entries + an optional catch handler. +// Source entries contain recursive Expression trees that evaluate to values. + +/** + * A recursive expression tree that evaluates to a single value within one + * source entry. + * + * This captures everything that can appear as the "value-producing" + * component of a wire: refs, literals, ternaries, boolean short-circuit + * operators, and control flow instructions. + * + * Note: Bridge `||` and `??` are wire-level fallback gates (sequential + * "try this source, if the gate opens try the next one"). They are NOT + * expression operators. They live on `WireSourceEntry.gate`. + */ +export type Expression = + | { + /** Pull a data source reference */ + type: "ref"; + ref: NodeRef; + safe?: true; + refLoc?: SourceLocation; + loc?: SourceLocation; + } + | { + /** JSON-encoded constant: "\"hello\"", "42", "true", "null" */ + type: "literal"; + value: string; + loc?: SourceLocation; + } + | { + /** Ternary: `cond ? then : else` */ + type: "ternary"; + cond: Expression; + then: Expression; + else: Expression; + condLoc?: SourceLocation; + thenLoc?: SourceLocation; + elseLoc?: SourceLocation; + loc?: SourceLocation; + } + | { + /** Short-circuit logical AND: `left && right` → boolean */ + type: "and"; + left: Expression; + right: Expression; + leftSafe?: true; + rightSafe?: true; + loc?: SourceLocation; + } + | { + /** Short-circuit logical OR: `left || right` → boolean */ + type: "or"; + left: Expression; + right: Expression; + leftSafe?: true; + rightSafe?: true; + loc?: SourceLocation; + } + | { + /** Loop/error control: throw, panic, continue, break */ + type: "control"; + control: ControlFlowInstruction; + loc?: SourceLocation; + }; + +/** + * One entry in the wire's ordered fallback chain. + * + * The first entry has no gate (always evaluated); subsequent entries have a + * gate that opens when the running value meets the condition. + * + * `gate` corresponds to `||` (falsy) and `??` (nullish) in bridge source. + * These are wire-level sequencing, not expression-level operators. + */ +export interface WireSourceEntry { + /** The expression to evaluate for this source */ + expr: Expression; + /** + * When to try this entry: + * - absent → always (first entry — the primary source) + * - "falsy" → previous value was falsy (0, "", false, null, undefined) + * - "nullish" → previous value was null or undefined + */ + gate?: "falsy" | "nullish"; + loc?: SourceLocation; +} + +/** + * Catch handler for a wire — provides error recovery via a ref, literal, or + * control flow instruction. + */ +export type WireCatch = + | { ref: NodeRef; loc?: SourceLocation } + | { value: string; loc?: SourceLocation } + | { control: ControlFlowInstruction; loc?: SourceLocation }; + /** * Named constant definition — a reusable value defined in the bridge file. * diff --git a/packages/bridge-core/test/resolve-wires-gates.test.ts b/packages/bridge-core/test/resolve-wires-gates.test.ts deleted file mode 100644 index 2f420f58..00000000 --- a/packages/bridge-core/test/resolve-wires-gates.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -/** - * Unit tests for the wire resolution gate helpers extracted from - * `resolveWires.ts`. These functions can be tested independently of the - * full execution engine via a lightweight mock `TreeContext`. - */ -import assert from "node:assert/strict"; -import { describe, test } from "node:test"; -import { - BREAK_SYM, - CONTINUE_SYM, - isLoopControlSignal, -} from "../src/tree-types.ts"; -import { applyFallbackGates, applyCatchGate } from "../src/resolveWires.ts"; -import type { TreeContext } from "../src/tree-types.ts"; -import type { NodeRef, Wire } from "../src/types.ts"; - -// ── Test helpers ───────────────────────────────────────────────────────────── - -/** Minimal NodeRef for use in test wires */ -const REF: NodeRef = { module: "m", type: "Query", field: "f", path: [] }; - -/** Build a NodeRef with an alternative field name. */ -function ref(field: string): NodeRef { - return { module: "m", type: "Query", field, path: [] }; -} - -/** Build a minimal TreeContext that resolves refs from a plain value map. */ -function makeCtx(values: Record = {}): TreeContext { - return { - pullSingle(ref) { - const key = `${ref.module}.${ref.field}`; - return (key in values ? values[key] : undefined) as ReturnType< - TreeContext["pullSingle"] - >; - }, - }; -} - -/** A wire with no gate modifiers — used as a baseline. */ -type TestWire = Extract; - -function fromWire(overrides: Partial = {}): TestWire { - return { from: REF, to: REF, ...overrides } as TestWire; -} - -// ── applyFallbackGates — falsy (||) ───────────────────────────────────────── - -describe("applyFallbackGates — falsy (||)", () => { - test("passes through a truthy value unchanged", async () => { - const ctx = makeCtx(); - const w = fromWire(); - assert.equal(await applyFallbackGates(ctx, w, "hello"), "hello"); - assert.equal(await applyFallbackGates(ctx, w, 42), 42); - assert.equal(await applyFallbackGates(ctx, w, true), true); - assert.deepEqual(await applyFallbackGates(ctx, w, { x: 1 }), { x: 1 }); - }); - - test("returns falsy value when no fallback is configured", async () => { - const ctx = makeCtx(); - const w = fromWire(); - assert.equal(await applyFallbackGates(ctx, w, 0), 0); - assert.equal(await applyFallbackGates(ctx, w, ""), ""); - assert.equal(await applyFallbackGates(ctx, w, false), false); - assert.equal(await applyFallbackGates(ctx, w, null), null); - }); - - test("returns first truthy ref from falsy fallback refs", async () => { - const ctx = makeCtx({ "m.a": null, "m.b": "found" }); - const w = fromWire({ - fallbacks: [ - { type: "falsy", ref: ref("a") }, - { type: "falsy", ref: ref("b") }, - ], - }); - assert.equal(await applyFallbackGates(ctx, w, null), "found"); - }); - - test("skips falsy refs and falls through to falsy constant", async () => { - const ctx = makeCtx({ "m.a": 0 }); - const w = fromWire({ - fallbacks: [ - { type: "falsy", ref: ref("a") }, - { type: "falsy", value: "42" }, - ], - }); - assert.equal(await applyFallbackGates(ctx, w, null), 42); - }); - - test("applies falsy constant when value is falsy and no refs given", async () => { - const ctx = makeCtx(); - const w = fromWire({ fallbacks: [{ type: "falsy", value: "default" }] }); - assert.equal(await applyFallbackGates(ctx, w, null), "default"); - assert.equal(await applyFallbackGates(ctx, w, false), "default"); - assert.equal(await applyFallbackGates(ctx, w, ""), "default"); - }); - - test("applies falsy control when value is falsy", async () => { - const ctx = makeCtx(); - const w = fromWire({ - fallbacks: [{ type: "falsy", control: { kind: "continue" } }], - }); - assert.equal(await applyFallbackGates(ctx, w, 0), CONTINUE_SYM); - }); - - test("falsy control kind=break returns BREAK_SYM", async () => { - const ctx = makeCtx(); - const w = fromWire({ - fallbacks: [{ type: "falsy", control: { kind: "break" } }], - }); - assert.equal(await applyFallbackGates(ctx, w, false), BREAK_SYM); - }); - - test("falsy control kind=break with level 2 returns multi-level signal", async () => { - const ctx = makeCtx(); - const w = fromWire({ - fallbacks: [{ type: "falsy", control: { kind: "break", levels: 2 } }], - }); - const out = await applyFallbackGates(ctx, w, false); - assert.ok(isLoopControlSignal(out)); - assert.notEqual(out, BREAK_SYM); - assert.notEqual(out, CONTINUE_SYM); - assert.deepStrictEqual(out, { __bridgeControl: "break", levels: 2 }); - }); - - test("falsy control kind=throw throws an error", async () => { - const ctx = makeCtx(); - const w = fromWire({ - fallbacks: [ - { type: "falsy", control: { kind: "throw", message: "boom" } }, - ], - }); - await assert.rejects(() => applyFallbackGates(ctx, w, null), /boom/); - }); - - test("forwards pullChain to ctx.pullSingle for falsy ref", async () => { - let capturedChain: Set | undefined; - const ctx: TreeContext = { - pullSingle(_ref, pullChain) { - capturedChain = pullChain; - return "value"; - }, - }; - const chain = new Set(["some:key"]); - const w = fromWire({ fallbacks: [{ type: "falsy", ref: ref("a") }] }); - await applyFallbackGates(ctx, w, null, chain); - assert.equal(capturedChain, chain); - }); -}); - -// ── applyFallbackGates — nullish (??) ──────────────────────────────────────── - -describe("applyFallbackGates — nullish (??)", () => { - test("passes through a non-nullish value unchanged", async () => { - const ctx = makeCtx(); - const w = fromWire({ fallbacks: [{ type: "nullish", value: "99" }] }); - assert.equal(await applyFallbackGates(ctx, w, "hello"), "hello"); - assert.equal(await applyFallbackGates(ctx, w, 0), 0); - assert.equal(await applyFallbackGates(ctx, w, false), false); - assert.equal(await applyFallbackGates(ctx, w, ""), ""); - }); - - test("returns null/undefined when no fallback is configured", async () => { - const ctx = makeCtx(); - const w = fromWire(); - assert.equal(await applyFallbackGates(ctx, w, null), null); - assert.equal(await applyFallbackGates(ctx, w, undefined), undefined); - }); - - test("resolves nullish ref when value is null", async () => { - const ctx = makeCtx({ "m.fallback": "resolved" }); - const w = fromWire({ - fallbacks: [{ type: "nullish", ref: ref("fallback") }], - }); - assert.equal(await applyFallbackGates(ctx, w, null), "resolved"); - }); - - test("applies nullish constant when value is null", async () => { - const ctx = makeCtx(); - const w = fromWire({ fallbacks: [{ type: "nullish", value: "99" }] }); - assert.equal(await applyFallbackGates(ctx, w, null), 99); - assert.equal(await applyFallbackGates(ctx, w, undefined), 99); - }); - - test("applies nullish control when value is null", async () => { - const ctx = makeCtx(); - const w = fromWire({ - fallbacks: [{ type: "nullish", control: { kind: "continue" } }], - }); - assert.equal(await applyFallbackGates(ctx, w, null), CONTINUE_SYM); - }); - - test("nullish control takes priority (returns immediately)", async () => { - const ctx = makeCtx({ "m.f": "should-not-be-used" }); - const w = fromWire({ - fallbacks: [ - { type: "nullish", control: { kind: "break" } }, - { type: "nullish", ref: REF }, - ], - }); - assert.equal(await applyFallbackGates(ctx, w, null), BREAK_SYM); - }); - - test("forwards pullChain to ctx.pullSingle for nullish ref", async () => { - let capturedChain: Set | undefined; - const ctx: TreeContext = { - pullSingle(_ref, pullChain) { - capturedChain = pullChain; - return "resolved"; - }, - }; - const chain = new Set(["some:key"]); - const w = fromWire({ fallbacks: [{ type: "nullish", ref: REF }] }); - await applyFallbackGates(ctx, w, null, chain); - assert.equal(capturedChain, chain); - }); -}); - -// ── applyFallbackGates — mixed chains ──────────────────────────────────────── - -describe("applyFallbackGates — mixed || and ??", () => { - test("A ?? B || C — nullish then falsy", async () => { - const ctx = makeCtx({ "m.b": 0, "m.c": "found" }); - const w = fromWire({ - fallbacks: [ - { type: "nullish", ref: ref("b") }, // ?? B → 0 (non-nullish, stops ?? but falsy) - { type: "falsy", ref: ref("c") }, // || C → "found" - ], - }); - assert.equal(await applyFallbackGates(ctx, w, null), "found"); - }); - - test("A || B ?? C — falsy then nullish", async () => { - const ctx = makeCtx({ "m.b": null, "m.c": "fallback" }); - const w = fromWire({ - fallbacks: [ - { type: "falsy", ref: ref("b") }, // || B → null (still falsy) - { type: "nullish", ref: ref("c") }, // ?? C → "fallback" - ], - }); - assert.equal(await applyFallbackGates(ctx, w, ""), "fallback"); - }); - - test("A ?? B || C ?? D — four-item chain", async () => { - const ctx = makeCtx({ "m.b": null, "m.c": null }); - const w = fromWire({ - fallbacks: [ - { type: "nullish", ref: ref("b") }, // ?? B → null (still nullish) - { type: "falsy", ref: ref("c") }, // || C → null (still falsy) - { type: "nullish", value: "final" }, // ?? D → "final" - ], - }); - assert.equal(await applyFallbackGates(ctx, w, null), "final"); - }); - - test("mixed chain stops when value becomes truthy and non-nullish", async () => { - const ctx = makeCtx({ "m.b": "good" }); - const w = fromWire({ - fallbacks: [ - { type: "nullish", ref: ref("b") }, // ?? B → "good" - { type: "falsy", value: "unused" }, // || ... gate closed, value is truthy - ], - }); - assert.equal(await applyFallbackGates(ctx, w, null), "good"); - }); - - test("falsy gate open but nullish gate closed for 0", async () => { - const ctx = makeCtx(); - const w = fromWire({ - fallbacks: [ - { type: "nullish", value: "unused" }, // ?? gate closed: 0 != null - { type: "falsy", value: "fallback" }, // || gate open: !0 is true - ], - }); - assert.equal(await applyFallbackGates(ctx, w, 0), "fallback"); - }); -}); - -// ── applyCatchGate ──────────────────────────────────────────────────────────── - -describe("applyCatchGate", () => { - test("returns undefined when no catch handler is configured", async () => { - const ctx = makeCtx(); - const w = fromWire(); - assert.equal(await applyCatchGate(ctx, w), undefined); - }); - - test("applies catchFallback constant", async () => { - const ctx = makeCtx(); - const w = fromWire({ catchFallback: "fallback" }); - assert.equal(await applyCatchGate(ctx, w), "fallback"); - }); - - test("resolves catchFallbackRef", async () => { - const ctx = makeCtx({ "m.backup": "backup-value" }); - const w = fromWire({ catchFallbackRef: ref("backup") }); - assert.equal(await applyCatchGate(ctx, w), "backup-value"); - }); - - test("applies catchControl kind=continue", async () => { - const ctx = makeCtx(); - const w = fromWire({ catchControl: { kind: "continue" } }); - assert.equal(await applyCatchGate(ctx, w), CONTINUE_SYM); - }); - - test("applies catchControl kind=continue with level 3", async () => { - const ctx = makeCtx(); - const w = fromWire({ catchControl: { kind: "continue", levels: 3 } }); - const out = await applyCatchGate(ctx, w); - assert.ok(isLoopControlSignal(out)); - assert.deepStrictEqual(out, { __bridgeControl: "continue", levels: 3 }); - }); - - test("catchControl takes priority over catchFallbackRef", async () => { - const ctx = makeCtx({ "m.backup": "should-not-be-used" }); - const w = fromWire({ - catchControl: { kind: "break" }, - catchFallbackRef: REF, - }); - assert.equal(await applyCatchGate(ctx, w), BREAK_SYM); - }); - - test("catchControl kind=throw propagates the error", async () => { - const ctx = makeCtx(); - const w = fromWire({ - catchControl: { kind: "throw", message: "catch-throw" }, - }); - await assert.rejects(() => applyCatchGate(ctx, w), /catch-throw/); - }); - - test("forwards pullChain to ctx.pullSingle for catchFallbackRef", async () => { - let capturedChain: Set | undefined; - const ctx: TreeContext = { - pullSingle(_ref, pullChain) { - capturedChain = pullChain; - return "recovered"; - }, - }; - const chain = new Set(["some:key"]); - const w = fromWire({ catchFallbackRef: REF }); - await applyCatchGate(ctx, w, chain); - assert.equal(capturedChain, chain); - }); -}); diff --git a/packages/bridge-core/test/resolve-wires.test.ts b/packages/bridge-core/test/resolve-wires.test.ts new file mode 100644 index 00000000..e923fbe1 --- /dev/null +++ b/packages/bridge-core/test/resolve-wires.test.ts @@ -0,0 +1,401 @@ +/** + * Unit tests for wire resolution. + * + * Tests expression evaluation, fallback gates, and catch handlers. + */ +import assert from "node:assert/strict"; +import { describe, test } from "node:test"; +import { + BREAK_SYM, + CONTINUE_SYM, + isLoopControlSignal, +} from "../src/tree-types.ts"; +import type { TreeContext } from "../src/tree-types.ts"; +import type { Expression, NodeRef, Wire } from "../src/types.ts"; +import { + evaluateExpression, + applyFallbackGates, + applyCatch, +} from "../src/resolveWiresSources.ts"; + +// ── Test helpers ───────────────────────────────────────────────────────────── + +const REF: NodeRef = { module: "m", type: "Query", field: "f", path: [] }; + +function ref(field: string): NodeRef { + return { module: "m", type: "Query", field, path: [] }; +} + +function makeCtx(values: Record = {}): TreeContext { + return { + pullSingle(ref) { + const key = `${ref.module}.${ref.field}`; + if (key in values) { + const v = values[key]; + if (v instanceof Error) throw v; + return v as ReturnType; + } + return undefined as ReturnType; + }, + }; +} + +function makeWire(sources: Wire["sources"], opts: Partial = {}): Wire { + return { to: REF, sources, ...opts }; +} + +// ── evaluateExpression ────────────────────────────────────────────────────── + +describe("evaluateExpression", () => { + test("evaluates a ref expression", async () => { + const ctx = makeCtx({ "m.x": "hello" }); + const expr: Expression = { type: "ref", ref: ref("x") }; + assert.equal(await evaluateExpression(ctx, expr), "hello"); + }); + + test("evaluates a literal expression", async () => { + const ctx = makeCtx(); + assert.equal( + await evaluateExpression(ctx, { type: "literal", value: "42" }), + 42, + ); + assert.equal( + await evaluateExpression(ctx, { type: "literal", value: '"hello"' }), + "hello", + ); + assert.equal( + await evaluateExpression(ctx, { type: "literal", value: "true" }), + true, + ); + }); + + test("safe ref returns undefined on error", async () => { + const ctx = makeCtx({ "m.x": new Error("boom") }); + const expr: Expression = { type: "ref", ref: ref("x"), safe: true }; + assert.equal(await evaluateExpression(ctx, expr), undefined); + }); + + test("evaluates a ternary expression — then branch", async () => { + const ctx = makeCtx({ "m.flag": true, "m.a": "yes", "m.b": "no" }); + const expr: Expression = { + type: "ternary", + cond: { type: "ref", ref: ref("flag") }, + then: { type: "ref", ref: ref("a") }, + else: { type: "ref", ref: ref("b") }, + }; + assert.equal(await evaluateExpression(ctx, expr), "yes"); + }); + + test("evaluates a ternary expression — else branch", async () => { + const ctx = makeCtx({ "m.flag": false, "m.a": "yes", "m.b": "no" }); + const expr: Expression = { + type: "ternary", + cond: { type: "ref", ref: ref("flag") }, + then: { type: "ref", ref: ref("a") }, + else: { type: "ref", ref: ref("b") }, + }; + assert.equal(await evaluateExpression(ctx, expr), "no"); + }); + + test("evaluates AND expression — both truthy", async () => { + const ctx = makeCtx({ "m.a": "yes", "m.b": "also" }); + const expr: Expression = { + type: "and", + left: { type: "ref", ref: ref("a") }, + right: { type: "ref", ref: ref("b") }, + }; + assert.equal(await evaluateExpression(ctx, expr), true); + }); + + test("evaluates AND expression — left falsy", async () => { + const ctx = makeCtx({ "m.a": false, "m.b": "yes" }); + const expr: Expression = { + type: "and", + left: { type: "ref", ref: ref("a") }, + right: { type: "ref", ref: ref("b") }, + }; + assert.equal(await evaluateExpression(ctx, expr), false); + }); + + test("evaluates OR expression — left truthy", async () => { + const ctx = makeCtx({ "m.a": "yes", "m.b": false }); + const expr: Expression = { + type: "or", + left: { type: "ref", ref: ref("a") }, + right: { type: "ref", ref: ref("b") }, + }; + assert.equal(await evaluateExpression(ctx, expr), true); + }); + + test("evaluates OR expression — both falsy", async () => { + const ctx = makeCtx({ "m.a": false, "m.b": false }); + const expr: Expression = { + type: "or", + left: { type: "ref", ref: ref("a") }, + right: { type: "ref", ref: ref("b") }, + }; + assert.equal(await evaluateExpression(ctx, expr), false); + }); + + test("evaluates control — continue", async () => { + const ctx = makeCtx(); + const expr: Expression = { + type: "control", + control: { kind: "continue" }, + }; + assert.equal(await evaluateExpression(ctx, expr), CONTINUE_SYM); + }); + + test("evaluates control — break", async () => { + const ctx = makeCtx(); + const expr: Expression = { + type: "control", + control: { kind: "break" }, + }; + assert.equal(await evaluateExpression(ctx, expr), BREAK_SYM); + }); + + test("evaluates control — throw", () => { + const ctx = makeCtx(); + const expr: Expression = { + type: "control", + control: { kind: "throw", message: "boom" }, + }; + assert.throws(() => evaluateExpression(ctx, expr), { message: "boom" }); + }); +}); + +// ── applyFallbackGates ────────────────────────────────────────────────── + +describe("applyFallbackGates — falsy (||)", () => { + test("passes through a truthy value unchanged", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }]); + assert.equal(await applyFallbackGates(ctx, w, "hello"), "hello"); + assert.equal(await applyFallbackGates(ctx, w, 42), 42); + }); + + test("returns falsy value when no fallback entries exist", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }]); + assert.equal(await applyFallbackGates(ctx, w, 0), 0); + assert.equal(await applyFallbackGates(ctx, w, null), null); + }); + + test("returns first truthy ref from falsy fallback refs", async () => { + const ctx = makeCtx({ "m.a": null, "m.b": "found" }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("a") }, gate: "falsy" }, + { expr: { type: "ref", ref: ref("b") }, gate: "falsy" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), "found"); + }); + + test("skips falsy refs and falls through to falsy constant", async () => { + const ctx = makeCtx({ "m.a": 0 }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("a") }, gate: "falsy" }, + { expr: { type: "literal", value: "42" }, gate: "falsy" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), 42); + }); + + test("applies falsy constant when value is falsy", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "literal", value: "default" }, gate: "falsy" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), "default"); + assert.equal(await applyFallbackGates(ctx, w, false), "default"); + assert.equal(await applyFallbackGates(ctx, w, ""), "default"); + }); + + test("applies falsy control — continue", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { + expr: { type: "control", control: { kind: "continue" } }, + gate: "falsy", + }, + ]); + assert.equal(await applyFallbackGates(ctx, w, 0), CONTINUE_SYM); + }); + + test("applies falsy control — break", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { + expr: { type: "control", control: { kind: "break" } }, + gate: "falsy", + }, + ]); + assert.equal(await applyFallbackGates(ctx, w, false), BREAK_SYM); + }); + + test("applies falsy control — break level 2", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { + expr: { type: "control", control: { kind: "break", levels: 2 } }, + gate: "falsy", + }, + ]); + const out = await applyFallbackGates(ctx, w, false); + assert.ok(isLoopControlSignal(out)); + assert.deepStrictEqual(out, { __bridgeControl: "break", levels: 2 }); + }); + + test("applies falsy control — throw", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { + expr: { type: "control", control: { kind: "throw", message: "boom" } }, + gate: "falsy", + }, + ]); + await assert.rejects(() => applyFallbackGates(ctx, w, null), /boom/); + }); +}); + +describe("applyFallbackGates — nullish (??)", () => { + test("passes through a non-nullish value unchanged", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "literal", value: "99" }, gate: "nullish" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, "hello"), "hello"); + assert.equal(await applyFallbackGates(ctx, w, 0), 0); + assert.equal(await applyFallbackGates(ctx, w, false), false); + assert.equal(await applyFallbackGates(ctx, w, ""), ""); + }); + + test("resolves nullish ref when value is null", async () => { + const ctx = makeCtx({ "m.fallback": "resolved" }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("fallback") }, gate: "nullish" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), "resolved"); + }); + + test("applies nullish constant when value is null", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "literal", value: "99" }, gate: "nullish" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), 99); + assert.equal(await applyFallbackGates(ctx, w, undefined), 99); + }); +}); + +describe("applyFallbackGates — mixed || and ??", () => { + test("A ?? B || C — nullish then falsy", async () => { + const ctx = makeCtx({ "m.b": 0, "m.c": "found" }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("b") }, gate: "nullish" }, + { expr: { type: "ref", ref: ref("c") }, gate: "falsy" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), "found"); + }); + + test("A || B ?? C — falsy then nullish", async () => { + const ctx = makeCtx({ "m.b": null, "m.c": "fallback" }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("b") }, gate: "falsy" }, + { expr: { type: "ref", ref: ref("c") }, gate: "nullish" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, ""), "fallback"); + }); + + test("four-item chain", async () => { + const ctx = makeCtx({ "m.b": null, "m.c": null }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("b") }, gate: "nullish" }, + { expr: { type: "ref", ref: ref("c") }, gate: "falsy" }, + { expr: { type: "literal", value: "final" }, gate: "nullish" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), "final"); + }); + + test("mixed chain stops when value becomes truthy", async () => { + const ctx = makeCtx({ "m.b": "good" }); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "ref", ref: ref("b") }, gate: "nullish" }, + { expr: { type: "literal", value: "unused" }, gate: "falsy" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, null), "good"); + }); + + test("falsy gate open but nullish gate closed for 0", async () => { + const ctx = makeCtx(); + const w = makeWire([ + { expr: { type: "ref", ref: REF } }, + { expr: { type: "literal", value: "unused" }, gate: "nullish" }, + { expr: { type: "literal", value: "fallback" }, gate: "falsy" }, + ]); + assert.equal(await applyFallbackGates(ctx, w, 0), "fallback"); + }); +}); + +// ── applyCatch ────────────────────────────────────────────────────────── + +describe("applyCatch", () => { + test("returns undefined when no catch handler", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }]); + assert.equal(await applyCatch(ctx, w), undefined); + }); + + test("applies catch value constant", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }], { + catch: { value: "fallback" }, + }); + assert.equal(await applyCatch(ctx, w), "fallback"); + }); + + test("resolves catch ref", async () => { + const ctx = makeCtx({ "m.backup": "backup-value" }); + const w = makeWire([{ expr: { type: "ref", ref: REF } }], { + catch: { ref: ref("backup") }, + }); + assert.equal(await applyCatch(ctx, w), "backup-value"); + }); + + test("applies catch control — continue", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }], { + catch: { control: { kind: "continue" } }, + }); + assert.equal(await applyCatch(ctx, w), CONTINUE_SYM); + }); + + test("applies catch control — break", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }], { + catch: { control: { kind: "break" } }, + }); + assert.equal(await applyCatch(ctx, w), BREAK_SYM); + }); + + test("catch control — throw", async () => { + const ctx = makeCtx(); + const w = makeWire([{ expr: { type: "ref", ref: REF } }], { + catch: { control: { kind: "throw", message: "catch-throw" } }, + }); + await assert.rejects(() => applyCatch(ctx, w), /catch-throw/); + }); +}); diff --git a/packages/bridge-core/test/traversal-manifest-locations.test.ts b/packages/bridge-core/test/traversal-manifest-locations.test.ts index d7fd03a6..1316f0c8 100644 --- a/packages/bridge-core/test/traversal-manifest-locations.test.ts +++ b/packages/bridge-core/test/traversal-manifest-locations.test.ts @@ -4,6 +4,7 @@ import { parseBridgeChevrotain } from "../../bridge-parser/src/index.ts"; import { buildTraversalManifest, type Bridge, + type Expression, type SourceLocation, type TraversalEntry, type Wire, @@ -27,12 +28,12 @@ function assertLoc( assert.deepEqual(entry.loc, expected); } -function isPullWire(wire: Wire): wire is Extract { - return "from" in wire; +function isPullWire(wire: Wire): boolean { + return wire.sources.length >= 1 && wire.sources[0]!.expr.type === "ref"; } -function isTernaryWire(wire: Wire): wire is Extract { - return "cond" in wire; +function isTernaryWire(wire: Wire): boolean { + return wire.sources.length >= 1 && wire.sources[0]!.expr.type === "ternary"; } describe("buildTraversalManifest source locations", () => { @@ -57,25 +58,33 @@ describe("buildTraversalManifest source locations", () => { assert.ok(messageWire); const manifest = buildTraversalManifest(instr); + const msgExpr = messageWire.sources[0]!.expr as Extract< + Expression, + { type: "ref" } + >; assertLoc( manifest.find((entry) => entry.id === "message/primary"), - messageWire.fromLoc, + msgExpr.refLoc, ); assertLoc( manifest.find((entry) => entry.id === "message/fallback:0"), - messageWire.fallbacks?.[0]?.loc, + messageWire.sources[1]?.loc, ); assertLoc( manifest.find((entry) => entry.id === "message/catch"), - messageWire.catchLoc, + messageWire.catch?.loc, ); + const aliasExpr = aliasWire.sources[0]!.expr as Extract< + Expression, + { type: "ref" } + >; assertLoc( manifest.find((entry) => entry.id === "clean/primary"), - aliasWire.fromLoc, + aliasExpr.refLoc, ); assertLoc( manifest.find((entry) => entry.id === "clean/catch"), - aliasWire.catchLoc, + aliasWire.catch?.loc, ); }); @@ -92,14 +101,18 @@ describe("buildTraversalManifest source locations", () => { const ternaryWire = instr.wires.find(isTernaryWire); assert.ok(ternaryWire); + const ternaryExpr = ternaryWire.sources[0]!.expr as Extract< + Expression, + { type: "ternary" } + >; const manifest = buildTraversalManifest(instr); assertLoc( manifest.find((entry) => entry.id === "name/then"), - ternaryWire.thenLoc, + ternaryExpr.thenLoc, ); assertLoc( manifest.find((entry) => entry.id === "name/else"), - ternaryWire.elseLoc, + ternaryExpr.elseLoc, ); }); diff --git a/packages/bridge-graphql/src/bridge-asserts.ts b/packages/bridge-graphql/src/bridge-asserts.ts index 2b626877..f97c9f39 100644 --- a/packages/bridge-graphql/src/bridge-asserts.ts +++ b/packages/bridge-graphql/src/bridge-asserts.ts @@ -58,36 +58,21 @@ export function assertBridgeGraphQLCompatible(bridge: Bridge): void { if (!isElementSubfield) continue; - const fallbacks = - "from" in wire - ? wire.fallbacks - : "cond" in wire - ? wire.fallbacks - : "condAnd" in wire - ? wire.fallbacks - : "condOr" in wire - ? wire.fallbacks - : undefined; + // Check sources for break/continue control flow in fallback gates + const hasControlFlowInSources = wire.sources.some( + (s) => + s.expr.type === "control" && + (s.expr.control.kind === "break" || s.expr.control.kind === "continue"), + ); - const catchControl = - "from" in wire - ? wire.catchControl - : "cond" in wire - ? wire.catchControl - : "condAnd" in wire - ? wire.catchControl - : "condOr" in wire - ? wire.catchControl - : undefined; + // Check catch handler for break/continue control flow + const catchHasControlFlow = + wire.catch && + "control" in wire.catch && + (wire.catch.control.kind === "break" || + wire.catch.control.kind === "continue"); - const isBreakOrContinue = ( - ctrl: { kind: string; levels?: number } | undefined, - ) => ctrl && (ctrl.kind === "break" || ctrl.kind === "continue"); - - if ( - fallbacks?.some((fb) => isBreakOrContinue(fb.control)) || - isBreakOrContinue(catchControl) - ) { + if (hasControlFlowInSources || catchHasControlFlow) { const path = wire.to.path.join("."); throw new BridgeGraphQLIncompatibleError( op, diff --git a/packages/bridge-parser/package.json b/packages/bridge-parser/package.json index e9ecf1aa..6e14d5c3 100644 --- a/packages/bridge-parser/package.json +++ b/packages/bridge-parser/package.json @@ -27,7 +27,7 @@ "dependencies": { "@stackables/bridge-core": "workspace:*", "@stackables/bridge-stdlib": "workspace:*", - "chevrotain": "^11.2.0" + "chevrotain": "^12.0.0" }, "devDependencies": { "@types/node": "^25.5.0", diff --git a/packages/bridge-parser/src/bridge-format.ts b/packages/bridge-parser/src/bridge-format.ts index 10511827..485f1d9f 100644 --- a/packages/bridge-parser/src/bridge-format.ts +++ b/packages/bridge-parser/src/bridge-format.ts @@ -4,6 +4,7 @@ import type { ConstDef, ControlFlowInstruction, DefineDef, + Expression, NodeRef, ToolDef, Wire, @@ -15,6 +16,31 @@ import { } from "./parser/index.ts"; export { parsePath } from "@stackables/bridge-core"; +// ── Wire shape helpers ────────────────────────────────────────────── +type RefExpr = Extract; +type LitExpr = Extract; +type TernExpr = Extract; +type AndOrExpr = + | Extract + | Extract; + +const isPull = (w: Wire): boolean => w.sources[0]?.expr.type === "ref"; +const isLit = (w: Wire): boolean => w.sources[0]?.expr.type === "literal"; +const isTern = (w: Wire): boolean => w.sources[0]?.expr.type === "ternary"; +const isAndW = (w: Wire): boolean => w.sources[0]?.expr.type === "and"; +const isOrW = (w: Wire): boolean => w.sources[0]?.expr.type === "or"; + +const wRef = (w: Wire): NodeRef => (w.sources[0].expr as RefExpr).ref; +const wVal = (w: Wire): string => (w.sources[0].expr as LitExpr).value; +const wSafe = (w: Wire): true | undefined => { + const e = w.sources[0].expr; + return e.type === "ref" ? e.safe : undefined; +}; +const wTern = (w: Wire): TernExpr => w.sources[0].expr as TernExpr; +const wAndOr = (w: Wire): AndOrExpr => w.sources[0].expr as AndOrExpr; +const eRef = (e: Expression): NodeRef => (e as RefExpr).ref; +const eVal = (e: Expression): string => (e as LitExpr).value; + /** * Parse .bridge text — delegates to the Chevrotain parser. */ @@ -70,6 +96,42 @@ function serializeControl(ctrl: ControlFlowInstruction): string { return ctrl.levels && ctrl.levels > 1 ? `break ${ctrl.levels}` : "break"; } +/** + * Serialize fallback entries (sources after the first) as `|| val` / `?? val`. + * `refFn` renders NodeRef→string; `valFn` renders literal value→string. + */ +function serFallbacks( + w: Wire, + refFn: (ref: NodeRef) => string, + valFn: (v: string) => string = (v) => v, +): string { + if (w.sources.length <= 1) return ""; + return w.sources + .slice(1) + .map((s) => { + const op = s.gate === "nullish" ? "??" : "||"; + const e = s.expr; + if (e.type === "control") return ` ${op} ${serializeControl(e.control)}`; + if (e.type === "ref") return ` ${op} ${refFn(e.ref)}`; + if (e.type === "literal") return ` ${op} ${valFn(e.value)}`; + return ""; + }) + .join(""); +} + +/** Serialize catch handler as ` catch `. */ +function serCatch( + w: Wire, + refFn: (ref: NodeRef) => string, + valFn: (v: string) => string = (v) => v, +): string { + if (!w.catch) return ""; + if ("control" in w.catch) + return ` catch ${serializeControl(w.catch.control)}`; + if ("ref" in w.catch) return ` catch ${refFn(w.catch.ref)}`; + return ` catch ${valFn(w.catch.value)}`; +} + // ── Serializer ─────────────────────────────────────────────────────────────── export function serializeBridge(doc: BridgeDocument): string { @@ -143,9 +205,10 @@ function formatExprValue(v: string): string { } function serializeToolBlock(tool: ToolDef): string { + const toolWires: Wire[] = tool.wires; const lines: string[] = []; const hasBody = - tool.handles.length > 0 || tool.wires.length > 0 || !!tool.onError; + tool.handles.length > 0 || toolWires.length > 0 || !!tool.onError; // Declaration line — use `tool from ` format const source = tool.extends ?? tool.fn; @@ -206,7 +269,7 @@ function serializeToolBlock(tool: ToolDef): string { // Expression fork info type ToolExprForkInfo = { op: string; - aWire: Extract | undefined; + aWire: Wire | undefined; bWire: Wire | undefined; }; const exprForks = new Map(); @@ -229,13 +292,12 @@ function serializeToolBlock(tool: ToolDef): string { if (ph.handle.startsWith("__expr_")) { const op = TOOL_FN_TO_OP[ph.baseTrunk.field]; if (!op) continue; - let aWire: Extract | undefined; + let aWire: Wire | undefined; let bWire: Wire | undefined; - for (const w of tool.wires) { + for (const w of toolWires) { const wTo = w.to; if (refTk(wTo) !== ph.key || wTo.path.length !== 1) continue; - if (wTo.path[0] === "a" && "from" in w) - aWire = w as Extract; + if (wTo.path[0] === "a" && isPull(w)) aWire = w as Wire; else if (wTo.path[0] === "b") bWire = w; } exprForks.set(ph.key, { op, aWire, bWire }); @@ -249,18 +311,18 @@ function serializeToolBlock(tool: ToolDef): string { number, { kind: "text"; value: string } | { kind: "ref"; ref: NodeRef } >(); - for (const w of tool.wires) { + for (const w of toolWires) { const wTo = w.to; if (refTk(wTo) !== ph.key) continue; if (wTo.path.length !== 2 || wTo.path[0] !== "parts") continue; const idx = parseInt(wTo.path[1], 10); if (isNaN(idx)) continue; - if ("value" in w && !("from" in w)) { - partsMap.set(idx, { kind: "text", value: (w as any).value }); - } else if ("from" in w) { + if (isLit(w) && !isPull(w)) { + partsMap.set(idx, { kind: "text", value: wVal(w) }); + } else if (isPull(w)) { partsMap.set(idx, { kind: "ref", - ref: (w as Extract).from, + ref: wRef(w), }); } concatInternalWires.add(w); @@ -276,11 +338,11 @@ function serializeToolBlock(tool: ToolDef): string { } // Mark output wires from expression/concat forks as internal - for (const w of tool.wires) { - if (!("from" in w)) continue; - const fromTk = refTk(w.from); + for (const w of toolWires) { + if (!isPull(w)) continue; + const fromTk = refTk(wRef(w)); if ( - w.from.path.length === 0 && + wRef(w).path.length === 0 && (exprForks.has(fromTk) || concatForks.has(fromTk)) ) { // This is the output wire from a fork to the tool's self-wire target. @@ -305,14 +367,14 @@ function serializeToolBlock(tool: ToolDef): string { // Reconstruct left operand let left: string; if (info.aWire) { - const aFromTk = refTk(info.aWire.from); + const aFromTk = refTk(wRef(info.aWire!)); if (exprForks.has(aFromTk)) { left = reconstructExpr( aFromTk, TOOL_PREC[info.op as keyof typeof TOOL_PREC], ); } else { - left = serToolRef(info.aWire.from); + left = serToolRef(wRef(info.aWire!)); } } else { left = "?"; @@ -321,22 +383,18 @@ function serializeToolBlock(tool: ToolDef): string { // Reconstruct right operand let right: string; if (info.bWire) { - if ("from" in info.bWire) { - const bFromTk = refTk( - (info.bWire as Extract).from, - ); + if (isPull(info.bWire)) { + const bFromTk = refTk(wRef(info.bWire!)); if (exprForks.has(bFromTk)) { right = reconstructExpr( bFromTk, TOOL_PREC[info.op as keyof typeof TOOL_PREC], ); } else { - right = serToolRef( - (info.bWire as Extract).from, - ); + right = serToolRef(wRef(info.bWire!)); } - } else if ("value" in info.bWire) { - right = formatExprValue((info.bWire as any).value); + } else if (isLit(info.bWire)) { + right = formatExprValue(wVal(info.bWire!)); } else { right = "?"; } @@ -381,7 +439,7 @@ function serializeToolBlock(tool: ToolDef): string { // Wires — self-wires (targeting the tool's own trunk) get `.` prefix; // handle-targeted wires (targeting declared handles) use bare target names - for (const wire of tool.wires) { + for (const wire of toolWires) { // Skip internal expression/concat wires if (exprInternalWires.has(wire) || concatInternalWires.has(wire)) continue; @@ -392,61 +450,53 @@ function serializeToolBlock(tool: ToolDef): string { const prefix = isSelfWire ? "." : ""; // Check if this wire's source is an expression or concat fork - if ("from" in wire) { - const fromTk = refTk(wire.from); + if (isPull(wire)) { + const fromTk = refTk(wRef(wire)); // Expression fork output wire - if (wire.from.path.length === 0 && exprForks.has(fromTk)) { + if (wRef(wire).path.length === 0 && exprForks.has(fromTk)) { const target = wire.to.path.join("."); const exprStr = reconstructExpr(fromTk); // Check for ternary, coalesce, fallbacks, catch on the wire let suffix = ""; - if ("cond" in wire) { - const condWire = wire as any; + if (isTern(wire)) { + const tern = wTern(wire); const trueVal = - "trueValue" in condWire - ? formatBareValue(condWire.trueValue) - : serToolRef(condWire.trueRef); + tern.then.type === "literal" + ? formatBareValue(eVal(tern.then)) + : serToolRef(eRef(tern.then)); const falseVal = - "falseValue" in condWire - ? formatBareValue(condWire.falseValue) - : serToolRef(condWire.falseRef); + tern.else.type === "literal" + ? formatBareValue(eVal(tern.else)) + : serToolRef(eRef(tern.else)); lines.push( ` ${prefix}${target} <- ${exprStr} ? ${trueVal} : ${falseVal}`, ); continue; } - if ((wire as any).nullCoalesceRef) { - suffix = ` ?? ${serToolRef((wire as any).nullCoalesceRef)}`; - } else if ((wire as any).nullCoalesceValue != null) { - suffix = ` ?? ${formatBareValue((wire as any).nullCoalesceValue)}`; - } - if ((wire as any).catchFallbackRef) { - suffix += ` catch ${serToolRef((wire as any).catchFallbackRef)}`; - } else if ((wire as any).catchFallback != null) { - suffix += ` catch ${formatBareValue((wire as any).catchFallback)}`; - } + suffix += serFallbacks(wire, serToolRef, formatBareValue); + suffix += serCatch(wire, serToolRef, formatBareValue); lines.push(` ${prefix}${target} <- ${exprStr}${suffix}`); continue; } // Concat fork output wire (template string) if ( - wire.from.path.length <= 1 && + wRef(wire).path.length <= 1 && concatForks.has( - wire.from.path.length === 0 + wRef(wire).path.length === 0 ? fromTk - : refTk({ ...wire.from, path: [] }), + : refTk({ ...wRef(wire), path: [] }), ) ) { const concatTk = - wire.from.path.length === 0 + wRef(wire).path.length === 0 ? fromTk - : refTk({ ...wire.from, path: [] }); + : refTk({ ...wRef(wire), path: [] }); // Only handle .value path (standard concat output) if ( - wire.from.path.length === 0 || - (wire.from.path.length === 1 && wire.from.path[0] === "value") + wRef(wire).path.length === 0 || + (wRef(wire).path.length === 1 && wRef(wire).path[0] === "value") ) { const target = wire.to.path.join("."); const tmpl = reconstructTemplateStr(concatTk); @@ -458,67 +508,45 @@ function serializeToolBlock(tool: ToolDef): string { } // Skip internal pipe wires (targeting fork inputs) - if ((wire as any).pipe && pipeHandleTrunkKeys.has(refTk(wire.to))) { + if (wire.pipe && pipeHandleTrunkKeys.has(refTk(wire.to))) { continue; } } // Ternary wire: has `cond` (condition ref), `thenValue`/`thenRef`, `elseValue`/`elseRef` - if ("cond" in wire) { - const condWire = wire as any; + if (isTern(wire)) { + const tern = wTern(wire); const target = wire.to.path.join("."); - const condStr = serToolRef(condWire.cond); + const condStr = serToolRef(eRef(tern.cond)); const thenVal = - "thenValue" in condWire - ? formatBareValue(condWire.thenValue) - : serToolRef(condWire.thenRef); + tern.then.type === "literal" + ? formatBareValue(eVal(tern.then)) + : serToolRef(eRef(tern.then)); const elseVal = - "elseValue" in condWire - ? formatBareValue(condWire.elseValue) - : serToolRef(condWire.elseRef); + tern.else.type === "literal" + ? formatBareValue(eVal(tern.else)) + : serToolRef(eRef(tern.else)); lines.push( ` ${prefix}${target} <- ${condStr} ? ${thenVal} : ${elseVal}`, ); continue; } - if ("value" in wire && !("cond" in wire)) { + if (isLit(wire) && !isTern(wire)) { // Constant wire const target = wire.to.path.join("."); - if (needsQuoting(wire.value)) { - lines.push(` ${prefix}${target} = "${wire.value}"`); + if (needsQuoting(wVal(wire))) { + lines.push(` ${prefix}${target} = "${wVal(wire)}"`); } else { - lines.push(` ${prefix}${target} = ${formatBareValue(wire.value)}`); + lines.push(` ${prefix}${target} = ${formatBareValue(wVal(wire))}`); } - } else if ("from" in wire) { + } else if (isPull(wire)) { // Pull wire — reconstruct source from handle map - const sourceStr = serializeToolWireSource(wire.from, tool); + const sourceStr = serializeToolWireSource(wRef(wire), tool); const target = wire.to.path.join("."); let suffix = ""; - // Fallbacks: || (or) and ?? (nullish coalesce) - const fallbacks = (wire as any).fallbacks as - | Array<{ - type: "or" | "nullish"; - value?: string; - ref?: NodeRef; - }> - | undefined; - if (fallbacks) { - for (const fb of fallbacks) { - const op = fb.type === "nullish" ? "??" : "||"; - if (fb.ref) { - suffix += ` ${op} ${serToolRef(fb.ref)}`; - } else if (fb.value != null) { - suffix += ` ${op} ${formatBareValue(fb.value)}`; - } - } - } - // Catch - if ((wire as any).catchFallbackRef) { - suffix += ` catch ${serToolRef((wire as any).catchFallbackRef)}`; - } else if ((wire as any).catchFallback != null) { - suffix += ` catch ${formatBareValue((wire as any).catchFallback)}`; - } + suffix += serFallbacks(wire, serToolRef, formatBareValue); + suffix += serCatch(wire, serToolRef, formatBareValue); lines.push(` ${prefix}${target} <- ${sourceStr}${suffix}`); } } @@ -602,7 +630,7 @@ function serializeToolWireSource(ref: NodeRef, tool: ToolDef): string { function serializePipeOrRef( ref: NodeRef, pipeHandleTrunkKeys: Set, - toInMap: Map>, + toInMap: Map, handleMap: Map, bridge: Bridge, inputHandle: string | undefined, @@ -629,13 +657,13 @@ function serializePipeOrRef( handleChain.push(token); if (!inWire) break; const fromTk = - inWire.from.instance != null - ? `${inWire.from.module}:${inWire.from.type}:${inWire.from.field}:${inWire.from.instance}` - : `${inWire.from.module}:${inWire.from.type}:${inWire.from.field}`; - if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { + wRef(inWire).instance != null + ? `${wRef(inWire).module}:${wRef(inWire).type}:${wRef(inWire).field}:${wRef(inWire).instance}` + : `${wRef(inWire).module}:${wRef(inWire).type}:${wRef(inWire).field}`; + if (wRef(inWire).path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { currentTk = fromTk; } else { - actualSourceRef = inWire.from; + actualSourceRef = wRef(inWire); break; } } @@ -678,6 +706,8 @@ function serializeDefineBlock(def: DefineDef): string { } function serializeBridgeBlock(bridge: Bridge): string { + const bridgeWires: Wire[] = bridge.wires; + // ── Passthrough shorthand ─────────────────────────────────────────── if (bridge.passthrough) { return `bridge ${bridge.type}.${bridge.field} with ${bridge.passthrough}`; @@ -718,9 +748,7 @@ function serializeBridgeBlock(bridge: Bridge): string { if (h2.name.lastIndexOf(".") === -1 && h2.name === h.name) inst++; if (h2 === h) break; } - defineInlinedTrunkKeys.add( - `${SELF_MODULE}:Tools:${h.name}:${inst}`, - ); + defineInlinedTrunkKeys.add(`${SELF_MODULE}:Tools:${h.name}:${inst}`); } } } @@ -728,10 +756,10 @@ function serializeBridgeBlock(bridge: Bridge): string { // Detect element-scoped define handles: defines whose __define_in_ wires // originate from element scope (i.e., the define is used inside an array block) const elementScopedDefines = new Set(); - for (const w of bridge.wires) { + for (const w of bridgeWires) { if ( - "from" in w && - w.from.element && + isPull(w) && + wRef(w).element && w.to.module.startsWith("__define_in_") ) { const defineHandle = w.to.module.substring("__define_in_".length); @@ -839,27 +867,27 @@ function serializeBridgeBlock(bridge: Bridge): string { ? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}` : `${ref.module}:${ref.type}:${ref.field}`; - type FW = Extract; + type FW = Wire; const toInMap = new Map(); const fromOutMap = new Map(); const pipeWireSet = new Set(); - for (const w of bridge.wires) { - if (!("from" in w) || !(w as any).pipe) continue; + for (const w of bridgeWires) { + if (!isPull(w) || !w.pipe) continue; const fw = w as FW; pipeWireSet.add(w); const toTk = refTrunkKey(fw.to); if (fw.to.path.length === 1 && pipeHandleTrunkKeys.has(toTk)) { toInMap.set(toTk, fw); } - const fromTk = refTrunkKey(fw.from); - if (fw.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { + const fromTk = refTrunkKey(wRef(fw)); + if (wRef(fw).path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { fromOutMap.set(fromTk, fw); } // Concat fork output: from.path=["value"], target is not a pipe handle if ( - fw.from.path.length === 1 && - fw.from.path[0] === "value" && + wRef(fw).path.length === 1 && + wRef(fw).path[0] === "value" && pipeHandleTrunkKeys.has(fromTk) && !pipeHandleTrunkKeys.has(toTk) ) { @@ -905,9 +933,7 @@ function serializeBridgeBlock(bridge: Bridge): string { bWire: Wire | undefined; aWire: FW | undefined; /** For condAnd/condOr wires: the logic wire itself */ - logicWire?: - | Extract - | Extract; + logicWire?: Wire | Wire; }; const exprForks = new Map(); const exprPipeWireSet = new Set(); // wires that belong to expression forks @@ -919,13 +945,11 @@ function serializeBridgeBlock(bridge: Bridge): string { // For condAnd/condOr wires (field === "__and" or "__or") if (ph.baseTrunk.field === "__and" || ph.baseTrunk.field === "__or") { - const logicWire = bridge.wires.find((w) => { - const prop = ph.baseTrunk.field === "__and" ? "condAnd" : "condOr"; - return prop in w && refTrunkKey(w.to) === ph.key; - }) as - | Extract - | Extract - | undefined; + const isAndField = ph.baseTrunk.field === "__and"; + const logicWire = bridgeWires.find( + (w) => + (isAndField ? isAndW(w) : isOrW(w)) && refTrunkKey(w.to) === ph.key, + ) as Wire | undefined; if (logicWire) { exprForks.set(ph.key, { @@ -942,11 +966,11 @@ function serializeBridgeBlock(bridge: Bridge): string { // Find the .a and .b wires for this fork let aWire: FW | undefined; let bWire: Wire | undefined; - for (const w of bridge.wires) { - const wTo = (w as any).to as NodeRef; + for (const w of bridgeWires) { + const wTo = w.to as NodeRef; if (!wTo || refTrunkKey(wTo) !== ph.key || wTo.path.length !== 1) continue; - if (wTo.path[0] === "a" && "from" in w) aWire = w as FW; + if (wTo.path[0] === "a" && isPull(w)) aWire = w as FW; else if (wTo.path[0] === "b") bWire = w; } exprForks.set(ph.key, { op, bWire, aWire }); @@ -972,16 +996,16 @@ function serializeBridgeBlock(bridge: Bridge): string { number, { kind: "text"; value: string } | { kind: "ref"; ref: NodeRef } >(); - for (const w of bridge.wires) { - const wTo = (w as any).to as NodeRef; + for (const w of bridgeWires) { + const wTo = w.to as NodeRef; if (!wTo || refTrunkKey(wTo) !== ph.key) continue; if (wTo.path.length !== 2 || wTo.path[0] !== "parts") continue; const idx = parseInt(wTo.path[1], 10); if (isNaN(idx)) continue; - if ("value" in w && !("from" in w)) { - partsMap.set(idx, { kind: "text", value: (w as any).value }); - } else if ("from" in w) { - partsMap.set(idx, { kind: "ref", ref: (w as FW).from }); + if (isLit(w) && !isPull(w)) { + partsMap.set(idx, { kind: "text", value: wVal(w) }); + } else if (isPull(w)) { + partsMap.set(idx, { kind: "ref", ref: wRef(w) }); } concatPipeWireSet.add(w); } @@ -1023,14 +1047,14 @@ function serializeBridgeBlock(bridge: Bridge): string { // Pull wires: from.element=true OR involving element-scoped tools // OR define-output wires targeting an array-scoped bridge path const isElementToolWire = (w: Wire): boolean => { - if (!("from" in w)) return false; - if (elementToolTrunkKeys.has(refTrunkKey(w.from))) return true; + if (!isPull(w)) return false; + if (elementToolTrunkKeys.has(refTrunkKey(wRef(w)))) return true; if (elementToolTrunkKeys.has(refTrunkKey(w.to))) return true; return false; }; const isDefineOutElementWire = (w: Wire): boolean => { - if (!("from" in w)) return false; - if (!w.from.module.startsWith("__define_out_")) return false; + if (!isPull(w)) return false; + if (!wRef(w).module.startsWith("__define_out_")) return false; // Check if target is a bridge trunk path under any array iterator const to = w.to; if ( @@ -1046,15 +1070,14 @@ function serializeBridgeBlock(bridge: Bridge): string { } return false; }; - const elementPullWires = bridge.wires.filter( - (w): w is Extract => - "from" in w && - (!!w.from.element || isElementToolWire(w) || isDefineOutElementWire(w)), + const elementPullWires = bridgeWires.filter( + (w): w is Wire => + isPull(w) && + (!!wRef(w).element || isElementToolWire(w) || isDefineOutElementWire(w)), ); - // Constant wires: "value" in w && to.element=true - const elementConstWires = bridge.wires.filter( - (w): w is Extract => - "value" in w && !!w.to.element, + // Constant wires: isLit(w) && to.element=true + const elementConstWires = bridgeWires.filter( + (w): w is Wire => isLit(w) && !!w.to.element, ); // Build grouped maps keyed by the full array-destination path (to.path joined) @@ -1145,9 +1168,9 @@ function serializeBridgeBlock(bridge: Bridge): string { ); // For each element tool, find its output wire to determine scope - for (const w of bridge.wires) { - if (!("from" in w)) continue; - const fromTk = refTrunkKey(w.from); + for (const w of bridgeWires) { + if (!isPull(w)) continue; + const fromTk = refTrunkKey(wRef(w)); if (!elementToolTrunkKeys.has(fromTk)) continue; if (elementToolScope.has(fromTk)) continue; // Output wire: from=tool → to=bridge output @@ -1196,26 +1219,26 @@ function serializeBridgeBlock(bridge: Bridge): string { const toIsDefine = isDefineBoundaryModule(w.to.module) || isDefineInlinedRef(w.to); if (!toIsDefine) return false; - if (!("from" in w)) return false; - const fromRef = (w as any).from as NodeRef; + if (!isPull(w)) return false; + const fromRef = wRef(w) as NodeRef; return ( isDefineBoundaryModule(fromRef.module) || isDefineInlinedRef(fromRef) ); }; // ── Exclude pipe, element-pull, element-const, expression-internal, concat-internal, __local, define-internal, and element-scoped ternary wires from main loop - const regularWires = bridge.wires.filter( + const regularWires = bridgeWires.filter( (w) => !pipeWireSet.has(w) && !exprPipeWireSet.has(w) && !concatPipeWireSet.has(w) && - (!("from" in w) || !w.from.element) && + (!isPull(w) || !wRef(w).element) && !isElementToolWire(w) && - (!("value" in w) || !w.to.element) && + (!isLit(w) || !w.to.element) && w.to.module !== "__local" && - (!("from" in w) || (w.from as NodeRef).module !== "__local") && - (!("cond" in w) || !isUnderArrayScope(w.to)) && - (!("from" in w) || !isDefineInlinedRef((w as any).from)) && + (!isPull(w) || (wRef(w) as NodeRef).module !== "__local") && + (!isTern(w) || !isUnderArrayScope(w.to)) && + (!isPull(w) || !isDefineInlinedRef(wRef(w))) && !isDefineInlinedRef(w.to) && !isDefineOutElementWire(w) && !isDefineInternalWire(w), @@ -1224,33 +1247,32 @@ function serializeBridgeBlock(bridge: Bridge): string { // ── Collect __local binding wires for array-scoped `with` declarations ── type LocalBindingInfo = { alias: string; - sourceWire?: Extract; - ternaryWire?: Extract; + sourceWire?: Wire; + ternaryWire?: Wire; }; const localBindingsByAlias = new Map(); - const localReadWires: Extract[] = []; - for (const w of bridge.wires) { - if (w.to.module === "__local" && "from" in w) { + const localReadWires: Wire[] = []; + for (const w of bridgeWires) { + if (w.to.module === "__local" && isPull(w)) { localBindingsByAlias.set(w.to.field, { alias: w.to.field, - sourceWire: w as Extract, + sourceWire: w as Wire, }); } - if (w.to.module === "__local" && "cond" in w) { + if (w.to.module === "__local" && isTern(w)) { localBindingsByAlias.set(w.to.field, { alias: w.to.field, - ternaryWire: w as Extract, + ternaryWire: w as Wire, }); } - if ("from" in w && (w.from as NodeRef).module === "__local") { - localReadWires.push(w as Extract); + if (isPull(w) && (wRef(w) as NodeRef).module === "__local") { + localReadWires.push(w as Wire); } } // ── Collect element-scoped ternary wires ──────────────────────────── - const elementTernaryWires = bridge.wires.filter( - (w): w is Extract => - "cond" in w && isUnderArrayScope(w.to), + const elementTernaryWires = bridgeWires.filter( + (w): w is Wire => isTern(w) && isUnderArrayScope(w.to), ); const serializedArrays = new Set(); @@ -1284,36 +1306,33 @@ function serializeBridgeBlock(bridge: Bridge): string { // condAnd/condOr logic wire — reconstruct from leftRef/rightRef if (info.logicWire) { - const logic = - "condAnd" in info.logicWire - ? info.logicWire.condAnd - : info.logicWire.condOr; + const logic = wAndOr(info.logicWire!); let leftStr: string; - const leftTk = refTrunkKey(logic.leftRef); - if (logic.leftRef.path.length === 0 && exprForks.has(leftTk)) { + const leftTk = refTrunkKey(eRef(logic.left)); + if (eRef(logic.left).path.length === 0 && exprForks.has(leftTk)) { leftStr = serializeElemExprTree(leftTk, OP_PREC_SER[info.op] ?? 0) ?? - sRef(logic.leftRef, true); + sRef(eRef(logic.left), true); } else { - leftStr = logic.leftRef.element - ? "ITER." + serPath(logic.leftRef.path) - : sRef(logic.leftRef, true); + leftStr = eRef(logic.left).element + ? "ITER." + serPath(eRef(logic.left).path) + : sRef(eRef(logic.left), true); } let rightStr: string; - if (logic.rightRef) { - const rightTk = refTrunkKey(logic.rightRef); - if (logic.rightRef.path.length === 0 && exprForks.has(rightTk)) { + if (logic.right.type === "ref") { + const rightTk = refTrunkKey(eRef(logic.right)); + if (eRef(logic.right).path.length === 0 && exprForks.has(rightTk)) { rightStr = serializeElemExprTree(rightTk, OP_PREC_SER[info.op] ?? 0) ?? - sRef(logic.rightRef, true); + sRef(eRef(logic.right), true); } else { - rightStr = logic.rightRef.element - ? "ITER." + serPath(logic.rightRef.path) - : sRef(logic.rightRef, true); + rightStr = eRef(logic.right).element + ? "ITER." + serPath(eRef(logic.right).path) + : sRef(eRef(logic.right), true); } - } else if (logic.rightValue != null) { - rightStr = formatExprValue(logic.rightValue); + } else if (logic.right.type === "literal") { + rightStr = formatExprValue(eVal(logic.right)); } else { rightStr = "0"; } @@ -1326,21 +1345,21 @@ function serializeBridgeBlock(bridge: Bridge): string { let leftStr: string | null = null; if (info.aWire) { - const fromTk = refTrunkKey(info.aWire.from); - if (info.aWire.from.path.length === 0 && exprForks.has(fromTk)) { + const fromTk = refTrunkKey(wRef(info.aWire!)); + if (wRef(info.aWire!).path.length === 0 && exprForks.has(fromTk)) { leftStr = serializeElemExprTree(fromTk, OP_PREC_SER[info.op] ?? 0); } else { - leftStr = info.aWire.from.element - ? "ITER." + serPath(info.aWire.from.path) - : sRef(info.aWire.from, true); + leftStr = wRef(info.aWire!).element + ? "ITER." + serPath(wRef(info.aWire!).path) + : sRef(wRef(info.aWire!), true); } } let rightStr: string; - if (info.bWire && "value" in info.bWire) { - rightStr = formatExprValue(info.bWire.value); - } else if (info.bWire && "from" in info.bWire) { - const bFrom = (info.bWire as FW).from; + if (info.bWire && isLit(info.bWire)) { + rightStr = formatExprValue(wVal(info.bWire!)); + } else if (info.bWire && isPull(info.bWire)) { + const bFrom = wRef(info.bWire!); const bTk = refTrunkKey(bFrom); if (bFrom.path.length === 0 && exprForks.has(bTk)) { rightStr = @@ -1402,38 +1421,24 @@ function serializeBridgeBlock(bridge: Bridge): string { fieldName === "in" ? handleName : `${handleName}.${fieldName}`; handleChain.push(token); if (!inWire) break; - if (inWire.from.element) { + if (wRef(inWire).element) { sourceStr = - inWire.from.path.length > 0 - ? "ITER." + serPath(inWire.from.path) + wRef(inWire).path.length > 0 + ? "ITER." + serPath(wRef(inWire).path) : "ITER"; break; } - const fromTk = refTrunkKey(inWire.from); - if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { + const fromTk = refTrunkKey(wRef(inWire)); + if (wRef(inWire).path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { currentTk = fromTk; } else { - sourceStr = sRef(inWire.from, true); + sourceStr = sRef(wRef(inWire), true); break; } } if (sourceStr && handleChain.length > 0) { - const fallbackStr = (outWire.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in outWire && outWire.catchControl - ? ` catch ${serializeControl(outWire.catchControl)}` - : outWire.catchFallbackRef - ? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}` - : outWire.catchFallback - ? ` catch ${outWire.catchFallback}` - : ""; + const fallbackStr = serFallbacks(outWire, sPipeOrRef); + const errf = serCatch(outWire, sPipeOrRef); elementPipeWires.push({ toPath: outWire.to.path, sourceStr: `${handleChain.join(":")}:${sourceStr}`, @@ -1484,13 +1489,10 @@ function serializeBridgeBlock(bridge: Bridge): string { if (!info) return null; if (info.logicWire) { - const logic = - "condAnd" in info.logicWire - ? info.logicWire.condAnd - : info.logicWire.condOr; + const logic = wAndOr(info.logicWire!); let leftStr: string; - const leftTk = refTrunkKey(logic.leftRef); - if (logic.leftRef.path.length === 0 && exprForks.has(leftTk)) { + const leftTk = refTrunkKey(eRef(logic.left)); + if (eRef(logic.left).path.length === 0 && exprForks.has(leftTk)) { leftStr = serializeElemExprTreeFn( leftTk, @@ -1498,19 +1500,19 @@ function serializeBridgeBlock(bridge: Bridge): string { ancestorIterNames, OP_PREC_SER[info.op] ?? 0, ) ?? - serializeElemRef(logic.leftRef, parentIterName, ancestorIterNames); + serializeElemRef(eRef(logic.left), parentIterName, ancestorIterNames); } else { leftStr = serializeElemRef( - logic.leftRef, + eRef(logic.left), parentIterName, ancestorIterNames, ); } let rightStr: string; - if (logic.rightRef) { - const rightTk = refTrunkKey(logic.rightRef); - if (logic.rightRef.path.length === 0 && exprForks.has(rightTk)) { + if (logic.right.type === "ref") { + const rightTk = refTrunkKey(eRef(logic.right)); + if (eRef(logic.right).path.length === 0 && exprForks.has(rightTk)) { rightStr = serializeElemExprTreeFn( rightTk, @@ -1518,16 +1520,20 @@ function serializeBridgeBlock(bridge: Bridge): string { ancestorIterNames, OP_PREC_SER[info.op] ?? 0, ) ?? - serializeElemRef(logic.rightRef, parentIterName, ancestorIterNames); + serializeElemRef( + eRef(logic.right), + parentIterName, + ancestorIterNames, + ); } else { rightStr = serializeElemRef( - logic.rightRef, + eRef(logic.right), parentIterName, ancestorIterNames, ); } - } else if (logic.rightValue != null) { - rightStr = formatExprValue(logic.rightValue); + } else if (logic.right.type === "literal") { + rightStr = formatExprValue(eVal(logic.right)); } else { rightStr = "0"; } @@ -1540,8 +1546,8 @@ function serializeBridgeBlock(bridge: Bridge): string { let leftStr: string | null = null; if (info.aWire) { - const fromTk = refTrunkKey(info.aWire.from); - if (info.aWire.from.path.length === 0 && exprForks.has(fromTk)) { + const fromTk = refTrunkKey(wRef(info.aWire!)); + if (wRef(info.aWire!).path.length === 0 && exprForks.has(fromTk)) { leftStr = serializeElemExprTreeFn( fromTk, parentIterName, @@ -1550,7 +1556,7 @@ function serializeBridgeBlock(bridge: Bridge): string { ); } else { leftStr = serializeElemRef( - info.aWire.from, + wRef(info.aWire!), parentIterName, ancestorIterNames, ); @@ -1558,10 +1564,10 @@ function serializeBridgeBlock(bridge: Bridge): string { } let rightStr: string; - if (info.bWire && "value" in info.bWire) { - rightStr = formatExprValue(info.bWire.value); - } else if (info.bWire && "from" in info.bWire) { - const bFrom = (info.bWire as FW).from; + if (info.bWire && isLit(info.bWire)) { + rightStr = formatExprValue(wVal(info.bWire!)); + } else if (info.bWire && isPull(info.bWire)) { + const bFrom = wRef(info.bWire!); const bTk = refTrunkKey(bFrom); if (bFrom.path.length === 0 && exprForks.has(bTk)) { rightStr = @@ -1616,7 +1622,7 @@ function serializeBridgeBlock(bridge: Bridge): string { return elementToolScope.get(ewToTk) === arrayPathStr; } // Tool-output wires: include if the tool belongs to this scope - const ewFromTk = refTrunkKey(ew.from); + const ewFromTk = refTrunkKey(wRef(ew)); if (elementToolTrunkKeys.has(ewFromTk)) { return elementToolScope.get(ewFromTk) === arrayPathStr; } @@ -1650,32 +1656,28 @@ function serializeBridgeBlock(bridge: Bridge): string { if (info.ternaryWire) { const tw = info.ternaryWire; const condStr = serializeElemRef( - tw.cond, + eRef(wTern(tw).cond), parentIterName, ancestorIterNames, ); - const thenStr = tw.thenRef - ? serializeElemRef(tw.thenRef, parentIterName, ancestorIterNames) - : (tw.thenValue ?? "null"); - const elseStr = tw.elseRef - ? serializeElemRef(tw.elseRef, parentIterName, ancestorIterNames) - : (tw.elseValue ?? "null"); - const fallbackStr = (tw.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in tw && tw.catchControl - ? ` catch ${serializeControl(tw.catchControl)}` - : tw.catchFallbackRef - ? ` catch ${sPipeOrRef(tw.catchFallbackRef)}` - : tw.catchFallback - ? ` catch ${tw.catchFallback}` - : ""; + const thenStr = + wTern(tw).then.type === "ref" + ? serializeElemRef( + eRef(wTern(tw).then), + parentIterName, + ancestorIterNames, + ) + : (eVal(wTern(tw).then) ?? "null"); + const elseStr = + wTern(tw).else.type === "ref" + ? serializeElemRef( + eRef(wTern(tw).else), + parentIterName, + ancestorIterNames, + ) + : (eVal(wTern(tw).else) ?? "null"); + const fallbackStr = serFallbacks(tw, sPipeOrRef); + const errf = serCatch(tw, sPipeOrRef); lines.push( `${indent}alias ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf} as ${alias}`, ); @@ -1683,7 +1685,7 @@ function serializeBridgeBlock(bridge: Bridge): string { } const srcWire = info.sourceWire!; // Reconstruct the source expression - const fromRef = srcWire.from; + const fromRef = wRef(srcWire); // Determine if this alias is element-scoped (skip top-level aliases) let isElementScoped = fromRef.element; @@ -1695,13 +1697,13 @@ function serializeBridgeBlock(bridge: Bridge): string { while (true) { const inWire = toInMap.get(walkTk); if (!inWire) break; - if (inWire.from.element) { + if (wRef(inWire).element) { isElementScoped = true; break; } - const innerTk = refTrunkKey(inWire.from); + const innerTk = refTrunkKey(wRef(inWire)); if ( - inWire.from.path.length === 0 && + wRef(inWire).path.length === 0 && pipeHandleTrunkKeys.has(innerTk) ) { walkTk = innerTk; @@ -1742,23 +1744,23 @@ function serializeBridgeBlock(bridge: Bridge): string { parts.push(handleName); const inWire = toInMap.get(currentTk); if (!inWire) break; - if (inWire.from.element) { + if (wRef(inWire).element) { parts.push( parentIterName + - (inWire.from.path.length > 0 - ? "." + serPath(inWire.from.path) + (wRef(inWire).path.length > 0 + ? "." + serPath(wRef(inWire).path) : ""), ); break; } - const innerTk = refTrunkKey(inWire.from); + const innerTk = refTrunkKey(wRef(inWire)); if ( - inWire.from.path.length === 0 && + wRef(inWire).path.length === 0 && pipeHandleTrunkKeys.has(innerTk) ) { currentTk = innerTk; } else { - parts.push(sRef(inWire.from, true)); + parts.push(sRef(wRef(inWire), true)); break; } } @@ -1803,7 +1805,7 @@ function serializeBridgeBlock(bridge: Bridge): string { for (const ew of levelConsts) { const fieldPath = ew.to.path.slice(pathDepth); const elemTo = "." + serPath(fieldPath); - lines.push(`${indent}${elemTo} = ${formatBareValue(ew.value)}`); + lines.push(`${indent}${elemTo} = ${formatBareValue(wVal(ew))}`); } // Emit pull element wires (direct level only) @@ -1822,14 +1824,14 @@ function serializeBridgeBlock(bridge: Bridge): string { serializedArrays.add(toPathStr); const nestedIterName = arrayIterators[toPathStr]; let nestedFromIter = parentIterName; - if (ew.from.element && ew.from.elementDepth) { + if (wRef(ew).element && wRef(ew).elementDepth) { const stack = [...ancestorIterNames, parentIterName]; - const idx = stack.length - 1 - ew.from.elementDepth; + const idx = stack.length - 1 - wRef(ew).elementDepth!; if (idx >= 0) nestedFromIter = stack[idx]; } - const fromPart = ew.from.element - ? nestedFromIter + "." + serPath(ew.from.path) - : sRef(ew.from, true); + const fromPart = wRef(ew).element + ? nestedFromIter + "." + serPath(wRef(ew).path) + : sRef(wRef(ew), true); const fieldPath = ew.to.path.slice(pathDepth); const elemTo = "." + serPath(fieldPath); lines.push( @@ -1845,15 +1847,15 @@ function serializeBridgeBlock(bridge: Bridge): string { // Regular element pull wire let resolvedIterName = parentIterName; - if (ew.from.element && ew.from.elementDepth) { + if (wRef(ew).element && wRef(ew).elementDepth) { const stack = [...ancestorIterNames, parentIterName]; - const idx = stack.length - 1 - ew.from.elementDepth; + const idx = stack.length - 1 - wRef(ew).elementDepth!; if (idx >= 0) resolvedIterName = stack[idx]; } - const fromPart = ew.from.element + const fromPart = wRef(ew).element ? resolvedIterName + - (ew.from.path.length > 0 ? "." + serPath(ew.from.path) : "") - : sRef(ew.from, true); + (wRef(ew).path.length > 0 ? "." + serPath(wRef(ew).path) : "") + : sRef(wRef(ew), true); // Tool input or define-in wires target a scoped handle const toTk = refTrunkKey(ew.to); const toToolHandle = @@ -1866,22 +1868,8 @@ function serializeBridgeBlock(bridge: Bridge): string { (ew.to.path.length > 0 ? "." + serPath(ew.to.path) : "") : "." + serPath(ew.to.path.slice(pathDepth)); - const fallbackStr = (ew.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in ew && ew.catchControl - ? ` catch ${serializeControl(ew.catchControl)}` - : "catchFallbackRef" in ew && ew.catchFallbackRef - ? ` catch ${sPipeOrRef(ew.catchFallbackRef)}` - : "catchFallback" in ew && ew.catchFallback - ? ` catch ${ew.catchFallback}` - : ""; + const fallbackStr = serFallbacks(ew, sPipeOrRef); + const errf = serCatch(ew, sPipeOrRef); lines.push(`${indent}${elemTo} <- ${fromPart}${fallbackStr}${errf}`); } @@ -1938,32 +1926,28 @@ function serializeBridgeBlock(bridge: Bridge): string { const elemTo = "." + serPath(fieldPath); // Serialize condition — resolve element refs to iterator name const condStr = serializeElemRef( - tw.cond, + eRef(wTern(tw).cond), parentIterName, ancestorIterNames, ); - const thenStr = tw.thenRef - ? serializeElemRef(tw.thenRef, parentIterName, ancestorIterNames) - : (tw.thenValue ?? "null"); - const elseStr = tw.elseRef - ? serializeElemRef(tw.elseRef, parentIterName, ancestorIterNames) - : (tw.elseValue ?? "null"); - const fallbackStr = (tw.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in tw && tw.catchControl - ? ` catch ${serializeControl(tw.catchControl)}` - : tw.catchFallbackRef - ? ` catch ${sPipeOrRef(tw.catchFallbackRef)}` - : tw.catchFallback - ? ` catch ${tw.catchFallback}` - : ""; + const thenStr = + wTern(tw).then.type === "ref" + ? serializeElemRef( + eRef(wTern(tw).then), + parentIterName, + ancestorIterNames, + ) + : (eVal(wTern(tw).then) ?? "null"); + const elseStr = + wTern(tw).else.type === "ref" + ? serializeElemRef( + eRef(wTern(tw).else), + parentIterName, + ancestorIterNames, + ) + : (eVal(wTern(tw).else) ?? "null"); + const fallbackStr = serFallbacks(tw, sPipeOrRef); + const errf = serCatch(tw, sPipeOrRef); lines.push( `${indent}${elemTo} <- ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf}`, ); @@ -1982,13 +1966,13 @@ function serializeBridgeBlock(bridge: Bridge): string { if (!match) continue; const fieldPath = lw.to.path.slice(pathDepth); const elemTo = "." + serPath(fieldPath); - const alias = lw.from.field; // __local:Shadow: - const safeSep = lw.safe || lw.from.rootSafe ? "?." : "."; + const alias = wRef(lw).field; // __local:Shadow: + const safeSep = wSafe(lw) || wRef(lw).rootSafe ? "?." : "."; const fromPart = - lw.from.path.length > 0 + wRef(lw).path.length > 0 ? alias + safeSep + - serPath(lw.from.path, lw.from.rootSafe, lw.from.pathSafe) + serPath(wRef(lw).path, wRef(lw).rootSafe, wRef(lw).pathSafe) : alias; lines.push(`${indent}${elemTo} <- ${fromPart}`); } @@ -2019,21 +2003,24 @@ function serializeBridgeBlock(bridge: Bridge): string { const myPrec = OP_PREC_SER[info.op] ?? 0; let leftStr: string | null = null; if (info.aWire) { - const aTk = refTrunkKey(info.aWire.from); - const concatLeft = tryResolveConcat(info.aWire.from); + const aTk = refTrunkKey(wRef(info.aWire!)); + const concatLeft = tryResolveConcat(wRef(info.aWire!)); if (concatLeft) { leftStr = concatLeft; - } else if (info.aWire.from.path.length === 0 && exprForks.has(aTk)) { + } else if ( + wRef(info.aWire!).path.length === 0 && + exprForks.has(aTk) + ) { leftStr = serFork(aTk, myPrec); } else { - leftStr = sRef(info.aWire.from, true); + leftStr = sRef(wRef(info.aWire!), true); } } let rightStr: string; - if (info.bWire && "value" in info.bWire) { - rightStr = formatExprValue(info.bWire.value); - } else if (info.bWire && "from" in info.bWire) { - const bFrom = (info.bWire as FW).from; + if (info.bWire && isLit(info.bWire)) { + rightStr = formatExprValue(wVal(info.bWire!)); + } else if (info.bWire && isPull(info.bWire)) { + const bFrom = wRef(info.bWire!); const bTk = refTrunkKey(bFrom); const concatRight = tryResolveConcat(bFrom); if (concatRight) { @@ -2062,7 +2049,7 @@ function serializeBridgeBlock(bridge: Bridge): string { // Spread wires must be emitted inside path scope blocks: `target { ...source; .field <- ... }` // Group each spread wire with sibling wires whose to.path extends the spread's to.path. type SpreadGroup = { - spreadWires: Extract[]; + spreadWires: Wire[]; siblingWires: Wire[]; scopePath: string[]; }; @@ -2071,8 +2058,7 @@ function serializeBridgeBlock(bridge: Bridge): string { { const spreadWiresInRegular = regularWires.filter( - (w): w is Extract => - "from" in w && !!w.spread, + (w): w is Wire => isPull(w) && !!w.spread, ); // Group by to.path (scope path) const groupMap = new Map(); @@ -2121,13 +2107,13 @@ function serializeBridgeBlock(bridge: Bridge): string { }, false, ) - : outputHandle ?? "o"; + : (outputHandle ?? "o"); lines.push(`${scopePrefix} {`); // Emit spread lines for (const sw of group.spreadWires) { - let fromStr = sRef(sw.from, true); - if (sw.safe) { - const ref = sw.from; + let fromStr = sRef(wRef(sw), true); + if (wSafe(sw)) { + const ref = wRef(sw); if (!ref.rootSafe && !ref.pathSafe?.some((s) => s)) { if (fromStr.includes(".")) { fromStr = fromStr.replace(".", "?."); @@ -2140,34 +2126,20 @@ function serializeBridgeBlock(bridge: Bridge): string { const scopeLen = group.scopePath.length; for (const w of group.siblingWires) { const relPath = w.to.path.slice(scopeLen); - if ("value" in w) { - lines.push(` .${relPath.join(".")} = ${formatBareValue(w.value)}`); - } else if ("from" in w) { - let fromStr = sRef(w.from, true); - if (w.safe) { - const ref = w.from; + if (isLit(w)) { + lines.push(` .${relPath.join(".")} = ${formatBareValue(wVal(w))}`); + } else if (isPull(w)) { + let fromStr = sRef(wRef(w), true); + if (wSafe(w)) { + const ref = wRef(w); if (!ref.rootSafe && !ref.pathSafe?.some((s) => s)) { if (fromStr.includes(".")) { fromStr = fromStr.replace(".", "?."); } } } - const fallbackStr = (w.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in w && w.catchControl - ? ` catch ${serializeControl(w.catchControl)}` - : w.catchFallbackRef - ? ` catch ${sPipeOrRef(w.catchFallbackRef)}` - : w.catchFallback - ? ` catch ${w.catchFallback}` - : ""; + const fallbackStr = serFallbacks(w, sPipeOrRef); + const errf = serCatch(w, sPipeOrRef); lines.push( ` .${relPath.join(".")} <- ${fromStr}${fallbackStr}${errf}`, ); @@ -2181,31 +2153,19 @@ function serializeBridgeBlock(bridge: Bridge): string { if (spreadConsumedWires.has(w)) continue; // Conditional (ternary) wire - if ("cond" in w) { + if (isTern(w)) { const toStr = sRef(w.to, false); - const condStr = serializeExprOrRef(w.cond); - const thenStr = w.thenRef - ? sRef(w.thenRef, true) - : (w.thenValue ?? "null"); - const elseStr = w.elseRef - ? sRef(w.elseRef, true) - : (w.elseValue ?? "null"); - const fallbackStr = (w.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in w && w.catchControl - ? ` catch ${serializeControl(w.catchControl)}` - : w.catchFallbackRef - ? ` catch ${sPipeOrRef(w.catchFallbackRef)}` - : w.catchFallback - ? ` catch ${w.catchFallback}` - : ""; + const condStr = serializeExprOrRef(eRef(wTern(w).cond)); + const thenStr = + wTern(w).then.type === "ref" + ? sRef(eRef(wTern(w).then), true) + : (eVal(wTern(w).then) ?? "null"); + const elseStr = + wTern(w).else.type === "ref" + ? sRef(eRef(wTern(w).else), true) + : (eVal(wTern(w).else) ?? "null"); + const fallbackStr = serFallbacks(w, sPipeOrRef); + const errf = serCatch(w, sPipeOrRef); lines.push( `${toStr} <- ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf}`, ); @@ -2213,14 +2173,14 @@ function serializeBridgeBlock(bridge: Bridge): string { } // Constant wire - if ("value" in w) { + if (isLit(w)) { const toStr = sRef(w.to, false); - lines.push(`${toStr} = ${formatBareValue(w.value)}`); + lines.push(`${toStr} = ${formatBareValue(wVal(w))}`); continue; } // Skip condAnd/condOr wires (handled in expression tree serialization) - if ("condAnd" in w || "condOr" in w) continue; + if (isAndW(w) || isOrW(w)) continue; // Array mapping — emit brace-delimited element block const arrayKey = w.to.path.join("."); @@ -2233,7 +2193,7 @@ function serializeBridgeBlock(bridge: Bridge): string { ) { serializedArrays.add(arrayKey); const iterName = arrayIterators[arrayKey]; - const fromStr = sRef(w.from, true) + "[]"; + const fromStr = sRef(wRef(w), true) + "[]"; const toStr = sRef(w.to, false); lines.push(`${toStr} <- ${fromStr} as ${iterName} {`); serializeArrayElements(w.to.path, iterName, " "); @@ -2242,10 +2202,10 @@ function serializeBridgeBlock(bridge: Bridge): string { } // Regular wire - let fromStr = sRef(w.from, true); + let fromStr = sRef(wRef(w), true); // Legacy safe flag without per-segment info: put ?. after root - if (w.safe) { - const ref = w.from; + if (wSafe(w)) { + const ref = wRef(w); if (!ref.rootSafe && !ref.pathSafe?.some((s) => s)) { if (fromStr.includes(".")) { fromStr = fromStr.replace(".", "?."); @@ -2253,22 +2213,8 @@ function serializeBridgeBlock(bridge: Bridge): string { } } const toStr = sRef(w.to, false); - const fallbackStr = (w.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in w && w.catchControl - ? ` catch ${serializeControl(w.catchControl)}` - : w.catchFallbackRef - ? ` catch ${sPipeOrRef(w.catchFallbackRef)}` - : w.catchFallback - ? ` catch ${w.catchFallback}` - : ""; + const fallbackStr = serFallbacks(w, sPipeOrRef); + const errf = serCatch(w, sPipeOrRef); lines.push(`${toStr} <- ${fromStr}${fallbackStr}${errf}`); } @@ -2279,43 +2225,31 @@ function serializeBridgeBlock(bridge: Bridge): string { // Ternary alias: emit `alias ? : [fallbacks] as ` if (info.ternaryWire) { const tw = info.ternaryWire; - const condStr = serializeExprOrRef(tw.cond); - const thenStr = tw.thenRef - ? sRef(tw.thenRef, true) - : (tw.thenValue ?? "null"); - const elseStr = tw.elseRef - ? sRef(tw.elseRef, true) - : (tw.elseValue ?? "null"); - const fallbackStr = (tw.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in tw && tw.catchControl - ? ` catch ${serializeControl(tw.catchControl)}` - : tw.catchFallbackRef - ? ` catch ${sPipeOrRef(tw.catchFallbackRef)}` - : tw.catchFallback - ? ` catch ${tw.catchFallback}` - : ""; + const condStr = serializeExprOrRef(eRef(wTern(tw).cond)); + const thenStr = + wTern(tw).then.type === "ref" + ? sRef(eRef(wTern(tw).then), true) + : (eVal(wTern(tw).then) ?? "null"); + const elseStr = + wTern(tw).else.type === "ref" + ? sRef(eRef(wTern(tw).else), true) + : (eVal(wTern(tw).else) ?? "null"); + const fallbackStr = serFallbacks(tw, sPipeOrRef); + const errf = serCatch(tw, sPipeOrRef); lines.push( `alias ${condStr} ? ${thenStr} : ${elseStr}${fallbackStr}${errf} as ${alias}`, ); continue; } const srcWire = info.sourceWire!; - const fromRef = srcWire.from; + const fromRef = wRef(srcWire); // Element-scoped bindings are emitted inside array blocks if (fromRef.element) continue; // Check if source is a pipe fork with element-sourced input (array-scoped) const srcTk = refTrunkKey(fromRef); if (fromRef.path.length === 0 && pipeHandleTrunkKeys.has(srcTk)) { const inWire = toInMap.get(srcTk); - if (inWire && inWire.from.element) continue; + if (inWire && wRef(inWire).element) continue; } // Reconstruct source expression let sourcePart: string; @@ -2334,11 +2268,14 @@ function serializeBridgeBlock(bridge: Bridge): string { parts.push(handleName); const inWire = toInMap.get(currentTk); if (!inWire) break; - const innerTk = refTrunkKey(inWire.from); - if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(innerTk)) { + const innerTk = refTrunkKey(wRef(inWire)); + if ( + wRef(inWire).path.length === 0 && + pipeHandleTrunkKeys.has(innerTk) + ) { currentTk = innerTk; } else { - parts.push(sRef(inWire.from, true)); + parts.push(sRef(wRef(inWire), true)); break; } } @@ -2347,30 +2284,16 @@ function serializeBridgeBlock(bridge: Bridge): string { sourcePart = sRef(fromRef, true); } // Serialize safe navigation on alias source - if (srcWire.safe) { - const ref = srcWire.from; + if (wSafe(srcWire)) { + const ref = wRef(srcWire); if (!ref.rootSafe && !ref.pathSafe?.some((s) => s)) { if (sourcePart.includes(".")) { sourcePart = sourcePart.replace(".", "?."); } } } - const aliasFb = (srcWire.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const aliasErrf = - "catchControl" in srcWire && srcWire.catchControl - ? ` catch ${serializeControl(srcWire.catchControl)}` - : srcWire.catchFallbackRef - ? ` catch ${sPipeOrRef(srcWire.catchFallbackRef)}` - : srcWire.catchFallback - ? ` catch ${srcWire.catchFallback}` - : ""; + const aliasFb = serFallbacks(srcWire, sPipeOrRef); + const aliasErrf = serCatch(srcWire, sPipeOrRef); lines.push(`alias ${sourcePart}${aliasFb}${aliasErrf} as ${alias}`); } // Also emit wires reading from top-level __local bindings @@ -2394,31 +2317,17 @@ function serializeBridgeBlock(bridge: Bridge): string { } if (isArrayElement) continue; } - const alias = lw.from.field; - const safeSep = lw.safe || lw.from.rootSafe ? "?." : "."; + const alias = wRef(lw).field; + const safeSep = wSafe(lw) || wRef(lw).rootSafe ? "?." : "."; const fromPart = - lw.from.path.length > 0 + wRef(lw).path.length > 0 ? alias + safeSep + - serPath(lw.from.path, lw.from.rootSafe, lw.from.pathSafe) + serPath(wRef(lw).path, wRef(lw).rootSafe, wRef(lw).pathSafe) : alias; const toStr = sRef(lw.to, false); - const lwFb = (lw.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const lwErrf = - "catchControl" in lw && lw.catchControl - ? ` catch ${serializeControl(lw.catchControl)}` - : lw.catchFallbackRef - ? ` catch ${sPipeOrRef(lw.catchFallbackRef)}` - : lw.catchFallback - ? ` catch ${lw.catchFallback}` - : ""; + const lwFb = serFallbacks(lw, sPipeOrRef); + const lwErrf = serCatch(lw, sPipeOrRef); lines.push(`${toStr} <- ${fromPart}${lwFb}${lwErrf}`); } @@ -2442,36 +2351,33 @@ function serializeBridgeBlock(bridge: Bridge): string { // condAnd/condOr logic wire — reconstruct from leftRef/rightRef if (info.logicWire) { - const logic = - "condAnd" in info.logicWire - ? info.logicWire.condAnd - : info.logicWire.condOr; + const logic = wAndOr(info.logicWire!); let leftStr: string; - const leftTk = refTrunkKey(logic.leftRef); - if (logic.leftRef.path.length === 0 && exprForks.has(leftTk)) { + const leftTk = refTrunkKey(eRef(logic.left)); + if (eRef(logic.left).path.length === 0 && exprForks.has(leftTk)) { leftStr = serializeExprTree(leftTk, OP_PREC_SER[info.op] ?? 0) ?? - sRef(logic.leftRef, true); + sRef(eRef(logic.left), true); } else { - leftStr = logic.leftRef.element - ? "ITER." + serPath(logic.leftRef.path) - : sRef(logic.leftRef, true); + leftStr = eRef(logic.left).element + ? "ITER." + serPath(eRef(logic.left).path) + : sRef(eRef(logic.left), true); } let rightStr: string; - if (logic.rightRef) { - const rightTk = refTrunkKey(logic.rightRef); - if (logic.rightRef.path.length === 0 && exprForks.has(rightTk)) { + if (logic.right.type === "ref") { + const rightTk = refTrunkKey(eRef(logic.right)); + if (eRef(logic.right).path.length === 0 && exprForks.has(rightTk)) { rightStr = serializeExprTree(rightTk, OP_PREC_SER[info.op] ?? 0) ?? - sRef(logic.rightRef, true); + sRef(eRef(logic.right), true); } else { - rightStr = logic.rightRef.element - ? "ITER." + serPath(logic.rightRef.path) - : sRef(logic.rightRef, true); + rightStr = eRef(logic.right).element + ? "ITER." + serPath(eRef(logic.right).path) + : sRef(eRef(logic.right), true); } - } else if (logic.rightValue != null) { - rightStr = formatExprValue(logic.rightValue); + } else if (logic.right.type === "literal") { + rightStr = formatExprValue(eVal(logic.right)); } else { rightStr = "0"; } @@ -2485,22 +2391,22 @@ function serializeBridgeBlock(bridge: Bridge): string { // Serialize left operand (from .a wire) let leftStr: string | null = null; if (info.aWire) { - const fromTk = refTrunkKey(info.aWire.from); - if (info.aWire.from.path.length === 0 && exprForks.has(fromTk)) { + const fromTk = refTrunkKey(wRef(info.aWire!)); + if (wRef(info.aWire!).path.length === 0 && exprForks.has(fromTk)) { leftStr = serializeExprTree(fromTk, OP_PREC_SER[info.op] ?? 0); } else { - leftStr = info.aWire.from.element - ? "ITER." + serPath(info.aWire.from.path) - : sRef(info.aWire.from, true); + leftStr = wRef(info.aWire!).element + ? "ITER." + serPath(wRef(info.aWire!).path) + : sRef(wRef(info.aWire!), true); } } // Serialize right operand (from .b wire) let rightStr: string; - if (info.bWire && "value" in info.bWire) { - rightStr = formatExprValue(info.bWire.value); - } else if (info.bWire && "from" in info.bWire) { - const bFrom = (info.bWire as FW).from; + if (info.bWire && isLit(info.bWire)) { + rightStr = formatExprValue(wVal(info.bWire!)); + } else if (info.bWire && isPull(info.bWire)) { + const bFrom = wRef(info.bWire!); const bTk = refTrunkKey(bFrom); if (bFrom.path.length === 0 && exprForks.has(bTk)) { rightStr = @@ -2527,22 +2433,8 @@ function serializeBridgeBlock(bridge: Bridge): string { const exprStr = serializeExprTree(tk); if (exprStr) { const destStr = sRef(outWire.to, false); - const fallbackStr = (outWire.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in outWire && outWire.catchControl - ? ` catch ${serializeControl(outWire.catchControl)}` - : outWire.catchFallbackRef - ? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}` - : outWire.catchFallback - ? ` catch ${outWire.catchFallback}` - : ""; + const fallbackStr = serFallbacks(outWire, sPipeOrRef); + const errf = serCatch(outWire, sPipeOrRef); lines.push(`${destStr} <- ${exprStr}${fallbackStr}${errf}`); } continue; @@ -2554,22 +2446,8 @@ function serializeBridgeBlock(bridge: Bridge): string { const templateStr = reconstructTemplateString(tk); if (templateStr) { const destStr = sRef(outWire.to, false); - const fallbackStr = (outWire.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in outWire && outWire.catchControl - ? ` catch ${serializeControl(outWire.catchControl)}` - : outWire.catchFallbackRef - ? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}` - : outWire.catchFallback - ? ` catch ${outWire.catchFallback}` - : ""; + const fallbackStr = serFallbacks(outWire, sPipeOrRef); + const errf = serCatch(outWire, sPipeOrRef); lines.push(`${destStr} <- ${templateStr}${fallbackStr}${errf}`); } continue; @@ -2591,11 +2469,11 @@ function serializeBridgeBlock(bridge: Bridge): string { fieldName === "in" ? handleName : `${handleName}.${fieldName}`; handleChain.push(token); if (!inWire) break; - const fromTk = refTrunkKey(inWire.from); - if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { + const fromTk = refTrunkKey(wRef(inWire)); + if (wRef(inWire).path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) { currentTk = fromTk; } else { - actualSourceRef = inWire.from; + actualSourceRef = wRef(inWire); break; } } @@ -2603,22 +2481,8 @@ function serializeBridgeBlock(bridge: Bridge): string { if (actualSourceRef && handleChain.length > 0) { const sourceStr = sRef(actualSourceRef, true); const destStr = sRef(outWire.to, false); - const fallbackStr = (outWire.fallbacks ?? []) - .map((f) => { - const op = f.type === "falsy" ? "||" : "??"; - if (f.control) return ` ${op} ${serializeControl(f.control)}`; - if (f.ref) return ` ${op} ${sPipeOrRef(f.ref)}`; - return ` ${op} ${f.value}`; - }) - .join(""); - const errf = - "catchControl" in outWire && outWire.catchControl - ? ` catch ${serializeControl(outWire.catchControl)}` - : outWire.catchFallbackRef - ? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}` - : outWire.catchFallback - ? ` catch ${outWire.catchFallback}` - : ""; + const fallbackStr = serFallbacks(outWire, sPipeOrRef); + const errf = serCatch(outWire, sPipeOrRef); lines.push( `${destStr} <- ${handleChain.join(":")}:${sourceStr}${fallbackStr}${errf}`, ); diff --git a/packages/bridge-parser/src/parser/parser.ts b/packages/bridge-parser/src/parser/parser.ts index 5674ee32..e608d693 100644 --- a/packages/bridge-parser/src/parser/parser.ts +++ b/packages/bridge-parser/src/parser/parser.ts @@ -74,13 +74,15 @@ import type { ConstDef, ControlFlowInstruction, DefineDef, + Expression, HandleBinding, Instruction, NodeRef, SourceLocation, ToolDef, Wire, - WireFallback, + WireCatch, + WireSourceEntry, } from "@stackables/bridge-core"; import { SELF_MODULE } from "@stackables/bridge-core"; @@ -1462,47 +1464,35 @@ type CoalesceAltResult = | { sourceRef: NodeRef } | { control: ControlFlowInstruction }; -function buildWireFallback( - type: "falsy" | "nullish", +function buildSourceEntry( + gate: "falsy" | "nullish", altNode: CstNode, altResult: CoalesceAltResult, -): WireFallback { +): WireSourceEntry { const loc = locFromNode(altNode); + let expr: Expression; if ("literal" in altResult) { - return { type, value: altResult.literal, ...(loc ? { loc } : {}) }; + expr = { type: "literal", value: altResult.literal, loc }; + } else if ("control" in altResult) { + expr = { type: "control", control: altResult.control, loc }; + } else { + expr = { type: "ref", ref: altResult.sourceRef, loc }; } - if ("control" in altResult) { - return { type, control: altResult.control, ...(loc ? { loc } : {}) }; - } - return { type, ref: altResult.sourceRef, ...(loc ? { loc } : {}) }; + return { expr, gate, loc }; } -function buildCatchAttrs( +function buildCatchHandler( catchAlt: CstNode, altResult: CoalesceAltResult, -): { - catchLoc?: SourceLocation; - catchFallback?: string; - catchFallbackRef?: NodeRef; - catchControl?: ControlFlowInstruction; -} { - const catchLoc = locFromNode(catchAlt); +): WireCatch { + const loc = locFromNode(catchAlt); if ("literal" in altResult) { - return { - ...(catchLoc ? { catchLoc } : {}), - catchFallback: altResult.literal, - }; + return { value: altResult.literal, ...(loc ? { loc } : {}) }; } if ("control" in altResult) { - return { - ...(catchLoc ? { catchLoc } : {}), - catchControl: altResult.control, - }; + return { control: altResult.control, ...(loc ? { loc } : {}) }; } - return { - ...(catchLoc ? { catchLoc } : {}), - catchFallbackRef: altResult.sourceRef, - }; + return { ref: altResult.sourceRef, ...(loc ? { loc } : {}) }; } /* ── extractNameToken: get string from nameToken CST node ── */ @@ -1914,7 +1904,6 @@ function processElementLines( wires.push( withLoc( { - value, to: { module: SELF_MODULE, type: bridgeType, @@ -1922,6 +1911,7 @@ function processElementLines( element: true, path: elemToPath, }, + sources: [{ expr: { type: "literal", value } }], }, elemLineLoc, ), @@ -1943,7 +1933,7 @@ function processElementLines( }; // Process coalesce modifiers - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(elemLine, "elemCoalesceItem")) { const type = tok(item, "falsyOp") @@ -1952,38 +1942,23 @@ function processElementLines( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAltIterAware(altNode, elemLineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchLoc: SourceLocation | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(elemLine, "elemCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAltIterAware(catchAlt, elemLineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } - const lastAttrs = { - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; - if (segs) { const concatOutRef = desugarTemplateStringFn( segs, @@ -1995,21 +1970,34 @@ function processElementLines( wires.push( withLoc( { - from: concatOutRef, to: elemToRefWithElement, + sources: [ + { expr: { type: "ref", ref: concatOutRef } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), pipe: true, - ...lastAttrs, }, elemLineLoc, ), ); } else { wires.push( - withLoc({ value: raw, to: elemToRef, ...lastAttrs }, elemLineLoc), + withLoc( + { + to: elemToRef, + sources: [ + { expr: { type: "literal", value: raw } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + }, + elemLineLoc, + ), ); } wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } @@ -2058,7 +2046,13 @@ function processElementLines( path: elemToPath, }; wires.push( - withLoc({ from: innerFromRef, to: innerToRef }, elemLineLoc), + withLoc( + { + to: innerToRef, + sources: [{ expr: { type: "ref", ref: innerFromRef } }], + }, + elemLineLoc, + ), ); // Register the inner iterator @@ -2228,7 +2222,7 @@ function processElementLines( ); // Process coalesce alternatives. - const elemFallbacks: WireFallback[] = []; + const elemFallbacks: WireSourceEntry[] = []; const elemFallbackInternalWires: Wire[] = []; for (const item of subs(elemLine, "elemCoalesceItem")) { const type = tok(item, "falsyOp") @@ -2237,20 +2231,14 @@ function processElementLines( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAltIterAware(altNode, elemLineNum); - if ("literal" in altResult) { - elemFallbacks.push({ type, value: altResult.literal }); - } else if ("control" in altResult) { - elemFallbacks.push({ type, control: altResult.control }); - } else { - elemFallbacks.push({ type, ref: altResult.sourceRef }); + elemFallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { elemFallbackInternalWires.push(...wires.splice(preLen)); } } // Process catch error fallback. - let elemCatchFallback: string | undefined; - let elemCatchControl: ControlFlowInstruction | undefined; - let elemCatchFallbackRef: NodeRef | undefined; + let elemCatchHandler: WireCatch | undefined; let elemCatchFallbackInternalWires: Wire[] = []; const elemCatchAlt = sub(elemLine, "elemCatchAlt"); if (elemCatchAlt) { @@ -2259,12 +2247,8 @@ function processElementLines( elemCatchAlt, elemLineNum, ); - if ("literal" in altResult) { - elemCatchFallback = altResult.literal; - } else if ("control" in altResult) { - elemCatchControl = altResult.control; - } else { - elemCatchFallbackRef = altResult.sourceRef; + elemCatchHandler = buildCatchHandler(elemCatchAlt, altResult); + if ("sourceRef" in altResult) { elemCatchFallbackInternalWires = wires.splice(preLen); } } @@ -2272,25 +2256,44 @@ function processElementLines( wires.push( withLoc( { - cond: elemCondRef, - ...(elemCondLoc ? { condLoc: elemCondLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(elemFallbacks.length > 0 ? { fallbacks: elemFallbacks } : {}), - ...(elemCatchFallback !== undefined - ? { catchFallback: elemCatchFallback } - : {}), - ...(elemCatchFallbackRef !== undefined - ? { catchFallbackRef: elemCatchFallbackRef } - : {}), - ...(elemCatchControl ? { catchControl: elemCatchControl } : {}), to: elemToRef, + sources: [ + { + expr: { + type: "ternary", + cond: { type: "ref", ref: elemCondRef, loc: elemCondLoc }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(elemCondLoc ? { condLoc: elemCondLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...elemFallbacks, + ], + ...(elemCatchHandler ? { catch: elemCatchHandler } : {}), }, elemLineLoc, ), @@ -2303,7 +2306,7 @@ function processElementLines( sourceParts.push({ ref: elemCondRef, isPipeFork: elemCondIsPipeFork }); // Coalesce alternatives (|| and ??) - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(elemLine, "elemCoalesceItem")) { const type = tok(item, "falsyOp") @@ -2312,47 +2315,41 @@ function processElementLines( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAltIterAware(altNode, elemLineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } // catch error fallback - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchLoc: SourceLocation | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(elemLine, "elemCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAltIterAware(catchAlt, elemLineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } // Emit wire const { ref: fromRef, isPipeFork } = sourceParts[0]; - const wireAttrs = { - ...(isPipeFork ? { pipe: true as const } : {}), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; + wires.push( - withLoc({ from: fromRef, to: elemToRef, ...wireAttrs }, elemLineLoc), + withLoc( + { + to: elemToRef, + sources: [{ expr: { type: "ref", ref: fromRef } }, ...fallbacks], + ...(catchHandler ? { catch: catchHandler } : {}), + ...(isPipeFork ? { pipe: true as const } : {}), + }, + elemLineLoc, + ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); } else if (elemC.elemScopeBlock) { // ── Path scope block inside array mapping: .field { lines: .sub <- ..., ...source } ── const scopeLines = subs(elemLine, "elemScopeLine"); @@ -2371,7 +2368,6 @@ function processElementLines( wires.push( withLoc( { - from: fromRef, to: { module: SELF_MODULE, type: bridgeType, @@ -2379,8 +2375,16 @@ function processElementLines( element: true, path: elemToPath, }, + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(spreadSafe ? { safe: true as const } : {}), + }, + }, + ], spread: true as const, - ...(spreadSafe ? { safe: true as const } : {}), }, locFromNode(spreadLine), ), @@ -2550,7 +2554,6 @@ function processElementScopeLines( wires.push( withLoc( { - from: fromRef, to: { module: SELF_MODULE, type: bridgeType, @@ -2558,8 +2561,16 @@ function processElementScopeLines( element: true, path: spreadToPath, }, + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(spreadSafe ? { safe: true as const } : {}), + }, + }, + ], spread: true as const, - ...(spreadSafe ? { safe: true as const } : {}), }, locFromNode(spreadLine), ), @@ -2592,7 +2603,6 @@ function processElementScopeLines( wires.push( withLoc( { - value, to: { module: SELF_MODULE, type: bridgeType, @@ -2600,6 +2610,7 @@ function processElementScopeLines( element: true, path: elemToPath, }, + sources: [{ expr: { type: "literal", value } }], }, scopeLineLoc, ), @@ -2624,7 +2635,7 @@ function processElementScopeLines( const raw = stringSourceToken.image.slice(1, -1); const segs = parseTemplateString(raw); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(scopeLine, "scopeCoalesceItem")) { const type = tok(item, "falsyOp") @@ -2633,36 +2644,23 @@ function processElementScopeLines( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAltIterAware(altNode, scopeLineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchLoc: SourceLocation | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(scopeLine, "scopeCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAltIterAware(catchAlt, scopeLineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } - const lastAttrs = { - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; + if (segs) { const concatOutRef = desugarTemplateStringFn( segs, @@ -2673,21 +2671,34 @@ function processElementScopeLines( wires.push( withLoc( { - from: concatOutRef, to: { ...elemToRef, element: true }, + sources: [ + { expr: { type: "ref", ref: concatOutRef } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), pipe: true, - ...lastAttrs, }, scopeLineLoc, ), ); } else { wires.push( - withLoc({ value: raw, to: elemToRef, ...lastAttrs }, scopeLineLoc), + withLoc( + { + to: elemToRef, + sources: [ + { expr: { type: "literal", value: raw } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + }, + scopeLineLoc, + ), ); } wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } @@ -2800,7 +2811,7 @@ function processElementScopeLines( iterNames, ); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(scopeLine, "scopeCoalesceItem")) { const type = tok(item, "falsyOp") @@ -2809,54 +2820,69 @@ function processElementScopeLines( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAltIterAware(altNode, scopeLineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchLoc: SourceLocation | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(scopeLine, "scopeCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAltIterAware(catchAlt, scopeLineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } wires.push( withLoc( { - cond: condRef, - ...(condLoc ? { condLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), to: elemToRef, + sources: [ + { + expr: { + type: "ternary", + cond: { type: "ref", ref: condRef, loc: condLoc }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(condLoc ? { condLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), }, scopeLineLoc, ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } @@ -2864,7 +2890,7 @@ function processElementScopeLines( sourceParts.push({ ref: condRef, isPipeFork: condIsPipeFork }); // Coalesce alternatives (|| and ??) - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(scopeLine, "scopeCoalesceItem")) { const type = tok(item, "falsyOp") @@ -2873,46 +2899,47 @@ function processElementScopeLines( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAltIterAware(altNode, scopeLineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchLoc: SourceLocation | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(scopeLine, "scopeCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAltIterAware(catchAlt, scopeLineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0]; - const wireAttrs = { - ...(isPipe ? { pipe: true as const } : {}), - ...(condLoc ? { fromLoc: condLoc } : {}), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; wires.push( - withLoc({ from: fromRef, to: elemToRef, ...wireAttrs }, scopeLineLoc), + withLoc( + { + to: elemToRef, + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(condLoc ? { refLoc: condLoc } : {}), + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + ...(isPipe ? { pipe: true as const } : {}), + }, + scopeLineLoc, + ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); } } } @@ -3599,8 +3626,8 @@ function buildBridgeBody( path: [], }; wires.push({ - from: prevOutRef, to: forkInRef, + sources: [{ expr: { type: "ref", ref: prevOutRef } }], pipe: true, }); prevOutRef = forkRootRef; @@ -3625,7 +3652,10 @@ function buildBridgeBody( field: alias, path: [], }; - wires.push({ from: sourceRef, to: localToRef }); + wires.push({ + to: localToRef, + sources: [{ expr: { type: "ref", ref: sourceRef } }], + }); } return () => { for (const [alias, previous] of shadowedAliases) { @@ -3762,7 +3792,12 @@ function buildBridgeBody( if (wc.equalsOp) { const value = extractBareValue(sub(wireNode, "constValue")!); - wires.push(withLoc({ value, to: toRef }, wireLoc)); + wires.push( + withLoc( + { to: toRef, sources: [{ expr: { type: "literal", value } }] }, + wireLoc, + ), + ); continue; } @@ -3771,7 +3806,7 @@ function buildBridgeBody( const raw = stringSourceToken.image.slice(1, -1); const segs = parseTemplateString(raw); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(wireNode, "coalesceItem")) { const type = tok(item, "falsyOp") @@ -3780,38 +3815,23 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum, iterNames); - if ("literal" in altResult) { - fallbacks.push({ type, value: altResult.literal }); - } else if ("control" in altResult) { - fallbacks.push({ type, control: altResult.control }); - } else { - fallbacks.push({ type, ref: altResult.sourceRef }); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(wireNode, "catchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, lineNum, iterNames); - if ("literal" in altResult) { - catchFallback = altResult.literal; - } else if ("control" in altResult) { - catchControl = altResult.control; - } else { - catchFallbackRef = altResult.sourceRef; - catchFallbackInternalWires = wires.splice(preLen); + catchHandler = buildCatchHandler(catchAlt, altResult); + if ("sourceRef" in altResult) { + catchInternalWires = wires.splice(preLen); } } - const lastAttrs = { - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchFallback ? { catchFallback } : {}), - ...(catchFallbackRef ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; + if (segs) { const concatOutRef = desugarTemplateString( segs, @@ -3822,19 +3842,34 @@ function buildBridgeBody( wires.push( withLoc( { - from: concatOutRef, to: toRef, + sources: [ + { expr: { type: "ref", ref: concatOutRef } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), pipe: true, - ...lastAttrs, }, wireLoc, ), ); } else { - wires.push(withLoc({ value: raw, to: toRef, ...lastAttrs }, wireLoc)); + wires.push( + withLoc( + { + to: toRef, + sources: [ + { expr: { type: "literal", value: raw } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + }, + wireLoc, + ), + ); } wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } @@ -3908,7 +3943,7 @@ function buildBridgeBody( const thenBranch = extractTernaryBranch(thenNode, lineNum, iterNames); const elseBranch = extractTernaryBranch(elseNode, lineNum, iterNames); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(wireNode, "coalesceItem")) { const type = tok(item, "falsyOp") @@ -3917,62 +3952,75 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum, iterNames); - if ("literal" in altResult) { - fallbacks.push({ type, value: altResult.literal }); - } else if ("control" in altResult) { - fallbacks.push({ type, control: altResult.control }); - } else { - fallbacks.push({ type, ref: altResult.sourceRef }); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(wireNode, "catchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, lineNum, iterNames); - if ("literal" in altResult) { - catchFallback = altResult.literal; - } else if ("control" in altResult) { - catchControl = altResult.control; - } else { - catchFallbackRef = altResult.sourceRef; - catchFallbackInternalWires = wires.splice(preLen); + catchHandler = buildCatchHandler(catchAlt, altResult); + if ("sourceRef" in altResult) { + catchInternalWires = wires.splice(preLen); } } wires.push( withLoc( { - cond: condRef, - ...(condLoc ? { condLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), to: toRef, + sources: [ + { + expr: { + type: "ternary", + cond: { type: "ref", ref: condRef, loc: condLoc }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(condLoc ? { condLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), }, wireLoc, ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; let hasTruthyLiteralFallback = false; for (const item of subs(wireNode, "coalesceItem")) { @@ -3983,53 +4031,50 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum, iterNames); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("literal" in altResult) { - fallbacks.push({ type, value: altResult.literal }); if (type === "falsy") { hasTruthyLiteralFallback = Boolean(JSON.parse(altResult.literal)); } - } else if ("control" in altResult) { - fallbacks.push({ type, control: altResult.control }); - } else { - fallbacks.push({ type, ref: altResult.sourceRef }); + } else if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(wireNode, "catchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, lineNum, iterNames); - if ("literal" in altResult) { - catchFallback = altResult.literal; - } else if ("control" in altResult) { - catchControl = altResult.control; - } else { - catchFallbackRef = altResult.sourceRef; - catchFallbackInternalWires = wires.splice(preLen); + catchHandler = buildCatchHandler(catchAlt, altResult); + if ("sourceRef" in altResult) { + catchInternalWires = wires.splice(preLen); } } wires.push( withLoc( { - from: condRef, to: toRef, + sources: [ + { + expr: { + type: "ref", + ref: condRef, + ...(condIsPipeFork ? {} : {}), + }, + }, + ...fallbacks, + ], ...(condIsPipeFork ? { pipe: true as const } : {}), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), + ...(catchHandler ? { catch: catchHandler } : {}), }, wireLoc, ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); } } @@ -4132,8 +4177,8 @@ function buildBridgeBody( wires.push( withLoc( { - from: prevOutRef, to: forkInRef, + sources: [{ expr: { type: "ref", ref: prevOutRef } }], pipe: true, }, sourceLoc, @@ -4193,7 +4238,15 @@ function buildBridgeBody( path: ["parts", String(idx)], }; if (seg.kind === "text") { - wires.push(withLoc({ value: seg.value, to: partRef }, loc)); + wires.push( + withLoc( + { + to: partRef, + sources: [{ expr: { type: "literal", value: seg.value } }], + }, + loc, + ), + ); } else { // Parse the ref path: e.g. "i.id" → root="i", segments=["id"] const dotParts = seg.path.split("."); @@ -4203,13 +4256,28 @@ function buildBridgeBody( // Check for iterator-relative refs const fromRef = resolveIterRef(root, segments, iterScope); if (fromRef) { - wires.push(withLoc({ from: fromRef, to: partRef }, loc)); + wires.push( + withLoc( + { + to: partRef, + sources: [{ expr: { type: "ref", ref: fromRef } }], + }, + loc, + ), + ); } else { wires.push( withLoc( { - from: resolveAddress(root, segments, lineNum), to: partRef, + sources: [ + { + expr: { + type: "ref", + ref: resolveAddress(root, segments, lineNum), + }, + }, + ], }, loc, ), @@ -4635,30 +4703,45 @@ function buildBridgeBody( instance: litInstance, path: [], }; - wires.push(withLoc({ value: left.value, to: litRef }, loc)); + wires.push( + withLoc( + { + to: litRef, + sources: [ + { expr: { type: "literal", value: left.value } }, + ], + }, + loc, + ), + ); return litRef; })(); - // Build right side - const rightSide = - right.kind === "ref" - ? { rightRef: right.ref } - : { rightValue: right.value }; - const safeAttr = leftSafe ? { safe: true as const } : {}; - const rightSafeAttr = rightSafe ? { rightSafe: true as const } : {}; + const rightSafeAttr = rightSafe ? { safe: true as const } : {}; + + const leftExpr: Expression = { type: "ref", ref: leftRef, ...safeAttr }; + const rightExpr: Expression = + right.kind === "ref" + ? { type: "ref", ref: right.ref, ...rightSafeAttr } + : { type: "literal", value: right.value }; if (opStr === "and") { wires.push( withLoc( { - condAnd: { - leftRef, - ...rightSide, - ...safeAttr, - ...rightSafeAttr, - }, to: toRef, + sources: [ + { + expr: { + type: "and", + left: leftExpr, + right: rightExpr, + ...(leftSafe ? { leftSafe: true as const } : {}), + ...(rightSafe ? { rightSafe: true as const } : {}), + }, + }, + ], }, loc, ), @@ -4667,13 +4750,18 @@ function buildBridgeBody( wires.push( withLoc( { - condOr: { - leftRef, - ...rightSide, - ...safeAttr, - ...rightSafeAttr, - }, to: toRef, + sources: [ + { + expr: { + type: "or", + left: leftExpr, + right: rightExpr, + ...(leftSafe ? { leftSafe: true as const } : {}), + ...(rightSafe ? { rightSafe: true as const } : {}), + }, + }, + ], }, loc, ), @@ -4714,16 +4802,23 @@ function buildBridgeBody( // Wire left → fork.a (propagate safe flag from operand) if (left.kind === "literal") { - wires.push(withLoc({ value: left.value, to: makeTarget("a") }, loc)); + wires.push( + withLoc( + { + to: makeTarget("a"), + sources: [{ expr: { type: "literal", value: left.value } }], + }, + loc, + ), + ); } else { const safeAttr = leftSafe ? { safe: true as const } : {}; wires.push( withLoc( { - from: left.ref, to: makeTarget("a"), + sources: [{ expr: { type: "ref", ref: left.ref, ...safeAttr } }], pipe: true, - ...safeAttr, }, loc, ), @@ -4732,16 +4827,23 @@ function buildBridgeBody( // Wire right → fork.b (propagate safe flag from operand) if (right.kind === "literal") { - wires.push(withLoc({ value: right.value, to: makeTarget("b") }, loc)); + wires.push( + withLoc( + { + to: makeTarget("b"), + sources: [{ expr: { type: "literal", value: right.value } }], + }, + loc, + ), + ); } else { const safeAttr = rightSafe ? { safe: true as const } : {}; wires.push( withLoc( { - from: right.ref, to: makeTarget("b"), + sources: [{ expr: { type: "ref", ref: right.ref, ...safeAttr } }], pipe: true, - ...safeAttr, }, loc, ), @@ -4822,7 +4924,6 @@ function buildBridgeBody( wires.push( withLoc( { - from: sourceRef, to: { module: forkTrunkModule, type: forkTrunkType, @@ -4830,8 +4931,8 @@ function buildBridgeBody( instance: forkInstance, path: ["a"], }, + sources: [{ expr: { type: "ref", ref: sourceRef, ...safeAttr } }], pipe: true, - ...safeAttr, }, loc, ), @@ -4902,9 +5003,16 @@ function buildBridgeBody( wires.push( withLoc( { - from: sourceRef, to: localToRef, - ...(aliasSafe ? { safe: true as const } : {}), + sources: [ + { + expr: { + type: "ref", + ref: sourceRef, + ...(aliasSafe ? { safe: true as const } : {}), + }, + }, + ], }, locFromNode(aliasNode), ), @@ -4922,10 +5030,17 @@ function buildBridgeBody( wires.push( withLoc( { - from: fromRef, to: nestedToRef, + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(spreadSafe ? { safe: true as const } : {}), + }, + }, + ], spread: true as const, - ...(spreadSafe ? { safe: true as const } : {}), }, locFromNode(spreadLine), ), @@ -4941,7 +5056,12 @@ function buildBridgeBody( // ── Constant wire: .field = value ── if (sc.scopeEquals) { const value = extractBareValue(sub(scopeLine, "scopeValue")!); - wires.push(withLoc({ value, to: toRef }, scopeLineLoc)); + wires.push( + withLoc( + { to: toRef, sources: [{ expr: { type: "literal", value } }] }, + scopeLineLoc, + ), + ); continue; } @@ -4955,7 +5075,7 @@ function buildBridgeBody( const raw = stringSourceToken.image.slice(1, -1); const segs = parseTemplateString(raw); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(scopeLine, "scopeCoalesceItem")) { const type = tok(item, "falsyOp") @@ -4964,36 +5084,23 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, scopeLineNum); - if ("literal" in altResult) { - fallbacks.push({ type, value: altResult.literal }); - } else if ("control" in altResult) { - fallbacks.push({ type, control: altResult.control }); - } else { - fallbacks.push({ type, ref: altResult.sourceRef }); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(scopeLine, "scopeCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, scopeLineNum); - if ("literal" in altResult) catchFallback = altResult.literal; - else if ("control" in altResult) catchControl = altResult.control; - else { - catchFallbackRef = altResult.sourceRef; - catchFallbackInternalWires = wires.splice(preLen); + catchHandler = buildCatchHandler(catchAlt, altResult); + if ("sourceRef" in altResult) { + catchInternalWires = wires.splice(preLen); } } - const lastAttrs = { - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchFallback ? { catchFallback } : {}), - ...(catchFallbackRef ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; + if (segs) { const concatOutRef = desugarTemplateString( segs, @@ -5004,21 +5111,34 @@ function buildBridgeBody( wires.push( withLoc( { - from: concatOutRef, to: toRef, + sources: [ + { expr: { type: "ref", ref: concatOutRef } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), pipe: true, - ...lastAttrs, }, scopeLineLoc, ), ); } else { wires.push( - withLoc({ value: raw, to: toRef, ...lastAttrs }, scopeLineLoc), + withLoc( + { + to: toRef, + sources: [ + { expr: { type: "literal", value: raw } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + }, + scopeLineLoc, + ), ); } wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } @@ -5106,7 +5226,7 @@ function buildBridgeBody( const elseNode = sub(scopeLine, "scopeElseBranch")!; const thenBranch = extractTernaryBranch(thenNode, scopeLineNum); const elseBranch = extractTernaryBranch(elseNode, scopeLineNum); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(scopeLine, "scopeCoalesceItem")) { const type = tok(item, "falsyOp") @@ -5115,61 +5235,80 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, scopeLineNum); - if ("literal" in altResult) { - fallbacks.push({ type, value: altResult.literal }); - } else if ("control" in altResult) { - fallbacks.push({ type, control: altResult.control }); - } else { - fallbacks.push({ type, ref: altResult.sourceRef }); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(scopeLine, "scopeCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, scopeLineNum); - if ("literal" in altResult) catchFallback = altResult.literal; - else if ("control" in altResult) catchControl = altResult.control; - else { - catchFallbackRef = altResult.sourceRef; - catchFallbackInternalWires = wires.splice(preLen); + catchHandler = buildCatchHandler(catchAlt, altResult); + if ("sourceRef" in altResult) { + catchInternalWires = wires.splice(preLen); } } wires.push( withLoc( { - cond: condRef, - ...(condLoc ? { condLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), to: toRef, + sources: [ + { + expr: { + type: "ternary", + cond: { + type: "ref", + ref: condRef, + ...(condLoc ? { refLoc: condLoc } : {}), + }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(condLoc ? { condLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), }, scopeLineLoc, ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } sourceParts.push({ ref: condRef, isPipeFork: condIsPipeFork }); // Coalesce alternatives (|| and ??) - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(scopeLine, "scopeCoalesceItem")) { const type = tok(item, "falsyOp") @@ -5178,45 +5317,47 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, scopeLineNum); - if ("literal" in altResult) { - fallbacks.push({ type, value: altResult.literal }); - } else if ("control" in altResult) { - fallbacks.push({ type, control: altResult.control }); - } else { - fallbacks.push({ type, ref: altResult.sourceRef }); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); + if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(scopeLine, "scopeCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, scopeLineNum); - if ("literal" in altResult) catchFallback = altResult.literal; - else if ("control" in altResult) catchControl = altResult.control; - else { - catchFallbackRef = altResult.sourceRef; - catchFallbackInternalWires = wires.splice(preLen); + catchHandler = buildCatchHandler(catchAlt, altResult); + if ("sourceRef" in altResult) { + catchInternalWires = wires.splice(preLen); } } const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0]; - const wireAttrs = { - ...(isPipe ? { pipe: true as const } : {}), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchFallback ? { catchFallback } : {}), - ...(catchFallbackRef ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; wires.push( - withLoc({ from: fromRef, to: toRef, ...wireAttrs }, scopeLineLoc), + withLoc( + { + to: toRef, + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(condLoc ? { refLoc: condLoc } : {}), + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + ...(isPipe ? { pipe: true as const } : {}), + }, + scopeLineLoc, + ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); } } } @@ -5241,7 +5382,7 @@ function buildBridgeBody( } // ── Extract coalesce modifiers FIRST (shared by ternary + pull paths) ── - const aliasFallbacks: WireFallback[] = []; + const aliasFallbacks: WireSourceEntry[] = []; const aliasFallbackInternalWires: Wire[] = []; for (const item of subs(nodeAliasNode, "aliasCoalesceItem")) { const type = tok(item, "falsyOp") @@ -5250,38 +5391,22 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum); - aliasFallbacks.push(buildWireFallback(type, altNode, altResult)); + aliasFallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { aliasFallbackInternalWires.push(...wires.splice(preLen)); } } - let aliasCatchLoc: SourceLocation | undefined; - let aliasCatchFallback: string | undefined; - let aliasCatchControl: ControlFlowInstruction | undefined; - let aliasCatchFallbackRef: NodeRef | undefined; - let aliasCatchFallbackInternalWires: Wire[] = []; + let aliasCatchHandler: WireCatch | undefined; + let aliasCatchInternalWires: Wire[] = []; const aliasCatchAlt = sub(nodeAliasNode, "aliasCatchAlt"); if (aliasCatchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(aliasCatchAlt, lineNum); - const catchAttrs = buildCatchAttrs(aliasCatchAlt, altResult); - aliasCatchLoc = catchAttrs.catchLoc; - aliasCatchFallback = catchAttrs.catchFallback; - aliasCatchControl = catchAttrs.catchControl; - aliasCatchFallbackRef = catchAttrs.catchFallbackRef; + aliasCatchHandler = buildCatchHandler(aliasCatchAlt, altResult); if ("sourceRef" in altResult) { - aliasCatchFallbackInternalWires = wires.splice(preLen); + aliasCatchInternalWires = wires.splice(preLen); } } - const modifierAttrs = { - ...(aliasFallbacks.length > 0 ? { fallbacks: aliasFallbacks } : {}), - ...(aliasCatchLoc ? { catchLoc: aliasCatchLoc } : {}), - ...(aliasCatchFallback ? { catchFallback: aliasCatchFallback } : {}), - ...(aliasCatchFallbackRef - ? { catchFallbackRef: aliasCatchFallbackRef } - : {}), - ...(aliasCatchControl ? { catchControl: aliasCatchControl } : {}), - }; // ── Compute the source ref ── let sourceRef: NodeRef; @@ -5343,24 +5468,50 @@ function buildBridgeBody( wires.push( withLoc( { - cond: sourceRef, - ...(sourceLoc ? { condLoc: sourceLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...modifierAttrs, to: ternaryToRef, + sources: [ + { + expr: { + type: "ternary", + cond: { type: "ref", ref: sourceRef, loc: sourceLoc }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(sourceLoc ? { condLoc: sourceLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...aliasFallbacks, + ], + ...(aliasCatchHandler ? { catch: aliasCatchHandler } : {}), }, aliasLoc, ), ); wires.push(...aliasFallbackInternalWires); - wires.push(...aliasCatchFallbackInternalWires); + wires.push(...aliasCatchInternalWires); continue; } aliasSafe = false; @@ -5451,24 +5602,54 @@ function buildBridgeBody( wires.push( withLoc( { - cond: condRef, - ...(sourceLoc ? { condLoc: sourceLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...modifierAttrs, to: ternaryToRef, + sources: [ + { + expr: { + type: "ternary", + cond: { + type: "ref", + ref: condRef, + ...(sourceLoc ? { refLoc: sourceLoc } : {}), + }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(sourceLoc ? { condLoc: sourceLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...aliasFallbacks, + ], + ...(aliasCatchHandler ? { catch: aliasCatchHandler } : {}), }, aliasLoc, ), ); wires.push(...aliasFallbackInternalWires); - wires.push(...aliasCatchFallbackInternalWires); + wires.push(...aliasCatchInternalWires); continue; } @@ -5491,16 +5672,28 @@ function buildBridgeBody( field: alias, path: [], }; - const aliasAttrs = { - ...(aliasSafe ? { safe: true as const } : {}), - ...(sourceLoc ? { fromLoc: sourceLoc } : {}), - ...modifierAttrs, - }; wires.push( - withLoc({ from: sourceRef, to: localToRef, ...aliasAttrs }, aliasLoc), + withLoc( + { + to: localToRef, + sources: [ + { + expr: { + type: "ref", + ref: sourceRef, + ...(aliasSafe ? { safe: true } : {}), + ...(sourceLoc ? { refLoc: sourceLoc } : {}), + }, + }, + ...aliasFallbacks, + ], + ...(aliasCatchHandler ? { catch: aliasCatchHandler } : {}), + }, + aliasLoc, + ), ); wires.push(...aliasFallbackInternalWires); - wires.push(...aliasCatchFallbackInternalWires); + wires.push(...aliasCatchInternalWires); } } @@ -5529,7 +5722,12 @@ function buildBridgeBody( // ── Constant wire: target = value ── if (wc.equalsOp) { const value = extractBareValue(sub(wireNode, "constValue")!); - wires.push(withLoc({ value, to: toRef }, wireLoc)); + wires.push( + withLoc( + { to: toRef, sources: [{ expr: { type: "literal", value } }] }, + wireLoc, + ), + ); continue; } @@ -5566,9 +5764,16 @@ function buildBridgeBody( wires.push( withLoc( { - from: sourceRef, to: localToRef, - ...(aliasSafe ? { safe: true as const } : {}), + sources: [ + { + expr: { + type: "ref", + ref: sourceRef, + ...(aliasSafe ? { safe: true as const } : {}), + }, + }, + ], }, locFromNode(aliasNode), ), @@ -5587,10 +5792,17 @@ function buildBridgeBody( wires.push( withLoc( { - from: fromRef, to: toRef, - spread: true as const, - ...(spreadSafe ? { safe: true as const } : {}), + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(spreadSafe ? { safe: true } : {}), + }, + }, + ], + spread: true, }, locFromNode(spreadLine), ), @@ -5609,7 +5821,7 @@ function buildBridgeBody( const segs = parseTemplateString(raw); // Process coalesce modifiers - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(wireNode, "coalesceItem")) { const type = tok(item, "falsyOp") @@ -5618,38 +5830,23 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchLoc: SourceLocation | undefined; - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(wireNode, "catchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, lineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } - const lastAttrs = { - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback ? { catchFallback } : {}), - ...(catchFallbackRef ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; - if (segs) { // Template string — desugar to synthetic internal.concat fork const concatOutRef = desugarTemplateString( @@ -5660,16 +5857,36 @@ function buildBridgeBody( ); wires.push( withLoc( - { from: concatOutRef, to: toRef, pipe: true, ...lastAttrs }, + { + to: toRef, + sources: [ + { expr: { type: "ref", ref: concatOutRef } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + pipe: true, + }, wireLoc, ), ); } else { // Plain string without interpolation — emit constant wire - wires.push(withLoc({ value: raw, to: toRef, ...lastAttrs }, wireLoc)); + wires.push( + withLoc( + { + to: toRef, + sources: [ + { expr: { type: "literal", value: raw } }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + }, + wireLoc, + ), + ); } wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } @@ -5689,7 +5906,7 @@ function buildBridgeBody( : buildSourceExpr(firstSourceNode!, lineNum); // Process coalesce modifiers on the array wire (same as plain pull wires) - const arrayFallbacks: WireFallback[] = []; + const arrayFallbacks: WireSourceEntry[] = []; const arrayFallbackInternalWires: Wire[] = []; for (const item of subs(wireNode, "coalesceItem")) { const type = tok(item, "falsyOp") @@ -5698,43 +5915,38 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum); - arrayFallbacks.push(buildWireFallback(type, altNode, altResult)); + arrayFallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { arrayFallbackInternalWires.push(...wires.splice(preLen)); } } - let arrayCatchLoc: SourceLocation | undefined; - let arrayCatchFallback: string | undefined; - let arrayCatchControl: ControlFlowInstruction | undefined; - let arrayCatchFallbackRef: NodeRef | undefined; - let arrayCatchFallbackInternalWires: Wire[] = []; + let arrayCatchHandler: WireCatch | undefined; + let arrayCatchInternalWires: Wire[] = []; const arrayCatchAlt = sub(wireNode, "catchAlt"); if (arrayCatchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(arrayCatchAlt, lineNum); - const catchAttrs = buildCatchAttrs(arrayCatchAlt, altResult); - arrayCatchLoc = catchAttrs.catchLoc; - arrayCatchFallback = catchAttrs.catchFallback; - arrayCatchControl = catchAttrs.catchControl; - arrayCatchFallbackRef = catchAttrs.catchFallbackRef; + arrayCatchHandler = buildCatchHandler(arrayCatchAlt, altResult); if ("sourceRef" in altResult) { - arrayCatchFallbackInternalWires = wires.splice(preLen); + arrayCatchInternalWires = wires.splice(preLen); } } - const arrayWireAttrs = { - ...(arrayFallbacks.length > 0 ? { fallbacks: arrayFallbacks } : {}), - ...(arrayCatchLoc ? { catchLoc: arrayCatchLoc } : {}), - ...(arrayCatchFallback ? { catchFallback: arrayCatchFallback } : {}), - ...(arrayCatchFallbackRef - ? { catchFallbackRef: arrayCatchFallbackRef } - : {}), - ...(arrayCatchControl ? { catchControl: arrayCatchControl } : {}), - }; + wires.push( - withLoc({ from: srcRef, to: toRef, ...arrayWireAttrs }, wireLoc), + withLoc( + { + to: toRef, + sources: [ + { expr: { type: "ref", ref: srcRef } }, + ...arrayFallbacks, + ], + ...(arrayCatchHandler ? { catch: arrayCatchHandler } : {}), + }, + wireLoc, + ), ); wires.push(...arrayFallbackInternalWires); - wires.push(...arrayCatchFallbackInternalWires); + wires.push(...arrayCatchInternalWires); const iterName = extractNameToken(sub(arrayMappingNode, "iterName")!); assertNotReserved(iterName, lineNum, "iterator handle"); @@ -5854,7 +6066,7 @@ function buildBridgeBody( const elseBranch = extractTernaryBranch(elseNode, lineNum); // Process coalesce alternatives. - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(wireNode, "coalesceItem")) { const type = tok(item, "falsyOp") @@ -5863,63 +6075,82 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } // Process catch error fallback. - let catchLoc: SourceLocation | undefined; - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(wireNode, "catchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, lineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } wires.push( withLoc( { - cond: condRef, - ...(sourceLoc ? { condLoc: sourceLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), to: toRef, + sources: [ + { + expr: { + type: "ternary", + cond: { + type: "ref", + ref: condRef, + ...(sourceLoc ? { refLoc: sourceLoc } : {}), + }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(sourceLoc ? { condLoc: sourceLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), }, wireLoc, ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); continue; } sourceParts.push({ ref: condRef, isPipeFork: condIsPipeFork }); - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; let hasTruthyLiteralFallback = false; for (const item of subs(wireNode, "coalesceItem")) { @@ -5931,51 +6162,55 @@ function buildBridgeBody( const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, lineNum); if ("literal" in altResult) { - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if (type === "falsy") { hasTruthyLiteralFallback = Boolean(JSON.parse(altResult.literal)); } } else if ("control" in altResult) { - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); } else { - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); fallbackInternalWires.push(...wires.splice(preLen)); } } - let catchLoc: SourceLocation | undefined; - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(wireNode, "catchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, lineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0]; - const wireAttrs = { - ...(isSafe ? { safe: true as const } : {}), - ...(sourceLoc ? { fromLoc: sourceLoc } : {}), - ...(isPipe ? { pipe: true as const } : {}), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback ? { catchFallback } : {}), - ...(catchFallbackRef ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; - wires.push(withLoc({ from: fromRef, to: toRef, ...wireAttrs }, wireLoc)); + + wires.push( + withLoc( + { + to: toRef, + sources: [ + { + expr: { + type: "ref", + ref: fromRef, + ...(isSafe ? { safe: true } : {}), + ...(sourceLoc ? { refLoc: sourceLoc } : {}), + }, + }, + ...fallbacks, + ], + ...(catchHandler ? { catch: catchHandler } : {}), + ...(isPipe ? { pipe: true as const } : {}), + }, + wireLoc, + ), + ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); } // ── Step 3: Collect force statements ────────────────────────────────── @@ -6025,7 +6260,12 @@ function buildBridgeBody( if (elemC.elemEquals) { // Constant self-wire: .property = value const value = extractBareValue(sub(elemLine, "elemValue")!); - wires.push(withLoc({ value, to: toRef }, elemLineLoc)); + wires.push( + withLoc( + { to: toRef, sources: [{ expr: { type: "literal", value } }] }, + elemLineLoc, + ), + ); continue; } @@ -6055,11 +6295,26 @@ function buildBridgeBody( elemLineLoc, ); wires.push( - withLoc({ from: concatOutRef, to: toRef, pipe: true }, elemLineLoc), + withLoc( + { + to: toRef, + sources: [{ expr: { type: "ref", ref: concatOutRef } }], + pipe: true, + }, + elemLineLoc, + ), ); } else { // Plain string without interpolation — emit constant wire - wires.push(withLoc({ value: raw, to: toRef }, elemLineLoc)); + wires.push( + withLoc( + { + to: toRef, + sources: [{ expr: { type: "literal", value: raw } }], + }, + elemLineLoc, + ), + ); } continue; } @@ -6146,7 +6401,7 @@ function buildBridgeBody( const elseBranch = extractTernaryBranch(elseNode, elemLineNum); // Coalesce - const ternFallbacks: WireFallback[] = []; + const ternFallbacks: WireSourceEntry[] = []; const ternFallbackWires: Wire[] = []; for (const item of subs(elemLine, "elemCoalesceItem")) { const type = tok(item, "falsyOp") @@ -6155,27 +6410,20 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, elemLineNum); - ternFallbacks.push(buildWireFallback(type, altNode, altResult)); + ternFallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { ternFallbackWires.push(...wires.splice(preLen)); } } // Catch - let ternCatchFallback: string | undefined; - let ternCatchControl: ControlFlowInstruction | undefined; - let ternCatchFallbackRef: NodeRef | undefined; - let ternCatchLoc: SourceLocation | undefined; + let ternCatchHandler: WireCatch | undefined; let ternCatchWires: Wire[] = []; const ternCatchAlt = sub(elemLine, "elemCatchAlt"); if (ternCatchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(ternCatchAlt, elemLineNum); - const catchAttrs = buildCatchAttrs(ternCatchAlt, altResult); - ternCatchLoc = catchAttrs.catchLoc; - ternCatchFallback = catchAttrs.catchFallback; - ternCatchControl = catchAttrs.catchControl; - ternCatchFallbackRef = catchAttrs.catchFallbackRef; + ternCatchHandler = buildCatchHandler(ternCatchAlt, altResult); if ("sourceRef" in altResult) { ternCatchWires = wires.splice(preLen); } @@ -6184,26 +6432,48 @@ function buildBridgeBody( wires.push( withLoc( { - cond: condRef, - ...(elemCondLoc ? { condLoc: elemCondLoc } : {}), - thenLoc: thenBranch.loc, - ...(thenBranch.kind === "ref" - ? { thenRef: thenBranch.ref } - : { thenValue: thenBranch.value }), - elseLoc: elseBranch.loc, - ...(elseBranch.kind === "ref" - ? { elseRef: elseBranch.ref } - : { elseValue: elseBranch.value }), - ...(ternFallbacks.length > 0 ? { fallbacks: ternFallbacks } : {}), - ...(ternCatchLoc ? { catchLoc: ternCatchLoc } : {}), - ...(ternCatchFallback !== undefined - ? { catchFallback: ternCatchFallback } - : {}), - ...(ternCatchFallbackRef !== undefined - ? { catchFallbackRef: ternCatchFallbackRef } - : {}), - ...(ternCatchControl ? { catchControl: ternCatchControl } : {}), to: toRef, + sources: [ + { + expr: { + type: "ternary", + cond: { + type: "ref", + ref: condRef, + ...(elemCondLoc ? { refLoc: elemCondLoc } : {}), + }, + then: + thenBranch.kind === "ref" + ? { + type: "ref" as const, + ref: thenBranch.ref, + loc: thenBranch.loc, + } + : { + type: "literal" as const, + value: thenBranch.value, + loc: thenBranch.loc, + }, + else: + elseBranch.kind === "ref" + ? { + type: "ref" as const, + ref: elseBranch.ref, + loc: elseBranch.loc, + } + : { + type: "literal" as const, + value: elseBranch.value, + loc: elseBranch.loc, + }, + ...(elemCondLoc ? { condLoc: elemCondLoc } : {}), + thenLoc: thenBranch.loc, + elseLoc: elseBranch.loc, + }, + }, + ...ternFallbacks, + ], + ...(ternCatchHandler ? { catch: ternCatchHandler } : {}), }, elemLineLoc, ), @@ -6214,7 +6484,7 @@ function buildBridgeBody( } // ── Coalesce chains ── - const fallbacks: WireFallback[] = []; + const fallbacks: WireSourceEntry[] = []; const fallbackInternalWires: Wire[] = []; for (const item of subs(elemLine, "elemCoalesceItem")) { const type = tok(item, "falsyOp") @@ -6223,46 +6493,39 @@ function buildBridgeBody( const altNode = sub(item, "altValue")!; const preLen = wires.length; const altResult = extractCoalesceAlt(altNode, elemLineNum); - fallbacks.push(buildWireFallback(type, altNode, altResult)); + fallbacks.push(buildSourceEntry(type, altNode, altResult)); if ("sourceRef" in altResult) { fallbackInternalWires.push(...wires.splice(preLen)); } } // ── Catch fallback ── - let catchFallback: string | undefined; - let catchControl: ControlFlowInstruction | undefined; - let catchFallbackRef: NodeRef | undefined; - let catchLoc: SourceLocation | undefined; - let catchFallbackInternalWires: Wire[] = []; + let catchHandler: WireCatch | undefined; + let catchInternalWires: Wire[] = []; const catchAlt = sub(elemLine, "elemCatchAlt"); if (catchAlt) { const preLen = wires.length; const altResult = extractCoalesceAlt(catchAlt, elemLineNum); - const catchAttrs = buildCatchAttrs(catchAlt, altResult); - catchLoc = catchAttrs.catchLoc; - catchFallback = catchAttrs.catchFallback; - catchControl = catchAttrs.catchControl; - catchFallbackRef = catchAttrs.catchFallbackRef; + catchHandler = buildCatchHandler(catchAlt, altResult); if ("sourceRef" in altResult) { - catchFallbackInternalWires = wires.splice(preLen); + catchInternalWires = wires.splice(preLen); } } // Emit wire - const wireAttrs = { - ...(condIsPipeFork ? { pipe: true as const } : {}), - ...(fallbacks.length > 0 ? { fallbacks } : {}), - ...(catchLoc ? { catchLoc } : {}), - ...(catchFallback !== undefined ? { catchFallback } : {}), - ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}), - ...(catchControl ? { catchControl } : {}), - }; wires.push( - withLoc({ from: condRef, to: toRef, ...wireAttrs }, elemLineLoc), + withLoc( + { + to: toRef, + sources: [{ expr: { type: "ref", ref: condRef } }, ...fallbacks], + ...(catchHandler ? { catch: catchHandler } : {}), + ...(condIsPipeFork ? { pipe: true as const } : {}), + }, + elemLineLoc, + ), ); wires.push(...fallbackInternalWires); - wires.push(...catchFallbackInternalWires); + wires.push(...catchInternalWires); } } @@ -6358,24 +6621,56 @@ function inlineDefine( } // Remap existing bridge wires pointing at the generic define module + function remapModuleInExpr( + expr: Expression, + fromModule: string, + toModule: string, + ): Expression { + if (expr.type === "ref" && expr.ref.module === fromModule) { + return { ...expr, ref: { ...expr.ref, module: toModule } }; + } + if (expr.type === "ternary") { + return { + ...expr, + cond: remapModuleInExpr(expr.cond, fromModule, toModule), + then: remapModuleInExpr(expr.then, fromModule, toModule), + else: remapModuleInExpr(expr.else, fromModule, toModule), + }; + } + if (expr.type === "and" || expr.type === "or") { + return { + ...expr, + left: remapModuleInExpr(expr.left, fromModule, toModule), + right: remapModuleInExpr(expr.right, fromModule, toModule), + }; + } + return expr; + } + for (const wire of wires) { - if ("from" in wire) { - if (wire.to.module === genericModule) - wire.to = { ...wire.to, module: inModule }; - if (wire.from.module === genericModule) - wire.from = { ...wire.from, module: outModule }; - if (wire.fallbacks) { - wire.fallbacks = wire.fallbacks.map((f) => - f.ref && f.ref.module === genericModule - ? { ...f, ref: { ...f.ref, module: outModule } } - : f, - ); + if (wire.to.module === genericModule) + wire.to = { ...wire.to, module: inModule }; + if (wire.sources) { + for (let i = 0; i < wire.sources.length; i++) { + wire.sources[i] = { + ...wire.sources[i], + expr: remapModuleInExpr( + wire.sources[i].expr, + genericModule, + outModule, + ), + }; } - if (wire.catchFallbackRef?.module === genericModule) - wire.catchFallbackRef = { ...wire.catchFallbackRef, module: outModule }; } - if ("value" in wire && wire.to.module === genericModule) - wire.to = { ...wire.to, module: inModule }; + if ( + wire.catch && + "ref" in wire.catch && + wire.catch.ref.module === genericModule + ) + wire.catch = { + ...wire.catch, + ref: { ...wire.catch.ref, module: outModule }, + }; } const forkOffset = nextForkSeqRef.value; @@ -6413,20 +6708,42 @@ function inlineDefine( return ref; } + function remapExpr(expr: Expression, side: "from" | "to"): Expression { + if (expr.type === "ref") { + return { ...expr, ref: remapRef(expr.ref, side) }; + } + if (expr.type === "ternary") { + return { + ...expr, + cond: remapExpr(expr.cond, "from"), + then: remapExpr(expr.then, "from"), + else: remapExpr(expr.else, "from"), + }; + } + if (expr.type === "and" || expr.type === "or") { + return { + ...expr, + left: remapExpr(expr.left, "from"), + right: remapExpr(expr.right, "from"), + }; + } + return expr; + } + for (const wire of defineDef.wires) { const cloned: Wire = JSON.parse(JSON.stringify(wire)); - if ("from" in cloned) { - cloned.from = remapRef(cloned.from, "from"); - cloned.to = remapRef(cloned.to, "to"); - if (cloned.fallbacks) { - cloned.fallbacks = cloned.fallbacks.map((f) => - f.ref ? { ...f, ref: remapRef(f.ref, "from") } : f, - ); - } - if (cloned.catchFallbackRef) - cloned.catchFallbackRef = remapRef(cloned.catchFallbackRef, "from"); - } else { - cloned.to = remapRef(cloned.to, "to"); + cloned.to = remapRef(cloned.to, "to"); + if (cloned.sources) { + cloned.sources = cloned.sources.map((s) => ({ + ...s, + expr: remapExpr(s.expr, "from"), + })); + } + if (cloned.catch && "ref" in cloned.catch) { + cloned.catch = { + ...cloned.catch, + ref: remapRef(cloned.catch.ref, "from"), + }; } wires.push(cloned); } diff --git a/packages/bridge-parser/test/bridge-format.test.ts b/packages/bridge-parser/test/bridge-format.test.ts index 4761f972..6e67b7fb 100644 --- a/packages/bridge-parser/test/bridge-format.test.ts +++ b/packages/bridge-parser/test/bridge-format.test.ts @@ -16,8 +16,11 @@ import { SELF_MODULE, parsePath } from "@stackables/bridge-core"; import { assertDeepStrictEqualIgnoringLoc } from "./utils/parse-test-utils.ts"; import { bridge } from "@stackables/bridge-core"; -/** Pull wire — the Wire variant that has a `from` field */ -type PullWire = Extract; +/** Helper to extract the source ref from a Wire */ +function sourceRef(wire: Wire) { + const expr = wire.sources[0]?.expr; + return expr?.type === "ref" ? expr.ref : undefined; +} // ── parsePath ─────────────────────────────────────────────────────────────── @@ -95,26 +98,27 @@ describe("parseBridge", () => { assert.equal(instr.wires.length, 2); assertDeepStrictEqualIgnoringLoc(instr.wires[0], { - from: { - module: SELF_MODULE, - type: "Query", - field: "geocode", - path: ["search"], - }, to: { module: SELF_MODULE, type: "Query", field: "geocode", path: ["search"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: SELF_MODULE, + type: "Query", + field: "geocode", + path: ["search"], + }, + }, + }, + ], }); assertDeepStrictEqualIgnoringLoc(instr.wires[1], { - from: { - module: SELF_MODULE, - type: "Query", - field: "geocode", - path: ["search"], - }, to: { module: "hereapi", type: "Query", @@ -122,6 +126,19 @@ describe("parseBridge", () => { instance: 1, path: ["q"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: SELF_MODULE, + type: "Query", + field: "geocode", + path: ["search"], + }, + }, + }, + ], }); }); @@ -146,13 +163,6 @@ describe("parseBridge", () => { )!; assert.equal(instr.handles.length, 3); assertDeepStrictEqualIgnoringLoc(instr.wires[0], { - from: { - module: "api", - type: "Query", - field: "data", - instance: 1, - path: ["raw"], - }, to: { module: SELF_MODULE, type: "Tools", @@ -160,21 +170,42 @@ describe("parseBridge", () => { instance: 1, path: ["value"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "api", + type: "Query", + field: "data", + instance: 1, + path: ["raw"], + }, + }, + }, + ], }); assertDeepStrictEqualIgnoringLoc(instr.wires[1], { - from: { - module: SELF_MODULE, - type: "Tools", - field: "toInt", - instance: 1, - path: ["result"], - }, to: { module: SELF_MODULE, type: "Query", field: "health", path: ["output"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: SELF_MODULE, + type: "Tools", + field: "toInt", + instance: 1, + path: ["result"], + }, + }, + }, + ], }); }); @@ -194,26 +225,26 @@ describe("parseBridge", () => { const instr = result.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - assertDeepStrictEqualIgnoringLoc((instr.wires[0] as PullWire).from, { + assertDeepStrictEqualIgnoringLoc(sourceRef(instr.wires[0]!), { module: "zillow", type: "Query", field: "find", instance: 1, path: ["properties", "0", "streetAddress"], }); - assertDeepStrictEqualIgnoringLoc(instr.wires[0].to, { + assertDeepStrictEqualIgnoringLoc(instr.wires[0]!.to, { module: SELF_MODULE, type: "Query", field: "search", path: ["topPick", "address"], }); - assertDeepStrictEqualIgnoringLoc((instr.wires[1] as PullWire).from.path, [ + assertDeepStrictEqualIgnoringLoc(sourceRef(instr.wires[1]!)?.path, [ "properties", "0", "location", "city", ]); - assertDeepStrictEqualIgnoringLoc(instr.wires[1].to.path, [ + assertDeepStrictEqualIgnoringLoc(instr.wires[1]!.to.path, [ "topPick", "city", ]); @@ -239,49 +270,70 @@ describe("parseBridge", () => { )!; assert.equal(instr.wires.length, 3); assertDeepStrictEqualIgnoringLoc(instr.wires[0], { - from: { - module: "provider", - type: "Query", - field: "list", - instance: 1, - path: ["items"], - }, to: { module: SELF_MODULE, type: "Query", field: "search", path: ["results"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "provider", + type: "Query", + field: "list", + instance: 1, + path: ["items"], + }, + }, + }, + ], }); assertDeepStrictEqualIgnoringLoc(instr.wires[1], { - from: { - module: SELF_MODULE, - type: "Query", - field: "search", - element: true, - path: ["title"], - }, to: { module: SELF_MODULE, type: "Query", field: "search", path: ["results", "name"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: SELF_MODULE, + type: "Query", + field: "search", + element: true, + path: ["title"], + }, + }, + }, + ], }); assertDeepStrictEqualIgnoringLoc(instr.wires[2], { - from: { - module: SELF_MODULE, - type: "Query", - field: "search", - element: true, - path: ["position", "lat"], - }, to: { module: SELF_MODULE, type: "Query", field: "search", path: ["results", "lat"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: SELF_MODULE, + type: "Query", + field: "search", + element: true, + path: ["position", "lat"], + }, + }, + }, + ], }); }); @@ -303,14 +355,14 @@ describe("parseBridge", () => { (i): i is Bridge => i.kind === "bridge", )!; assert.equal(instr.type, "Mutation"); - assertDeepStrictEqualIgnoringLoc(instr.wires[0].to, { + assertDeepStrictEqualIgnoringLoc(instr.wires[0]!.to, { module: "sendgrid", type: "Mutation", field: "send", instance: 1, path: ["content"], }); - assertDeepStrictEqualIgnoringLoc((instr.wires[1] as PullWire).from.path, [ + assertDeepStrictEqualIgnoringLoc(sourceRef(instr.wires[1]!)?.path, [ "headers", "x-message-id", ]); @@ -366,7 +418,7 @@ describe("parseBridge", () => { handle: "c", kind: "context", }); - assertDeepStrictEqualIgnoringLoc((instr.wires[0] as PullWire).from, { + assertDeepStrictEqualIgnoringLoc(sourceRef(instr.wires[0]!), { module: SELF_MODULE, type: "Context", field: "context", @@ -533,12 +585,6 @@ describe("serializeBridge", () => { ], wires: [ { - from: { - module: SELF_MODULE, - type: "Mutation", - field: "sendEmail", - path: ["body"], - }, to: { module: "sendgrid", type: "Mutation", @@ -546,22 +592,42 @@ describe("serializeBridge", () => { instance: 1, path: ["content"], }, - }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: SELF_MODULE, + type: "Mutation", + field: "sendEmail", + path: ["body"], + }, + }, + }, + ], + } as Wire, { - from: { - module: "sendgrid", - type: "Mutation", - field: "send", - instance: 1, - path: ["headers", "x-message-id"], - }, to: { module: SELF_MODULE, type: "Mutation", field: "sendEmail", path: ["messageId"], }, - }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "sendgrid", + type: "Mutation", + field: "send", + instance: 1, + path: ["headers", "x-message-id"], + }, + }, + }, + ], + } as Wire, ], }, ]; @@ -700,22 +766,36 @@ describe("parseBridge: tool blocks", () => { ]); assertDeepStrictEqualIgnoringLoc(root.wires, [ { - value: "https://geocode.search.hereapi.com/v1", to: { module: "_", type: "Tools", field: "hereapi", path: ["baseUrl"] }, + sources: [ + { + expr: { + type: "literal", + value: "https://geocode.search.hereapi.com/v1", + }, + }, + ], }, { - from: { - module: "_", - type: "Context", - field: "context", - path: ["hereapi", "apiKey"], - }, to: { module: "_", type: "Tools", field: "hereapi", path: ["headers", "apiKey"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "_", + type: "Context", + field: "context", + path: ["hereapi", "apiKey"], + }, + }, + }, + ], }, ]); @@ -724,22 +804,22 @@ describe("parseBridge: tool blocks", () => { assert.equal(child.extends, "hereapi"); assertDeepStrictEqualIgnoringLoc(child.wires, [ { - value: "GET", to: { module: "_", type: "Tools", field: "hereapi.geocode", path: ["method"], }, + sources: [{ expr: { type: "literal", value: "GET" } }], }, { - value: "/geocode", to: { module: "_", type: "Tools", field: "hereapi.geocode", path: ["path"], }, + sources: [{ expr: { type: "literal", value: "/geocode" } }], }, ]); }); @@ -773,36 +853,45 @@ describe("parseBridge: tool blocks", () => { )!; assertDeepStrictEqualIgnoringLoc(root.wires, [ { - value: "https://api.sendgrid.com/v3", to: { module: "_", type: "Tools", field: "sendgrid", path: ["baseUrl"], }, + sources: [ + { expr: { type: "literal", value: "https://api.sendgrid.com/v3" } }, + ], }, { - from: { - module: "_", - type: "Context", - field: "context", - path: ["sendgrid", "bearerToken"], - }, to: { module: "_", type: "Tools", field: "sendgrid", path: ["headers", "Authorization"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "_", + type: "Context", + field: "context", + path: ["sendgrid", "bearerToken"], + }, + }, + }, + ], }, { - value: "static-value", to: { module: "_", type: "Tools", field: "sendgrid", path: ["headers", "X-Custom"], }, + sources: [{ expr: { type: "literal", value: "static-value" } }], }, ]); @@ -812,22 +901,22 @@ describe("parseBridge: tool blocks", () => { assert.equal(child.extends, "sendgrid"); assertDeepStrictEqualIgnoringLoc(child.wires, [ { - value: "POST", to: { module: "_", type: "Tools", field: "sendgrid.send", path: ["method"], }, + sources: [{ expr: { type: "literal", value: "POST" } }], }, { - value: "/mail/send", to: { module: "_", type: "Tools", field: "sendgrid.send", path: ["path"], }, + sources: [{ expr: { type: "literal", value: "/mail/send" } }], }, ]); }); @@ -867,19 +956,26 @@ describe("parseBridge: tool blocks", () => { { kind: "tool", handle: "auth", name: "authService" }, ]); assertDeepStrictEqualIgnoringLoc(serviceB.wires[1], { - from: { - module: "_", - type: "Tools", - field: "authService", - path: ["access_token"], - instance: 1, - }, to: { module: "_", type: "Tools", field: "serviceB", path: ["headers", "Authorization"], }, + sources: [ + { + expr: { + type: "ref", + ref: { + module: "_", + type: "Tools", + field: "authService", + path: ["access_token"], + instance: 1, + }, + }, + }, + ], }); }); }); @@ -1152,7 +1248,10 @@ describe("parser robustness", () => { "version 1.5\nbridge Query.search {\n\twith hereapi.geocode as gc\n\twith input as i\n\twith output as o\n\ngc.q <- i.search\no.results <- gc.items[] as item {\n\t.lat <- item.position.lat\n\t.lng <- item.position.lng\n}\n}\n", ).instructions.find((i) => i.kind === "bridge") as Bridge; assert.equal( - instr.wires.filter((w) => "from" in w && w.from.element).length, + instr.wires.filter( + (w) => + w.sources[0]?.expr.type === "ref" && w.sources[0].expr.ref.element, + ).length, 2, ); }); @@ -1168,11 +1267,13 @@ describe("parser robustness", () => { o.name <- i.username # copy the name across } `).instructions.find((inst) => inst.kind === "bridge") as Bridge; - const wire = instr.wires.find( - (w) => "from" in w && !("value" in w), - ) as PullWire; + const wire = instr.wires.find((w) => w.sources[0]?.expr.type === "ref")!; assert.equal(wire.to.path.join("."), "name"); - assert.equal(wire.from.path.join("."), "username"); + const expr = wire.sources[0]!.expr; + assert.equal( + expr.type === "ref" ? expr.ref.path.join(".") : undefined, + "username", + ); }); test("# inside a string literal is not treated as a comment", () => { @@ -1184,11 +1285,14 @@ describe("parser robustness", () => { } `).instructions.find((inst) => inst.kind === "tool") as ToolDef; const urlWire = tool.wires.find( - (w) => "value" in w && w.to.path.join(".") === "url", + (w) => + w.sources[0]?.expr.type === "literal" && w.to.path.join(".") === "url", ); assert.ok(urlWire); assert.equal( - (urlWire as { value: string }).value, + urlWire.sources[0]!.expr.type === "literal" + ? urlWire.sources[0]!.expr.value + : undefined, "https://example.com/things#anchor", ); }); diff --git a/packages/bridge-parser/test/expressions-parser.test.ts b/packages/bridge-parser/test/expressions-parser.test.ts index a33e543e..d747f584 100644 --- a/packages/bridge-parser/test/expressions-parser.test.ts +++ b/packages/bridge-parser/test/expressions-parser.test.ts @@ -115,10 +115,13 @@ describe("expressions: parser desugaring", () => { `); const instr = doc.instructions.find((i) => i.kind === "bridge")!; const bWire = instr.wires.find( - (w) => "from" in w && w.to.path.length === 1 && w.to.path[0] === "b", + (w) => + w.sources[0]?.expr.type === "ref" && + w.to.path.length === 1 && + w.to.path[0] === "b", ); assert.ok(bWire, "should have a .b wire"); - assert.ok("from" in bWire!); + assert.ok(bWire!.sources[0]?.expr.type === "ref"); }); test("expression in array mapping element", () => { @@ -572,9 +575,16 @@ describe("serializeBridge: keyword strings are quoted", () => { (i) => i.kind === "bridge", ) as any; const wire = instr.wires.find( - (w: any) => "value" in w && w.to?.path?.[0] === "result", + (w: any) => + w.sources?.[0]?.expr?.type === "literal" && + w.to?.path?.[0] === "result", + ); + assert.equal( + wire?.sources[0]?.expr.type === "literal" + ? wire.sources[0].expr.value + : undefined, + kw, ); - assert.equal(wire?.value, kw); }); } }); diff --git a/packages/bridge-parser/test/force-wire-parser.test.ts b/packages/bridge-parser/test/force-wire-parser.test.ts index 51a7ef67..b17bcdf3 100644 --- a/packages/bridge-parser/test/force-wire-parser.test.ts +++ b/packages/bridge-parser/test/force-wire-parser.test.ts @@ -74,13 +74,7 @@ describe("parseBridge: force ", () => { assert.equal(instr.forces!.length, 1); assert.equal(instr.forces![0].handle, "audit"); for (const w of instr.wires) { - if ("from" in w) { - assert.equal( - (w as any).force, - undefined, - "wires should not have force", - ); - } + assert.equal((w as any).force, undefined, "wires should not have force"); } }); @@ -167,11 +161,7 @@ describe("parseBridge: force ", () => { assert.ok(instr.forces); assert.equal(instr.forces![0].handle, "se"); - assert.equal( - instr.forces![0].catchError, - undefined, - "default is critical", - ); + assert.equal(instr.forces![0].catchError, undefined, "default is critical"); }); test("force catch null sets catchError flag", () => { diff --git a/packages/bridge-parser/test/path-scoping-parser.test.ts b/packages/bridge-parser/test/path-scoping-parser.test.ts index 703dc54c..64e88495 100644 --- a/packages/bridge-parser/test/path-scoping-parser.test.ts +++ b/packages/bridge-parser/test/path-scoping-parser.test.ts @@ -4,7 +4,7 @@ import { parseBridgeFormat as parseBridge, serializeBridge, } from "../src/index.ts"; -import type { Bridge, Wire } from "@stackables/bridge-core"; +import type { Bridge } from "@stackables/bridge-core"; import { assertDeepStrictEqualIgnoringLoc } from "./utils/parse-test-utils.ts"; import { bridge } from "@stackables/bridge-core"; @@ -29,7 +29,7 @@ describe("path scoping – parser", () => { )!; assert.ok(instr); const constWires = instr.wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); assert.equal(constWires.length, 2); const theme = constWires.find( @@ -39,9 +39,19 @@ describe("path scoping – parser", () => { (w) => w.to.path.join(".") === "settings.lang", ); assert.ok(theme); - assert.equal(theme.value, "dark"); + assert.equal( + theme.sources[0]!.expr.type === "literal" + ? theme.sources[0]!.expr.value + : undefined, + "dark", + ); assert.ok(lang); - assert.equal(lang.value, "en"); + assert.equal( + lang.sources[0]!.expr.type === "literal" + ? lang.sources[0]!.expr.value + : undefined, + "en", + ); }); test("scope block with pull wires", () => { @@ -62,7 +72,7 @@ describe("path scoping – parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); assert.equal(pullWires.length, 2); const nameWire = pullWires.find((w) => w.to.path.join(".") === "user.name"); @@ -70,9 +80,17 @@ describe("path scoping – parser", () => { (w) => w.to.path.join(".") === "user.email", ); assert.ok(nameWire); - assertDeepStrictEqualIgnoringLoc(nameWire.from.path, ["name"]); + const nameExpr = nameWire.sources[0]!.expr; + assertDeepStrictEqualIgnoringLoc( + nameExpr.type === "ref" ? nameExpr.ref.path : undefined, + ["name"], + ); assert.ok(emailWire); - assertDeepStrictEqualIgnoringLoc(emailWire.from.path, ["email"]); + const emailExpr = emailWire.sources[0]!.expr; + assertDeepStrictEqualIgnoringLoc( + emailExpr.type === "ref" ? emailExpr.ref.path : undefined, + ["email"], + ); }); test("nested scope blocks", () => { @@ -101,9 +119,7 @@ describe("path scoping – parser", () => { const wires = instr.wires; // Pull wires - const pullWires = wires.filter( - (w): w is Extract => "from" in w, - ); + const pullWires = wires.filter((w) => w.sources[0]?.expr.type === "ref"); const idWire = pullWires.find( (w) => w.to.path.join(".") === "body.user.profile.id", ); @@ -112,12 +128,20 @@ describe("path scoping – parser", () => { ); assert.ok(idWire, "id wire should exist"); assert.ok(nameWire, "name wire should exist"); - assertDeepStrictEqualIgnoringLoc(idWire.from.path, ["id"]); - assertDeepStrictEqualIgnoringLoc(nameWire.from.path, ["name"]); + const idExpr = idWire.sources[0]!.expr; + assertDeepStrictEqualIgnoringLoc( + idExpr.type === "ref" ? idExpr.ref.path : undefined, + ["id"], + ); + const nameExpr2 = nameWire.sources[0]!.expr; + assertDeepStrictEqualIgnoringLoc( + nameExpr2.type === "ref" ? nameExpr2.ref.path : undefined, + ["name"], + ); // Constant wires const constWires = wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); const themeWire = constWires.find( (w) => w.to.path.join(".") === "body.user.settings.theme", @@ -126,9 +150,19 @@ describe("path scoping – parser", () => { (w) => w.to.path.join(".") === "body.user.settings.notifications", ); assert.ok(themeWire); - assert.equal(themeWire.value, "dark"); + assert.equal( + themeWire.sources[0]!.expr.type === "literal" + ? themeWire.sources[0]!.expr.value + : undefined, + "dark", + ); assert.ok(notifWire); - assert.equal(notifWire.value, "true"); + assert.equal( + notifWire.sources[0]!.expr.type === "literal" + ? notifWire.sources[0]!.expr.value + : undefined, + "true", + ); }); test("scope block with pipe operator", () => { @@ -170,19 +204,29 @@ describe("path scoping – parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const nameWire = pullWires.find((w) => w.to.path.join(".") === "data.name"); assert.ok(nameWire); - assertDeepStrictEqualIgnoringLoc(nameWire.fallbacks, [ - { type: "falsy", value: '"anonymous"' }, - ]); + assert.equal(nameWire.sources.length, 2); + assert.equal(nameWire.sources[1]!.gate, "falsy"); + assert.equal( + nameWire.sources[1]!.expr.type === "literal" + ? nameWire.sources[1]!.expr.value + : undefined, + '"anonymous"', + ); const valueWire = pullWires.find( (w) => w.to.path.join(".") === "data.value", ); assert.ok(valueWire); - assert.equal(valueWire.catchFallback, "0"); + assert.equal( + valueWire.catch && "value" in valueWire.catch + ? valueWire.catch.value + : undefined, + "0", + ); }); test("scope block with expression", () => { @@ -222,7 +266,9 @@ describe("path scoping – parser", () => { const instr = result.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const ternaryWires = instr.wires.filter((w) => "cond" in w); + const ternaryWires = instr.wires.filter( + (w) => w.sources[0]?.expr.type === "ternary", + ); assert.equal(ternaryWires.length, 2); }); @@ -266,7 +312,7 @@ describe("path scoping – parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const constWires = instr.wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); assert.equal(constWires.length, 3); assert.ok(constWires.find((w) => w.to.path.join(".") === "method")); @@ -299,7 +345,7 @@ describe("path scoping – parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const nameWire = pullWires.find((w) => w.to.path.join(".") === "body.name"); const emailWire = pullWires.find( @@ -377,9 +423,7 @@ describe("path scoping – parser", () => { const br = parsed.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const pullWires = br.wires.filter( - (w): w is Extract => "from" in w, - ); + const pullWires = br.wires.filter((w) => w.sources[0]?.expr.type === "ref"); const qWire = pullWires.find((w) => w.to.path.join(".") === "q"); assert.ok(qWire, "wire to api.q should exist"); }); @@ -406,9 +450,7 @@ describe("path scoping – parser", () => { const br = parsed.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const pullWires = br.wires.filter( - (w): w is Extract => "from" in w, - ); + const pullWires = br.wires.filter((w) => w.sources[0]?.expr.type === "ref"); // Alias creates a __local wire const localWire = pullWires.find( (w) => w.to.module === "__local" && w.to.field === "upper", @@ -419,8 +461,15 @@ describe("path scoping – parser", () => { (w) => w.to.path.join(".") === "info.displayName", ); assert.ok(displayWire, "wire to o.info.displayName should exist"); - assert.equal(displayWire!.from.module, "__local"); - assert.equal(displayWire!.from.field, "upper"); + const displayExpr = displayWire!.sources[0]!.expr; + assert.equal( + displayExpr.type === "ref" ? displayExpr.ref.module : undefined, + "__local", + ); + assert.equal( + displayExpr.type === "ref" ? displayExpr.ref.field : undefined, + "upper", + ); // email wire reads from input const emailWire = pullWires.find( (w) => w.to.path.join(".") === "info.email", @@ -514,11 +563,16 @@ describe("path scoping – array mapper blocks", () => { (i): i is Bridge => i.kind === "bridge", )!; const constWires = instr.wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); assert.equal(constWires.length, 1); const wire = constWires[0]; - assert.equal(wire.value, "1"); + assert.equal( + wire.sources[0]!.expr.type === "literal" + ? wire.sources[0]!.expr.value + : undefined, + "1", + ); assertDeepStrictEqualIgnoringLoc(wire.to.path, ["obj", "etc"]); assert.equal(wire.to.element, true); }); @@ -542,12 +596,19 @@ describe("path scoping – array mapper blocks", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const nameWire = pullWires.find((w) => w.to.path.join(".") === "obj.name"); assert.ok(nameWire, "wire to obj.name should exist"); - assert.equal(nameWire!.from.element, true); - assertDeepStrictEqualIgnoringLoc(nameWire!.from.path, ["title"]); + const nameExpr = nameWire!.sources[0]!.expr; + assert.equal( + nameExpr.type === "ref" ? nameExpr.ref.element : undefined, + true, + ); + assertDeepStrictEqualIgnoringLoc( + nameExpr.type === "ref" ? nameExpr.ref.path : undefined, + ["title"], + ); }); test("nested scope blocks inside array mapper flatten to correct paths", () => { @@ -571,7 +632,7 @@ describe("path scoping – array mapper blocks", () => { (i): i is Bridge => i.kind === "bridge", )!; const constWires = instr.wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); assert.equal(constWires.length, 1); assertDeepStrictEqualIgnoringLoc(constWires[0].to.path, ["a", "b", "c"]); @@ -599,10 +660,10 @@ describe("path scoping – array mapper blocks", () => { (i): i is Bridge => i.kind === "bridge", )!; const constWires = instr.wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); assert.ok( constWires.find((w) => w.to.path.join(".") === "nested.x"), @@ -642,11 +703,16 @@ describe("path scoping – spread syntax parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const spreadWire = pullWires.find((w) => w.to.path.length === 0); assert.ok(spreadWire, "spread wire targeting tool root should exist"); - assertDeepStrictEqualIgnoringLoc(spreadWire.from.path, []); + assertDeepStrictEqualIgnoringLoc( + spreadWire.sources[0]!.expr.type === "ref" + ? spreadWire.sources[0]!.expr.ref.path + : undefined, + [], + ); }); test("spread combined with constant wires in scope block", () => { @@ -670,10 +736,10 @@ describe("path scoping – spread syntax parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const constWires = instr.wires.filter( - (w): w is Extract => "value" in w, + (w) => w.sources[0]?.expr.type === "literal", ); assert.ok( pullWires.find((w) => w.to.path.length === 0), @@ -705,11 +771,16 @@ describe("path scoping – spread syntax parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const spreadWire = pullWires.find((w) => w.to.path.length === 0); assert.ok(spreadWire, "spread wire should exist"); - assertDeepStrictEqualIgnoringLoc(spreadWire.from.path, ["profile"]); + assertDeepStrictEqualIgnoringLoc( + spreadWire.sources[0]!.expr.type === "ref" + ? spreadWire.sources[0]!.expr.ref.path + : undefined, + ["profile"], + ); }); test("spread in nested scope block produces wire to nested path", () => { @@ -730,10 +801,14 @@ describe("path scoping – spread syntax parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const spreadWire = pullWires.find( - (w) => w.to.path.join(".") === "wrapper" && w.from.path.length === 0, + (w) => + w.to.path.join(".") === "wrapper" && + (w.sources[0]!.expr.type === "ref" + ? w.sources[0]!.expr.ref.path.length === 0 + : false), ); assert.ok(spreadWire, "spread wire to o.wrapper should exist"); }); @@ -758,10 +833,15 @@ describe("path scoping – spread syntax parser", () => { (i): i is Bridge => i.kind === "bridge", )!; const pullWires = instr.wires.filter( - (w): w is Extract => "from" in w, + (w) => w.sources[0]?.expr.type === "ref", ); const spreadWire = pullWires.find((w) => w.to.path.join(".") === "nested"); assert.ok(spreadWire, "spread wire to tool.nested should exist"); - assertDeepStrictEqualIgnoringLoc(spreadWire.from.path, []); + assertDeepStrictEqualIgnoringLoc( + spreadWire.sources[0]!.expr.type === "ref" + ? spreadWire.sources[0]!.expr.ref.path + : undefined, + [], + ); }); }); diff --git a/packages/bridge-parser/test/resilience-parser.test.ts b/packages/bridge-parser/test/resilience-parser.test.ts index 6e63ff0d..e78b300e 100644 --- a/packages/bridge-parser/test/resilience-parser.test.ts +++ b/packages/bridge-parser/test/resilience-parser.test.ts @@ -4,13 +4,7 @@ import { parseBridgeFormat as parseBridge, serializeBridge, } from "@stackables/bridge-parser"; -import type { - Bridge, - ConstDef, - NodeRef, - ToolDef, - Wire, -} from "@stackables/bridge-core"; +import type { Bridge, ConstDef, ToolDef } from "@stackables/bridge-core"; import { assertDeepStrictEqualIgnoringLoc } from "./utils/parse-test-utils.ts"; import { bridge } from "@stackables/bridge-core"; @@ -35,9 +29,7 @@ describe("parseBridge: const blocks", () => { const c = parseBridge(bridge` version 1.5 const currency = "EUR" - `).instructions.find( - (i): i is ConstDef => i.kind === "const", - )!; + `).instructions.find((i): i is ConstDef => i.kind === "const")!; assert.equal(c.name, "currency"); assert.equal(JSON.parse(c.value), "EUR"); }); @@ -55,9 +47,7 @@ describe("parseBridge: const blocks", () => { const c = parseBridge(bridge` version 1.5 const empty = null - `).instructions.find( - (i): i is ConstDef => i.kind === "const", - )!; + `).instructions.find((i): i is ConstDef => i.kind === "const")!; assert.equal(JSON.parse(c.value), null); }); @@ -299,13 +289,12 @@ describe("parseBridge: wire fallback (catch)", () => { } `).instructions.find((i): i is Bridge => i.kind === "bridge")!; - const fbWire = instr.wires.find( - (w) => "from" in w && w.catchFallback != null, + const fbWire = instr.wires.find((w) => w.catch && "value" in w.catch); + assert.ok(fbWire, "should have a wire with catch"); + assert.equal( + "value" in fbWire!.catch! ? fbWire!.catch.value : undefined, + "0", ); - assert.ok(fbWire, "should have a wire with catchFallback"); - if ("from" in fbWire!) { - assert.equal(fbWire.catchFallback, "0"); - } }); test("catch with JSON object catchFallback", () => { @@ -322,13 +311,12 @@ describe("parseBridge: wire fallback (catch)", () => { } `).instructions.find((i): i is Bridge => i.kind === "bridge")!; - const fbWire = instr.wires.find( - (w) => "from" in w && w.catchFallback != null, - ); + const fbWire = instr.wires.find((w) => w.catch && "value" in w.catch); assert.ok(fbWire); - if ("from" in fbWire!) { - assert.equal(fbWire.catchFallback, `{"default":true}`); - } + assert.equal( + "value" in fbWire!.catch! ? fbWire!.catch.value : undefined, + `{"default":true}`, + ); }); test("catch with string catchFallback", () => { @@ -345,13 +333,12 @@ describe("parseBridge: wire fallback (catch)", () => { } `).instructions.find((i): i is Bridge => i.kind === "bridge")!; - const fbWire = instr.wires.find( - (w) => "from" in w && w.catchFallback != null, - ); + const fbWire = instr.wires.find((w) => w.catch && "value" in w.catch); assert.ok(fbWire); - if ("from" in fbWire!) { - assert.equal(fbWire.catchFallback, `"unknown"`); - } + assert.equal( + "value" in fbWire!.catch! ? fbWire!.catch.value : undefined, + `"unknown"`, + ); }); test("catch with null catchFallback", () => { @@ -368,13 +355,12 @@ describe("parseBridge: wire fallback (catch)", () => { } `).instructions.find((i): i is Bridge => i.kind === "bridge")!; - const fbWire = instr.wires.find( - (w) => "from" in w && w.catchFallback != null, - ); + const fbWire = instr.wires.find((w) => w.catch && "value" in w.catch); assert.ok(fbWire); - if ("from" in fbWire!) { - assert.equal(fbWire.catchFallback, "null"); - } + assert.equal( + "value" in fbWire!.catch! ? fbWire!.catch.value : undefined, + "null", + ); }); test("catch on pipe chain attaches to output wire", () => { @@ -391,16 +377,15 @@ describe("parseBridge: wire fallback (catch)", () => { } `).instructions.find((i): i is Bridge => i.kind === "bridge")!; - const fbWire = instr.wires.find( - (w) => "from" in w && w.catchFallback != null, + const fbWire = instr.wires.find((w) => w.catch && "value" in w.catch); + assert.ok(fbWire, "should have pipe output wire with catch"); + assert.equal( + "value" in fbWire!.catch! ? fbWire!.catch.value : undefined, + `"fallback"`, ); - assert.ok(fbWire, "should have pipe output wire with catchFallback"); - if ("from" in fbWire!) { - assert.equal(fbWire.catchFallback, `"fallback"`); - } }); - test("wires without catch have no catchFallback property", () => { + test("wires without catch have no catch property", () => { const instr = parseBridge(bridge` version 1.5 @@ -416,13 +401,7 @@ describe("parseBridge: wire fallback (catch)", () => { `).instructions.find((i): i is Bridge => i.kind === "bridge")!; for (const w of instr.wires) { - if ("from" in w) { - assert.equal( - w.catchFallback, - undefined, - "no catchFallback on regular wire", - ); - } + assert.equal(w.catch, undefined, "no catch on regular wire"); } }); }); @@ -501,11 +480,16 @@ describe("parseBridge: wire || falsy-fallback", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const wire = instr.wires[0] as Extract; - assertDeepStrictEqualIgnoringLoc(wire.fallbacks, [ - { type: "falsy", value: '"World"' }, - ]); - assert.equal(wire.catchFallback, undefined); + const wire = instr.wires[0]!; + assert.equal(wire.sources.length, 2); + assert.equal(wire.sources[1]!.gate, "falsy"); + assert.equal( + wire.sources[1]!.expr.type === "literal" + ? wire.sources[1]!.expr.value + : undefined, + '"World"', + ); + assert.equal(wire.catch, undefined); }); test("wire with both || and catch", () => { @@ -523,11 +507,17 @@ describe("parseBridge: wire || falsy-fallback", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const wire = instr.wires[0] as Extract; - assertDeepStrictEqualIgnoringLoc(wire.fallbacks, [ - { type: "falsy", value: '"World"' }, - ]); - assert.equal(wire.catchFallback, '"Error"'); + const wire = instr.wires[0]!; + assert.equal(wire.sources.length, 2); + assert.equal(wire.sources[1]!.gate, "falsy"); + assert.equal( + wire.sources[1]!.expr.type === "literal" + ? wire.sources[1]!.expr.value + : undefined, + '"World"', + ); + assert.ok(wire.catch && "value" in wire.catch); + assert.equal(wire.catch.value, '"Error"'); }); test("wire with || JSON object literal", () => { @@ -548,11 +538,18 @@ describe("parseBridge: wire || falsy-fallback", () => { (i): i is Bridge => i.kind === "bridge", )!; const wire = instr.wires.find( - (w) => "from" in w && (w as any).from.path[0] === "data", - ) as Extract; - assertDeepStrictEqualIgnoringLoc(wire.fallbacks, [ - { type: "falsy", value: '{"lat":0,"lon":0}' }, - ]); + (w) => + w.sources[0]?.expr.type === "ref" && + w.sources[0].expr.ref.path[0] === "data", + )!; + assert.equal(wire.sources.length, 2); + assert.equal(wire.sources[1]!.gate, "falsy"); + assert.equal( + wire.sources[1]!.expr.type === "literal" + ? wire.sources[1]!.expr.value + : undefined, + '{"lat":0,"lon":0}', + ); }); test("wire without || has no fallbacks", () => { @@ -570,8 +567,8 @@ describe("parseBridge: wire || falsy-fallback", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const wire = instr.wires[0] as Extract; - assert.equal(wire.fallbacks, undefined); + const wire = instr.wires[0]!; + assert.equal(wire.sources.length, 1, "should have no fallback sources"); }); test("pipe wire with || falsy-fallback", () => { @@ -592,11 +589,18 @@ describe("parseBridge: wire || falsy-fallback", () => { )!; const terminalWire = instr.wires.find( (w) => - "from" in w && (w as any).pipe && (w as any).from.path.length === 0, - ) as Extract; - assertDeepStrictEqualIgnoringLoc(terminalWire?.fallbacks, [ - { type: "falsy", value: '"N/A"' }, - ]); + w.pipe && + w.sources[0]?.expr.type === "ref" && + w.sources[0].expr.ref.path.length === 0, + )!; + assert.equal(terminalWire.sources.length, 2); + assert.equal(terminalWire.sources[1]!.gate, "falsy"); + assert.equal( + terminalWire.sources[1]!.expr.type === "literal" + ? terminalWire.sources[1]!.expr.value + : undefined, + '"N/A"', + ); }); }); @@ -677,15 +681,18 @@ describe("parseBridge: || source references", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const labelWires = instr.wires.filter( - (w) => "from" in w && (w as any).to.path[0] === "label", - ) as Extract[]; + const labelWires = instr.wires.filter((w) => w.to.path[0] === "label"); assert.equal(labelWires.length, 1, "should be one wire, not two"); - assert.ok(labelWires[0].fallbacks, "should have fallbacks"); - assert.equal(labelWires[0].fallbacks!.length, 1); - assert.equal(labelWires[0].fallbacks![0].type, "falsy"); - assert.deepEqual(labelWires[0].fallbacks![0].ref!.path, ["label"]); - assert.equal(labelWires[0].catchFallback, undefined); + assert.ok( + labelWires[0].sources.length >= 2, + "should have fallback sources", + ); + assert.equal(labelWires[0].sources[1]!.gate, "falsy"); + const fb0Expr = labelWires[0].sources[1]!.expr; + assert.deepEqual(fb0Expr.type === "ref" ? fb0Expr.ref.path : undefined, [ + "label", + ]); + assert.equal(labelWires[0].catch, undefined); }); test("|| source || literal — one wire with fallbacks", () => { @@ -707,16 +714,21 @@ describe("parseBridge: || source references", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const labelWires = instr.wires.filter( - (w) => "from" in w && (w as any).to.path[0] === "label", - ) as Extract[]; + const labelWires = instr.wires.filter((w) => w.to.path[0] === "label"); assert.equal(labelWires.length, 1); - assert.ok(labelWires[0].fallbacks, "should have fallbacks"); - assert.equal(labelWires[0].fallbacks!.length, 2); - assert.equal(labelWires[0].fallbacks![0].type, "falsy"); - assert.ok(labelWires[0].fallbacks![0].ref); - assert.equal(labelWires[0].fallbacks![1].type, "falsy"); - assert.equal(labelWires[0].fallbacks![1].value, '"default"'); + assert.ok( + labelWires[0].sources.length >= 3, + "should have 2 fallback sources", + ); + assert.equal(labelWires[0].sources[1]!.gate, "falsy"); + assert.equal(labelWires[0].sources[1]!.expr.type, "ref"); + assert.equal(labelWires[0].sources[2]!.gate, "falsy"); + assert.equal( + labelWires[0].sources[2]!.expr.type === "literal" + ? labelWires[0].sources[2]!.expr.value + : undefined, + '"default"', + ); }); }); @@ -742,16 +754,16 @@ describe("parseBridge: catch source/pipe references", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const wire = instr.wires.find( - (w) => "from" in w && (w as any).to.path[0] === "label", - ) as Extract; - assert.ok(wire.catchFallbackRef, "should have catchFallbackRef"); + const wire = instr.wires.find((w) => w.to.path[0] === "label")!; + assert.ok(wire.catch && "ref" in wire.catch, "should have catch ref"); assert.equal( - wire.catchFallback, + wire.catch && "value" in wire.catch ? wire.catch.value : undefined, undefined, - "should not have JSON catchFallback", + "should not have JSON catch value", ); - assert.deepEqual(wire.catchFallbackRef!.path, ["fallbackLabel"]); + assert.deepEqual("ref" in wire.catch ? wire.catch.ref.path : undefined, [ + "fallbackLabel", + ]); }); test("catch pipe:source stores catchFallbackRef pointing to fork root + registers fork", () => { @@ -772,11 +784,9 @@ describe("parseBridge: catch source/pipe references", () => { const instr = doc.instructions.find( (i): i is Bridge => i.kind === "bridge", )!; - const wire = instr.wires.find( - (w) => "from" in w && !("pipe" in w) && (w as any).to.path[0] === "label", - ) as Extract; - assert.ok(wire.catchFallbackRef, "should have catchFallbackRef"); - assert.deepEqual(wire.catchFallbackRef!.path, []); + const wire = instr.wires.find((w) => !w.pipe && w.to.path[0] === "label")!; + assert.ok(wire.catch && "ref" in wire.catch, "should have catch ref"); + assert.deepEqual(wire.catch.ref.path, []); assert.ok( instr.pipeHandles && instr.pipeHandles.length > 0, "should have pipe forks", @@ -803,20 +813,32 @@ describe("parseBridge: catch source/pipe references", () => { (i): i is Bridge => i.kind === "bridge", )!; const labelWires = instr.wires.filter( - (w) => "from" in w && !("pipe" in w) && (w as any).to.path[0] === "label", - ) as Extract[]; + (w) => !w.pipe && w.to.path[0] === "label", + ); assert.equal(labelWires.length, 1); - assert.ok(labelWires[0].fallbacks, "should have fallbacks"); - assert.equal(labelWires[0].fallbacks!.length, 2); - assert.equal(labelWires[0].fallbacks![0].type, "falsy"); - assert.ok(labelWires[0].fallbacks![0].ref); - assert.equal(labelWires[0].fallbacks![1].type, "falsy"); - assert.equal(labelWires[0].fallbacks![1].value, '"default"'); assert.ok( - labelWires[0].catchFallbackRef, - "wire should have catchFallbackRef", + labelWires[0].sources.length >= 3, + "should have fallback sources", + ); + assert.equal(labelWires[0].sources[1]!.gate, "falsy"); + assert.equal(labelWires[0].sources[1]!.expr.type, "ref"); + assert.equal(labelWires[0].sources[2]!.gate, "falsy"); + assert.equal( + labelWires[0].sources[2]!.expr.type === "literal" + ? labelWires[0].sources[2]!.expr.value + : undefined, + '"default"', + ); + assert.ok( + labelWires[0].catch && "ref" in labelWires[0].catch, + "wire should have catch ref", + ); + assert.equal( + labelWires[0].catch && "value" in labelWires[0].catch + ? labelWires[0].catch.value + : undefined, + undefined, ); - assert.equal(labelWires[0].catchFallback, undefined); }); }); diff --git a/packages/bridge-parser/test/source-locations.test.ts b/packages/bridge-parser/test/source-locations.test.ts index a773b321..d10e6dd0 100644 --- a/packages/bridge-parser/test/source-locations.test.ts +++ b/packages/bridge-parser/test/source-locations.test.ts @@ -57,14 +57,20 @@ describe("parser source locations", () => { } `); - const ternaryWire = instr.wires.find((wire) => "cond" in wire); + const ternaryWire = instr.wires.find( + (wire) => wire.sources[0]?.expr.type === "ternary", + ); assertLoc(ternaryWire, 5, 3); - assert.equal(ternaryWire?.condLoc?.startLine, 5); - assert.equal(ternaryWire?.condLoc?.startColumn, 13); - assert.equal(ternaryWire?.thenLoc?.startLine, 5); - assert.equal(ternaryWire?.thenLoc?.startColumn, 22); - assert.equal(ternaryWire?.elseLoc?.startLine, 5); - assert.equal(ternaryWire?.elseLoc?.startColumn, 36); + const ternaryExpr = ternaryWire!.sources[0]!.expr; + assert.equal(ternaryExpr.type, "ternary"); + if (ternaryExpr.type === "ternary") { + assert.equal(ternaryExpr.condLoc?.startLine, 5); + assert.equal(ternaryExpr.condLoc?.startColumn, 13); + assert.equal(ternaryExpr.thenLoc?.startLine, 5); + assert.equal(ternaryExpr.thenLoc?.startColumn, 22); + assert.equal(ternaryExpr.elseLoc?.startLine, 5); + assert.equal(ternaryExpr.elseLoc?.startColumn, 36); + } }); it("desugared template wires inherit the originating source loc", () => { @@ -94,25 +100,28 @@ describe("parser source locations", () => { } `); - const aliasWire = instr.wires.find( - (wire) => "to" in wire && wire.to.field === "clean", - ); - assert.ok(aliasWire && "catchLoc" in aliasWire); - assert.equal(aliasWire.catchLoc?.startLine, 5); - assert.equal(aliasWire.catchLoc?.startColumn, 35); + const aliasWire = instr.wires.find((wire) => wire.to.field === "clean"); + assert.ok(aliasWire?.catch); + assert.equal(aliasWire.catch.loc?.startLine, 5); + assert.equal(aliasWire.catch.loc?.startColumn, 35); const messageWire = instr.wires.find( - (wire) => "to" in wire && wire.to.path.join(".") === "message", + (wire) => wire.to.path.join(".") === "message", ); - assert.ok( - messageWire && "from" in messageWire && "fallbacks" in messageWire, + assert.ok(messageWire && messageWire.sources.length >= 2); + const msgExpr0 = messageWire.sources[0]!.expr; + assert.equal( + msgExpr0.type === "ref" ? msgExpr0.refLoc?.startLine : undefined, + 6, ); - assert.equal(messageWire.fromLoc?.startLine, 6); - assert.equal(messageWire.fromLoc?.startColumn, 16); - assert.equal(messageWire.fallbacks?.[0]?.loc?.startLine, 6); - assert.equal(messageWire.fallbacks?.[0]?.loc?.startColumn, 40); - assert.equal(messageWire.catchLoc?.startLine, 6); - assert.equal(messageWire.catchLoc?.startColumn, 66); + assert.equal( + msgExpr0.type === "ref" ? msgExpr0.refLoc?.startColumn : undefined, + 16, + ); + assert.equal(messageWire.sources[1]!.loc?.startLine, 6); + assert.equal(messageWire.sources[1]!.loc?.startColumn, 40); + assert.equal(messageWire.catch?.loc?.startLine, 6); + assert.equal(messageWire.catch?.loc?.startColumn, 66); }); it("element scope wires in nested blocks carry source locations", () => { @@ -135,39 +144,41 @@ describe("parser source locations", () => { `); const destinationIdWire = instr.wires.find( - (wire) => - "to" in wire && - wire.to.path.join(".") === "legs.destination.station.id", + (wire) => wire.to.path.join(".") === "legs.destination.station.id", ); assertLoc(destinationIdWire, 8, 9); - assert.ok(destinationIdWire && "from" in destinationIdWire); - assert.equal(destinationIdWire.fromLoc?.startLine, 8); - assert.equal(destinationIdWire.fromLoc?.startColumn, 16); + assert.ok(destinationIdWire); + const idExpr = destinationIdWire.sources[0]!.expr; + assert.equal( + idExpr.type === "ref" ? idExpr.refLoc?.startLine : undefined, + 8, + ); + assert.equal( + idExpr.type === "ref" ? idExpr.refLoc?.startColumn : undefined, + 16, + ); const destinationPlannedTimeWire = instr.wires.find( - (wire) => - "to" in wire && - wire.to.path.join(".") === "legs.destination.plannedTime", + (wire) => wire.to.path.join(".") === "legs.destination.plannedTime", ); assertLoc(destinationPlannedTimeWire, 11, 7); - assert.ok( - destinationPlannedTimeWire && "from" in destinationPlannedTimeWire, + assert.ok(destinationPlannedTimeWire); + const ptExpr = destinationPlannedTimeWire.sources[0]!.expr; + assert.equal( + ptExpr.type === "ref" ? ptExpr.refLoc?.startLine : undefined, + 11, + ); + assert.equal( + ptExpr.type === "ref" ? ptExpr.refLoc?.startColumn : undefined, + 23, ); - assert.equal(destinationPlannedTimeWire.fromLoc?.startLine, 11); - assert.equal(destinationPlannedTimeWire.fromLoc?.startColumn, 23); const destinationDelayWire = instr.wires.find( - (wire) => - "to" in wire && - wire.to.path.join(".") === "legs.destination.delayMinutes", - ); - assert.ok( - destinationDelayWire && - "from" in destinationDelayWire && - "fallbacks" in destinationDelayWire, + (wire) => wire.to.path.join(".") === "legs.destination.delayMinutes", ); + assert.ok(destinationDelayWire && destinationDelayWire.sources.length >= 2); assertLoc(destinationDelayWire, 12, 7); - assert.equal(destinationDelayWire.fallbacks?.[0]?.loc?.startLine, 12); - assert.equal(destinationDelayWire.fallbacks?.[0]?.loc?.startColumn, 43); + assert.equal(destinationDelayWire.sources[1]!.loc?.startLine, 12); + assert.equal(destinationDelayWire.sources[1]!.loc?.startColumn, 43); }); }); diff --git a/packages/bridge-parser/test/ternary-parser.test.ts b/packages/bridge-parser/test/ternary-parser.test.ts index 644d989d..6a163bef 100644 --- a/packages/bridge-parser/test/ternary-parser.test.ts +++ b/packages/bridge-parser/test/ternary-parser.test.ts @@ -17,13 +17,20 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); assert.ok(condWire, "should have a conditional wire"); - assert.ok("cond" in condWire); - assert.ok(condWire.thenRef, "thenRef should be a NodeRef"); - assert.ok(condWire.elseRef, "elseRef should be a NodeRef"); - assert.deepEqual(condWire.thenRef!.path, ["proPrice"]); - assert.deepEqual(condWire.elseRef!.path, ["basicPrice"]); + const expr = condWire.sources[0].expr; + assert.equal(expr.type, "ternary"); + assert.equal(expr.then.type, "ref"); + assert.equal(expr.else.type, "ref"); + assert.deepEqual(expr.then.type === "ref" ? expr.then.ref.path : [], [ + "proPrice", + ]); + assert.deepEqual(expr.else.type === "ref" ? expr.else.ref.path : [], [ + "basicPrice", + ]); }); test("string literal branches produce thenValue / elseValue", () => { @@ -37,10 +44,19 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); - assert.equal(condWire.thenValue, '"premium"'); - assert.equal(condWire.elseValue, '"basic"'); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + const expr = condWire.sources[0].expr; + assert.equal( + expr.then.type === "literal" ? expr.then.value : undefined, + '"premium"', + ); + assert.equal( + expr.else.type === "literal" ? expr.else.value : undefined, + '"basic"', + ); }); test("numeric literal branches produce thenValue / elseValue", () => { @@ -54,10 +70,19 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); - assert.equal(condWire.thenValue, "20"); - assert.equal(condWire.elseValue, "0"); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + const expr = condWire.sources[0].expr; + assert.equal( + expr.then.type === "literal" ? expr.then.value : undefined, + "20", + ); + assert.equal( + expr.else.type === "literal" ? expr.else.value : undefined, + "0", + ); }); test("boolean literal branches", () => { @@ -71,10 +96,19 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); - assert.equal(condWire.thenValue, "true"); - assert.equal(condWire.elseValue, "false"); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + const expr = condWire.sources[0].expr; + assert.equal( + expr.then.type === "literal" ? expr.then.value : undefined, + "true", + ); + assert.equal( + expr.else.type === "literal" ? expr.else.value : undefined, + "false", + ); }); test("null literal branch", () => { @@ -88,10 +122,16 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); - assert.ok(condWire.thenRef, "thenRef should be NodeRef"); - assert.equal(condWire.elseValue, "null"); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + const expr = condWire.sources[0].expr; + assert.equal(expr.then.type, "ref"); + assert.equal( + expr.else.type === "literal" ? expr.else.value : undefined, + "null", + ); }); test("condition with expression chain: i.age >= 18 ? a : b", () => { @@ -105,10 +145,15 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + const expr = condWire.sources[0].expr; assert.ok( - condWire.cond.instance != null && condWire.cond.instance >= 100000, + expr.cond.type === "ref" && + expr.cond.ref.instance != null && + expr.cond.ref.instance >= 100000, "cond should be an expression fork result", ); const exprHandle = instr.pipeHandles!.find((ph) => @@ -129,11 +174,18 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); - assert.equal(condWire.fallbacks?.length, 1); - assert.equal(condWire.fallbacks![0]!.type, "falsy"); - assert.equal(condWire.fallbacks![0]!.value, "0"); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + assert.equal(condWire.sources.length, 2); + assert.equal(condWire.sources[1].gate, "falsy"); + assert.equal( + condWire.sources[1].expr.type === "literal" + ? condWire.sources[1].expr.value + : undefined, + "0", + ); }); test("catch literal fallback stored on conditional wire", () => { @@ -147,8 +199,11 @@ describe("ternary: parser", () => { } `); const instr = doc.instructions.find((inst) => inst.kind === "bridge")!; - const condWire = instr.wires.find((w) => "cond" in w); - assert.ok(condWire && "cond" in condWire); - assert.equal(condWire.catchFallback, "-1"); + const condWire = instr.wires.find( + (w) => w.sources[0]?.expr.type === "ternary", + ); + assert.ok(condWire && condWire.sources[0].expr.type === "ternary"); + assert.ok(condWire.catch && "value" in condWire.catch); + assert.equal(condWire.catch.value, "-1"); }); }); diff --git a/packages/bridge-parser/test/tool-self-wires.test.ts b/packages/bridge-parser/test/tool-self-wires.test.ts index b2d8ea49..3c0e2457 100644 --- a/packages/bridge-parser/test/tool-self-wires.test.ts +++ b/packages/bridge-parser/test/tool-self-wires.test.ts @@ -65,8 +65,8 @@ describe("tool self-wires: constant (=)", () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - value: "https://example.com", to: toolRef("api", ["baseUrl"]), + sources: [{ expr: { type: "literal", value: "https://example.com" } }], }); }); @@ -78,8 +78,8 @@ describe("tool self-wires: constant (=)", () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - value: "GET", to: toolRef("api", ["method"]), + sources: [{ expr: { type: "literal", value: "GET" } }], }); }); @@ -91,8 +91,8 @@ describe("tool self-wires: constant (=)", () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - value: "application/json", to: toolRef("api", ["headers", "Content-Type"]), + sources: [{ expr: { type: "literal", value: "application/json" } }], }); }); }); @@ -107,8 +107,8 @@ describe("tool self-wires: simple pull (<-)", () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - from: contextRef(["auth", "token"]), to: toolRef("api", ["headers", "Authorization"]), + sources: [{ expr: { type: "ref", ref: contextRef(["auth", "token"]) } }], }); }); @@ -122,8 +122,8 @@ describe("tool self-wires: simple pull (<-)", () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - from: constRef(["timeout"]), to: toolRef("api", ["timeout"]), + sources: [{ expr: { type: "ref", ref: constRef(["timeout"]) } }], }); }); @@ -139,8 +139,15 @@ describe("tool self-wires: simple pull (<-)", () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - from: { ...toolRef("authService", ["access_token"]), instance: 1 }, to: toolRef("api", ["headers", "Authorization"]), + sources: [ + { + expr: { + type: "ref", + ref: { ...toolRef("authService", ["access_token"]), instance: 1 }, + }, + }, + ], }); }); }); @@ -154,8 +161,8 @@ describe('tool self-wires: plain string (<- "...")', () => { } `); assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - value: "json", to: toolRef("api", ["format"]), + sources: [{ expr: { type: "literal", value: "json" } }], }); }); }); @@ -171,17 +178,20 @@ describe('tool self-wires: string interpolation (<- "...{ref}...")', () => { } `); // Should produce a concat fork + pipeHandle, similar to bridge blocks - const pathWire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "path", - )!; + const pathWire = tool.wires.find((w) => w.to.path[0] === "path")!; assert.ok(pathWire, "Expected a wire targeting .path"); - assert.ok("from" in pathWire, "Expected a pull wire, not constant"); + assert.equal( + pathWire.sources[0]!.expr.type, + "ref", + "Expected a pull wire, not constant", + ); // The from ref should be the concat fork output - assert.equal((pathWire as any).from.field, "concat"); - assert.ok( - (pathWire as any).pipe, - "Expected pipe flag on interpolation wire", + const pathExpr = pathWire.sources[0]!.expr; + assert.equal( + pathExpr.type === "ref" ? pathExpr.ref.field : undefined, + "concat", ); + assert.ok(pathWire.pipe, "Expected pipe flag on interpolation wire"); }); test("string interpolation with context ref", () => { @@ -192,12 +202,18 @@ describe('tool self-wires: string interpolation (<- "...{ref}...")', () => { .path <- "/users/{context.userId}/profile" } `); - const pathWire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "path", - )!; + const pathWire = tool.wires.find((w) => w.to.path[0] === "path")!; assert.ok(pathWire, "Expected a wire targeting .path"); - assert.ok("from" in pathWire, "Expected a pull wire, not constant"); - assert.equal((pathWire as any).from.field, "concat"); + assert.equal( + pathWire.sources[0]!.expr.type, + "ref", + "Expected a pull wire, not constant", + ); + const ctxPathExpr = pathWire.sources[0]!.expr; + assert.equal( + ctxPathExpr.type === "ref" ? ctxPathExpr.ref.field : undefined, + "concat", + ); }); test("self-reference in interpolation is circular dependency error", () => { @@ -230,13 +246,15 @@ describe("tool self-wires: expression chain (<- ref + expr)", () => { .limit <- const.one + 1 } `); - const limitWire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "limit", - )!; + const limitWire = tool.wires.find((w) => w.to.path[0] === "limit")!; assert.ok(limitWire, "Expected a wire targeting .limit"); - assert.ok("from" in limitWire, "Expected a pull wire"); + assert.equal( + limitWire.sources[0]!.expr.type, + "ref", + "Expected a pull wire", + ); // Expression chains produce a pipe fork (desugared to internal.add/compare/etc.) - assert.ok((limitWire as any).pipe, "Expected pipe flag on expression wire"); + assert.ok(limitWire.pipe, "Expected pipe flag on expression wire"); }); test("expression with > operator", () => { @@ -248,12 +266,10 @@ describe("tool self-wires: expression chain (<- ref + expr)", () => { .verbose <- const.threshold > 5 } `); - const wire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "verbose", - )!; + const wire = tool.wires.find((w) => w.to.path[0] === "verbose")!; assert.ok(wire, "Expected a wire targeting .verbose"); - assert.ok("from" in wire, "Expected a pull wire"); - assert.ok((wire as any).pipe, "Expected pipe flag on expression wire"); + assert.equal(wire.sources[0]!.expr.type, "ref", "Expected a pull wire"); + assert.ok(wire.pipe, "Expected pipe flag on expression wire"); }); }); @@ -267,14 +283,25 @@ describe("tool self-wires: ternary (<- cond ? then : else)", () => { .method <- const.flag ? "POST" : "GET" } `); - const wire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "method", - )!; + const wire = tool.wires.find((w) => w.to.path[0] === "method")!; assert.ok(wire, "Expected a wire targeting .method"); - // Ternary wires have a `cond` field - assert.ok("cond" in wire, "Expected a ternary wire with cond field"); - assert.equal((wire as any).thenValue, '"POST"'); - assert.equal((wire as any).elseValue, '"GET"'); + // Ternary wires have sources[0].expr.type === "ternary" + assert.equal( + wire.sources[0]!.expr.type, + "ternary", + "Expected a ternary wire", + ); + const expr = wire.sources[0]!.expr; + if (expr.type === "ternary") { + assert.equal( + expr.then.type === "literal" ? expr.then.value : undefined, + '"POST"', + ); + assert.equal( + expr.else.type === "literal" ? expr.else.value : undefined, + '"GET"', + ); + } }); test("ternary with ref branches", () => { @@ -288,13 +315,18 @@ describe("tool self-wires: ternary (<- cond ? then : else)", () => { .baseUrl <- const.flag ? const.urlA : const.urlB } `); - const wire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "baseUrl", - )!; + const wire = tool.wires.find((w) => w.to.path[0] === "baseUrl")!; assert.ok(wire, "Expected a wire targeting .baseUrl"); - assert.ok("cond" in wire, "Expected a ternary wire with cond field"); - assert.ok("thenRef" in wire, "Expected thenRef for ref branch"); - assert.ok("elseRef" in wire, "Expected elseRef for ref branch"); + assert.equal( + wire.sources[0]!.expr.type, + "ternary", + "Expected a ternary wire", + ); + const expr = wire.sources[0]!.expr; + if (expr.type === "ternary") { + assert.equal(expr.then.type, "ref", "Expected thenRef for ref branch"); + assert.equal(expr.else.type, "ref", "Expected elseRef for ref branch"); + } }); }); @@ -307,15 +339,17 @@ describe("tool self-wires: coalesce (<- ref ?? fallback)", () => { .timeout <- context.settings.timeout ?? "5000" } `); - const wire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "timeout", - )!; + const wire = tool.wires.find((w) => w.to.path[0] === "timeout")!; assert.ok(wire, "Expected a wire targeting .timeout"); - assert.ok("from" in wire, "Expected a pull wire"); - assert.ok("fallbacks" in wire, "Expected fallbacks for coalesce"); - assert.equal((wire as any).fallbacks.length, 1); - assert.equal((wire as any).fallbacks[0].type, "nullish"); - assert.equal((wire as any).fallbacks[0].value, '"5000"'); + assert.equal(wire.sources[0]!.expr.type, "ref", "Expected a pull wire"); + assert.ok(wire.sources.length >= 2, "Expected fallbacks for coalesce"); + assert.equal(wire.sources[1]!.gate, "nullish"); + assert.equal( + wire.sources[1]!.expr.type === "literal" + ? wire.sources[1]!.expr.value + : undefined, + '"5000"', + ); }); test("falsy coalesce with literal fallback", () => { @@ -326,12 +360,10 @@ describe("tool self-wires: coalesce (<- ref ?? fallback)", () => { .format <- context.settings.format || "json" } `); - const wire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "format", - )!; + const wire = tool.wires.find((w) => w.to.path[0] === "format")!; assert.ok(wire, "Expected a wire targeting .format"); - assert.ok("fallbacks" in wire, "Expected fallbacks for coalesce"); - assert.equal((wire as any).fallbacks[0].type, "falsy"); + assert.ok(wire.sources.length >= 2, "Expected fallbacks for coalesce"); + assert.equal(wire.sources[1]!.gate, "falsy"); }); }); @@ -344,10 +376,14 @@ describe("tool self-wires: catch fallback", () => { .path <- context.settings.path catch "/default" } `); - const wire = tool.wires.find((w) => "to" in w && w.to.path[0] === "path")!; + const wire = tool.wires.find((w) => w.to.path[0] === "path")!; assert.ok(wire, "Expected a wire targeting .path"); - assert.ok("from" in wire, "Expected a pull wire"); - assert.equal((wire as any).catchFallback, '"/default"'); + assert.equal(wire.sources[0]!.expr.type, "ref", "Expected a pull wire"); + assert.ok(wire.catch, "Expected catch handler"); + assert.equal( + "value" in wire.catch ? wire.catch.value : undefined, + '"/default"', + ); }); }); @@ -361,13 +397,11 @@ describe("tool self-wires: not prefix", () => { .silent <- not const.debug } `); - const wire = tool.wires.find( - (w) => "to" in w && w.to.path[0] === "silent", - )!; + const wire = tool.wires.find((w) => w.to.path[0] === "silent")!; assert.ok(wire, "Expected a wire targeting .silent"); - assert.ok("from" in wire, "Expected a pull wire"); + assert.equal(wire.sources[0]!.expr.type, "ref", "Expected a pull wire"); // `not` produces a pipe fork through the negation tool - assert.ok((wire as any).pipe, "Expected pipe flag on not wire"); + assert.ok(wire.pipe, "Expected pipe flag on not wire"); }); }); @@ -390,7 +424,11 @@ describe("tool self-wires: integration", () => { assert.ok( tool.wires.length >= 4, `Expected at least 4 wires, got ${tool.wires.length}: ${JSON.stringify( - tool.wires.map((w) => ("value" in w ? w.value : "pull")), + tool.wires.map((w) => + w.sources[0]?.expr.type === "literal" + ? w.sources[0].expr.value + : "pull", + ), null, 2, )}`, @@ -398,27 +436,35 @@ describe("tool self-wires: integration", () => { // First 3 are constants assertDeepStrictEqualIgnoringLoc(tool.wires[0], { - value: "https://nominatim.openstreetmap.org", to: toolRef("geo", ["baseUrl"]), + sources: [ + { + expr: { + type: "literal", + value: "https://nominatim.openstreetmap.org", + }, + }, + ], }); assertDeepStrictEqualIgnoringLoc(tool.wires[1], { - value: "/search", to: toolRef("geo", ["path"]), + sources: [{ expr: { type: "literal", value: "/search" } }], }); assertDeepStrictEqualIgnoringLoc(tool.wires[2], { - value: "json", to: toolRef("geo", ["format"]), + sources: [{ expr: { type: "literal", value: "json" } }], }); // Expression wire targets .limit (with internal fork wires before it) const limitWire = tool.wires.find( - (w) => - "to" in w && - (w as any).to.field === "geo" && - (w as any).to.path?.[0] === "limit", + (w) => w.to.field === "geo" && w.to.path?.[0] === "limit", ); assert.ok(limitWire, "Expected a wire targeting geo.limit"); - assert.ok("from" in limitWire!, "Expected limit wire to be a pull wire"); - assert.ok((limitWire as any).pipe, "Expected pipe flag on expression wire"); + assert.equal( + limitWire.sources[0]!.expr.type, + "ref", + "Expected limit wire to be a pull wire", + ); + assert.ok(limitWire.pipe, "Expected pipe flag on expression wire"); }); }); diff --git a/packages/bridge/test/control-flow.test.ts b/packages/bridge/test/control-flow.test.ts index e6a9c08e..b7507545 100644 --- a/packages/bridge/test/control-flow.test.ts +++ b/packages/bridge/test/control-flow.test.ts @@ -82,6 +82,16 @@ regressionTest("throw control flow", { }, assertTraces: 1, }, + "tool throws but name present → only catch fires": { + input: { name: "ok", a: { _error: "network error" } }, + assertError: /api call failed/, + assertTraces: 1, + assertGraphql: { + falsyThrow: "ok", + nullishThrow: "ok", + catchThrow: /api call failed/i, + }, + }, }, }, }); diff --git a/packages/bridge/test/utils/regression.ts b/packages/bridge/test/utils/regression.ts index 427cf2bb..9351bd04 100644 --- a/packages/bridge/test/utils/regression.ts +++ b/packages/bridge/test/utils/regression.ts @@ -320,14 +320,18 @@ function replaceNamedTypeNode(typeNode: TypeNode, newName: string): TypeNode { case "NonNullType": return { ...typeNode, - type: replaceNamedTypeNode(typeNode.type, newName) as - typeof typeNode.type, + type: replaceNamedTypeNode( + typeNode.type, + newName, + ) as typeof typeNode.type, }; case "ListType": return { ...typeNode, - type: replaceNamedTypeNode(typeNode.type, newName) as - typeof typeNode.type, + type: replaceNamedTypeNode( + typeNode.type, + newName, + ) as typeof typeNode.type, }; default: return typeNode; @@ -435,10 +439,7 @@ function replaceFieldTypesWithJSONObject( }); let result = printGraphQL(modifiedAst); - if ( - fieldsToReplace.size > 0 && - !result.includes("scalar JSONObject") - ) { + if (fieldsToReplace.size > 0 && !result.includes("scalar JSONObject")) { result = `scalar JSONObject\n\n${result}`; } return result; @@ -1185,7 +1186,10 @@ export function regressionTest(name: string, data: RegressionTest) { }; const startMs = performance.now(); - const assertCtx: AssertContext = { engine: engineName, startMs }; + const assertCtx: AssertContext = { + engine: engineName, + startMs, + }; try { const { @@ -1213,8 +1217,16 @@ export function regressionTest(name: string, data: RegressionTest) { ); } - assertDataExpectation(scenario.assertData, resultData, assertCtx); - assertTraceExpectation(scenario.assertTraces, traces, assertCtx); + assertDataExpectation( + scenario.assertData, + resultData, + assertCtx, + ); + assertTraceExpectation( + scenario.assertTraces, + traces, + assertCtx, + ); } catch (e: any) { if (engineName === "runtime" && scenario.assertError) { observedRuntimeSamples.push({ @@ -1423,12 +1435,16 @@ export function regressionTest(name: string, data: RegressionTest) { querySchema = buildGraphQLSchema(modifiedSDL); } - const transformedSchema = bridgeTransform(querySchema, document, { - tools, - signalMapper: (ctx) => ctx.__bridgeSignal, - toolTimeoutMs: data.toolTimeoutMs ?? 5_000, - trace: "full", - }); + const transformedSchema = bridgeTransform( + querySchema, + document, + { + tools, + signalMapper: (ctx) => ctx.__bridgeSignal, + toolTimeoutMs: data.toolTimeoutMs ?? 5_000, + trace: "full", + }, + ); const source = buildGraphQLOperationSource( querySchema, operation, @@ -1484,7 +1500,11 @@ export function regressionTest(name: string, data: RegressionTest) { graphQLData, graphQLErrors, ); - assertTraceExpectation(scenario.assertTraces, graphqlTraces, assertCtx); + assertTraceExpectation( + scenario.assertTraces, + graphqlTraces, + assertCtx, + ); return; } @@ -1493,7 +1513,11 @@ export function regressionTest(name: string, data: RegressionTest) { (graphQLErrors?.length ?? 0) > 0, `GraphQL replay expected errors for ${operation}.${scenarioName}`, ); - assertTraceExpectation(scenario.assertTraces, graphqlTraces, assertCtx); + assertTraceExpectation( + scenario.assertTraces, + graphqlTraces, + assertCtx, + ); return; } @@ -1503,15 +1527,23 @@ export function regressionTest(name: string, data: RegressionTest) { `GraphQL execution failed for ${operation}.${scenarioName}: ${JSON.stringify(result.errors)}`, ); - assertDataExpectation(scenario.assertData, graphQLData, assertCtx); - assertTraceExpectation(scenario.assertTraces, graphqlTraces, assertCtx); + assertDataExpectation( + scenario.assertData, + graphQLData, + assertCtx, + ); + assertTraceExpectation( + scenario.assertTraces, + graphqlTraces, + assertCtx, + ); }); } }); } // After all scenarios for this operation, verify traversal coverage - test("traversal coverage", (t) => { + test("traversal coverage", async (t) => { const allRuntimeDisabled = scenarioNames.every((name) => scenarios[name]!.disable?.includes("runtime"), ); @@ -1520,6 +1552,9 @@ export function regressionTest(name: string, data: RegressionTest) { return; } + // Wait for all runtime scenario tests to finish populating traceMasks + await runtimeCollectionDone; + const [type, field] = operation.split(".") as [string, string]; const bridge = document.instructions.find( (i): i is Bridge => diff --git a/packages/playground/package.json b/packages/playground/package.json index b691b2ea..a01c2d9c 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -34,7 +34,7 @@ "lucide-react": "0.577.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-resizable-panels": "4.7.2", + "react-resizable-panels": "4.7.3", "tailwind-merge": "3.5.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e4680bd..cd7dcf50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,8 @@ overrides: minimatch@<10.2.1: '>=10.2.1' minimatch@>=10.0.0 <10.2.3: '>=10.2.3' undici@>=7.0.0 <7.18.2: '>=7.18.2' + undici@>=7.0.0 <7.24.0: '>=7.24.0' + undici@>=7.17.0 <7.24.0: '>=7.24.0' wrangler@>=4.0.0 <4.59.1: '>=4.59.1' importers: @@ -233,8 +235,8 @@ importers: specifier: workspace:* version: link:../bridge-stdlib chevrotain: - specifier: ^11.2.0 - version: 11.2.0 + specifier: ^12.0.0 + version: 12.0.0 devDependencies: '@types/node': specifier: ^25.5.0 @@ -425,8 +427,8 @@ importers: specifier: ^19.2.4 version: 19.2.4(react@19.2.4) react-resizable-panels: - specifier: 4.7.2 - version: 4.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 4.7.3 + version: 4.7.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) tailwind-merge: specifier: 3.5.0 version: 3.5.0 @@ -776,20 +778,20 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@chevrotain/cst-dts-gen@11.2.0': - resolution: {integrity: sha512-ssJFvn/UXhQQeICw3SR/fZPmYVj+JM2mP+Lx7bZ51cOeHaMWOKp3AUMuyM3QR82aFFXTfcAp67P5GpPjGmbZWQ==} + '@chevrotain/cst-dts-gen@12.0.0': + resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} - '@chevrotain/gast@11.2.0': - resolution: {integrity: sha512-c+KoD6eSI1xjAZZoNUW+V0l13UEn+a4ShmUrjIKs1BeEWCji0Kwhmqn5FSx1K4BhWL7IQKlV7wLR4r8lLArORQ==} + '@chevrotain/gast@12.0.0': + resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - '@chevrotain/regexp-to-ast@11.2.0': - resolution: {integrity: sha512-lG73pBFqbXODTbXhdZwv0oyUaI+3Irm+uOv5/W79lI3g5hasYaJnVJOm3H2NkhA0Ef4XLBU4Scr7TJDJwgFkAw==} + '@chevrotain/regexp-to-ast@12.0.0': + resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - '@chevrotain/types@11.2.0': - resolution: {integrity: sha512-vBMSj/lz/LqolbGQEHB0tlpW5BnljHVtp+kzjQfQU+5BtGMTuZCPVgaAjtKvQYXnHb/8i/02Kii00y0tsuwfsw==} + '@chevrotain/types@12.0.0': + resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - '@chevrotain/utils@11.2.0': - resolution: {integrity: sha512-+7whECg4yNWHottjvr2To2BRxL4XJVjIyyv5J4+bJ0iMOVU8j/8n1qPDLZS/90W/BObDR8VNL46lFbzY/Hosmw==} + '@chevrotain/utils@12.0.0': + resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} '@cloudflare/kv-asset-handler@0.4.2': resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} @@ -3268,8 +3270,9 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} - chevrotain@11.2.0: - resolution: {integrity: sha512-mHCHTxM51nCklUw9RzRVc0DLjAh/SAUPM4k/zMInlTIo25ldWXOZoPt7XEIk/LwoT4lFVmJcu9g5MHtx371x3A==} + chevrotain@12.0.0: + resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} + engines: {node: '>=22.0.0'} chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} @@ -4241,9 +4244,6 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} - lodash-es@4.17.23: - resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} - lodash.groupby@4.6.0: resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} @@ -4793,8 +4793,8 @@ packages: '@types/react': optional: true - react-resizable-panels@4.7.2: - resolution: {integrity: sha512-1L2vyeBG96hp7N6x6rzYXJ8EjYiDiffMsqj3cd+T9aOKwscvuyCn2CuZ5q3PoUSTIJUM6Q5DgXH1bdDe6uvh2w==} + react-resizable-panels@4.7.3: + resolution: {integrity: sha512-PYcYMLtvJD+Pr0TQNeMvddcnLOwUa/Yb4iNwU7ThNLlHaQYEEC9MIBWHaBGODzYuXIkPRZ/OWe5sbzG1Rzq5ew==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -5205,8 +5205,8 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.18.2: - resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + undici@7.24.2: + resolution: {integrity: sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==} engines: {node: '>=20.18.1'} unenv@2.0.0-rc.24: @@ -6296,22 +6296,20 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@chevrotain/cst-dts-gen@11.2.0': + '@chevrotain/cst-dts-gen@12.0.0': dependencies: - '@chevrotain/gast': 11.2.0 - '@chevrotain/types': 11.2.0 - lodash-es: 4.17.23 + '@chevrotain/gast': 12.0.0 + '@chevrotain/types': 12.0.0 - '@chevrotain/gast@11.2.0': + '@chevrotain/gast@12.0.0': dependencies: - '@chevrotain/types': 11.2.0 - lodash-es: 4.17.23 + '@chevrotain/types': 12.0.0 - '@chevrotain/regexp-to-ast@11.2.0': {} + '@chevrotain/regexp-to-ast@12.0.0': {} - '@chevrotain/types@11.2.0': {} + '@chevrotain/types@12.0.0': {} - '@chevrotain/utils@11.2.0': {} + '@chevrotain/utils@12.0.0': {} '@cloudflare/kv-asset-handler@0.4.2': {} @@ -8505,14 +8503,13 @@ snapshots: chardet@2.1.1: {} - chevrotain@11.2.0: + chevrotain@12.0.0: dependencies: - '@chevrotain/cst-dts-gen': 11.2.0 - '@chevrotain/gast': 11.2.0 - '@chevrotain/regexp-to-ast': 11.2.0 - '@chevrotain/types': 11.2.0 - '@chevrotain/utils': 11.2.0 - lodash-es: 4.17.23 + '@chevrotain/cst-dts-gen': 12.0.0 + '@chevrotain/gast': 12.0.0 + '@chevrotain/regexp-to-ast': 12.0.0 + '@chevrotain/types': 12.0.0 + '@chevrotain/utils': 12.0.0 chokidar@4.0.3: dependencies: @@ -9636,8 +9633,6 @@ snapshots: dependencies: p-locate: 5.0.0 - lodash-es@4.17.23: {} - lodash.groupby@4.6.0: {} lodash.startcase@4.4.0: {} @@ -10150,7 +10145,7 @@ snapshots: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.18.2 + undici: 7.24.2 workerd: 1.20260114.0 ws: 8.18.0 youch: 4.1.0-beta.10 @@ -10163,7 +10158,7 @@ snapshots: dependencies: '@cspotcode/source-map-support': 0.8.1 sharp: 0.34.5 - undici: 7.18.2 + undici: 7.24.2 workerd: 1.20260312.1 ws: 8.18.0 youch: 4.1.0-beta.10 @@ -10437,7 +10432,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-resizable-panels@4.7.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-resizable-panels@4.7.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -10966,7 +10961,7 @@ snapshots: undici-types@7.18.2: {} - undici@7.18.2: {} + undici@7.24.2: {} unenv@2.0.0-rc.24: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f75e45a8..3eb5a1f5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,8 +4,10 @@ packages: overrides: graphql: 16.13.1 - lodash@>=4.0.0 <=4.17.22: ">=4.17.23" - minimatch@<10.2.1: ">=10.2.1" - minimatch@>=10.0.0 <10.2.3: ">=10.2.3" - undici@>=7.0.0 <7.18.2: ">=7.18.2" - wrangler@>=4.0.0 <4.59.1: ">=4.59.1" + lodash@>=4.0.0 <=4.17.22: '>=4.17.23' + minimatch@<10.2.1: '>=10.2.1' + minimatch@>=10.0.0 <10.2.3: '>=10.2.3' + undici@>=7.0.0 <7.18.2: '>=7.18.2' + undici@>=7.0.0 <7.24.0: '>=7.24.0' + undici@>=7.17.0 <7.24.0: '>=7.24.0' + wrangler@>=4.0.0 <4.59.1: '>=4.59.1'