diff --git a/packages/bridge-compiler/package.json b/packages/bridge-compiler/package.json index a4cd6c16..0812fef7 100644 --- a/packages/bridge-compiler/package.json +++ b/packages/bridge-compiler/package.json @@ -12,11 +12,11 @@ "build" ], "scripts": { - "disabled___build": "tsc -p tsconfig.build.json", - "disabled___lint:types": "tsc -p tsconfig.json", - "disabled___test": "node --experimental-transform-types --test test/*.test.ts", - "disabled___fuzz": "node --experimental-transform-types --test test/*.fuzz.ts", - "disabled___prepack": "pnpm build" + "build": "tsc -p tsconfig.build.json", + "lint:types": "tsc -p tsconfig.json", + "test": "node --experimental-transform-types --test test/*.test.ts", + "fuzz": "node --experimental-transform-types --test test/*.fuzz.ts", + "prepack": "pnpm build" }, "dependencies": { "@stackables/bridge-core": "workspace:*", diff --git a/packages/bridge-compiler/src/bridge-asserts.ts b/packages/bridge-compiler/src/bridge-asserts.ts index acff2aa5..a188b911 100644 --- a/packages/bridge-compiler/src/bridge-asserts.ts +++ b/packages/bridge-compiler/src/bridge-asserts.ts @@ -2,12 +2,10 @@ import { SELF_MODULE, type Bridge, type NodeRef, - type Wire, + type Statement, + type Expression, } 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, @@ -54,117 +52,115 @@ function isToolRef(ref: NodeRef, bridge: Bridge): boolean { return true; } -export function assertBridgeCompilerCompatible( - bridge: Bridge, - requestedFields?: string[], -): 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 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 (!isPull(w)) continue; - const fromKey = - 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, - "Alias (shadow) wires are not yet supported by the compiler.", - ); - } - 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 (w.pipe && w.catch) { - throw new BridgeCompilerIncompatibleError( - op, - "Catch fallback on expression (pipe) wires is not yet supported by the compiler.", +function isToolBackedExpr(expr: Expression, bridge: Bridge): boolean { + switch (expr.type) { + case "ref": + return isToolRef(expr.ref, bridge); + case "pipe": + return true; + case "literal": + case "control": + return false; + case "ternary": + return ( + isToolBackedExpr(expr.cond, bridge) || + isToolBackedExpr(expr.then, bridge) || + isToolBackedExpr(expr.else, bridge) ); - } + case "and": + case "or": + return ( + isToolBackedExpr(expr.left, bridge) || + isToolBackedExpr(expr.right, bridge) + ); + case "binary": + return ( + isToolBackedExpr(expr.left, bridge) || + isToolBackedExpr(expr.right, bridge) + ); + case "unary": + return isToolBackedExpr(expr.operand, bridge); + case "concat": + return expr.parts.some((p) => isToolBackedExpr(p, bridge)); + case "array": + return isToolBackedExpr(expr.source, bridge); + } +} + +interface OutputWireInfo { + targetPath: string; + primaryIsToolRef: boolean; +} - // Catch fallback that references a pipe handle — the compiler eagerly - // calls all tools in the catch branch even when the main wire succeeds. - 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)) { - throw new BridgeCompilerIncompatibleError( - op, - "Catch fallback referencing a pipe expression is not yet supported by the compiler.", - ); +function collectOutputWires( + body: Statement[], + bridge: Bridge, +): OutputWireInfo[] { + const selfKey = `${SELF_MODULE}:${bridge.type}:${bridge.field}`; + const results: OutputWireInfo[] = []; + + function walk(stmts: Statement[], pathPrefix: string[]) { + for (const s of stmts) { + if (s.kind === "wire") { + const tk = `${s.target.module}:${s.target.type}:${s.target.field}`; + if (tk === selfKey && s.target.instance == null) { + const fullPath = [...pathPrefix, ...s.target.path]; + const primary = s.sources[0]!; + results.push({ + targetPath: fullPath.join("."), + primaryIsToolRef: + primary.expr.type === "ref" && + isToolRef(primary.expr.ref, bridge), + }); } } + if (s.kind === "scope") { + walk(s.body, [...pathPrefix, ...s.target.path]); + } } + } + walk(body, []); + return results; +} - // 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 (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(wRef(iw), bridge)) { - throw new BridgeCompilerIncompatibleError( - op, - "Catch fallback on wires with tool chain dependencies is not yet supported by the compiler.", - ); +export function assertBridgeCompilerCompatible( + bridge: Bridge, + requestedFields?: string[], +): void { + if (!bridge.body) return; + + const op = `${bridge.type}.${bridge.field}`; + + // Check fallback chains with tool-backed refs + function checkFallbackChains(stmts: Statement[]) { + for (const s of stmts) { + if (s.kind === "wire" || s.kind === "alias" || s.kind === "spread") { + for (const src of s.sources.slice(1)) { + if (isToolBackedExpr(src.expr, bridge)) { + throw new BridgeCompilerIncompatibleError( + op, + "Fallback chains (|| / ??) with tool-backed sources are not yet supported by the compiler.", + ); + } } } - } - - // 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. - 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.", - ); + if (s.kind === "scope") { + checkFallbackChains(s.body); } } } + checkFallbackChains(bridge.body); - // Same-cost overdefinition sourced only from tools can diverge from runtime - // tracing/error behavior in current AOT codegen; compile must downgrade. + // Same-cost overdefinition sourced only from tools + const outputWires = collectOutputWires(bridge.body, bridge); const toolOnlyOverdefs = new Map(); - for (const w of wires) { - if ( - w.to.module !== SELF_MODULE || - w.to.type !== bridge.type || - w.to.field !== bridge.field - ) { - continue; - } - if (!isPull(w) || !isToolRef(wRef(w), bridge)) { - continue; - } - - const outputPath = w.to.path.join("."); - if (!matchesRequestedField(outputPath, requestedFields)) { - continue; - } + for (const w of outputWires) { + if (!w.primaryIsToolRef) continue; + if (!matchesRequestedField(w.targetPath, requestedFields)) continue; toolOnlyOverdefs.set( - outputPath, - (toolOnlyOverdefs.get(outputPath) ?? 0) + 1, + w.targetPath, + (toolOnlyOverdefs.get(w.targetPath) ?? 0) + 1, ); } @@ -176,37 +172,4 @@ export function assertBridgeCompilerCompatible( ); } } - - // Pipe handles with extra bridge wires to the same tool — the compiler - // treats pipe forks as independent tool calls, so bridge wires that set - // fields on the main tool trunk are not merged into the fork's input. - if (bridge.pipeHandles && bridge.pipeHandles.length > 0) { - const pipeHandleKeys = new Set(); - const pipedToolNames = new Set(); - for (const ph of bridge.pipeHandles) { - pipeHandleKeys.add(ph.key); - pipedToolNames.add( - `${ph.baseTrunk.module}:${ph.baseTrunk.type}:${ph.baseTrunk.field}`, - ); - } - - 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 - ? `${w.to.module}:${w.to.type}:${w.to.field}:${w.to.instance}` - : `${w.to.module}:${w.to.type}:${w.to.field}`; - // Skip wires that target the pipe handle itself (fork input) - if (pipeHandleKeys.has(fullKey)) continue; - // Check if this wire targets a tool that also has pipe calls - const toolName = `${w.to.module}:${w.to.type}:${w.to.field}`; - if (pipedToolNames.has(toolName)) { - throw new BridgeCompilerIncompatibleError( - op, - "Bridge wires that set fields on a tool with pipe calls are not yet supported by the compiler.", - ); - } - } - } } diff --git a/packages/bridge-compiler/src/codegen.ts b/packages/bridge-compiler/src/codegen.ts index 5de436c7..60f00f3c 100644 --- a/packages/bridge-compiler/src/codegen.ts +++ b/packages/bridge-compiler/src/codegen.ts @@ -10,27 +10,21 @@ * reviewing; after review the usage here is intentional and safe. * * lgtm [js/code-injection] - * - * Supports: - * - Pull wires (`target <- source`) - * - Constant wires (`target = "value"`) - * - Nullish coalescing (`?? fallback`) - * - Falsy fallback (`|| fallback`) - * - Catch fallback (`catch`) - * - Conditional wires (ternary) - * - Array mapping (`[] as iter { }`) - * - Force statements (`force `, `force catch null`) - * - ToolDef merging (tool blocks with wires and `on error`) */ import type { BridgeDocument, Bridge, - Wire, NodeRef, ToolDef, Expression, ControlFlowInstruction, + Statement, + WireSourceEntry, + WireCatch, + HandleBinding, + JsonValue, + ScopeStatement, } from "@stackables/bridge-core"; import { BridgePanicError } from "@stackables/bridge-core"; import type { SourceLocation } from "@stackables/bridge-types"; @@ -40,102 +34,13 @@ import { } from "./bridge-asserts.ts"; const SELF_MODULE = "_"; +const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]); -// ── 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 as string; -} -/** 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 as string; -} +// ── Helpers ───────────────────────────────────────────────────────────────── -/** 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 as string) : 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 refTrunkKey(ref: NodeRef): string { + if (ref.element) return `${ref.module}:${ref.type}:${ref.field}:*`; + return `${ref.module}:${ref.type}:${ref.field}${ref.instance != null ? `:${ref.instance}` : ""}`; } function matchesRequestedFields( @@ -143,14 +48,10 @@ function matchesRequestedFields( requestedFields: string[] | undefined, ): boolean { if (!requestedFields || requestedFields.length === 0) return true; - for (const pattern of requestedFields) { if (pattern === fieldPath) return true; - if (fieldPath.startsWith(pattern + ".")) return true; - if (pattern.startsWith(fieldPath + ".")) return true; - if (pattern.endsWith(".*")) { const prefix = pattern.slice(0, -2); if (fieldPath.startsWith(prefix + ".")) { @@ -160,40 +61,22 @@ function matchesRequestedFields( if (fieldPath === prefix) return true; } } - return false; } // ── Public API ────────────────────────────────────────────────────────────── export interface CompileOptions { - /** The operation to compile, e.g. "Query.livingStandard" */ operation: string; - /** - * Sparse fieldset filter — only emit code for the listed output fields. - * Supports dot-separated paths and a trailing `*` wildcard. - * Omit or pass an empty array to compile all output fields. - */ requestedFields?: string[]; } export interface CompileResult { - /** Generated JavaScript source code */ code: string; - /** The exported function name */ functionName: string; - /** The function body (without the function signature wrapper) */ functionBody: string; } -/** - * Compile a single bridge operation into a standalone async JavaScript function. - * - * The generated function has the signature: - * `async function _(input, tools, context) → Promise` - * - * It calls tools in topological dependency order and returns the output object. - */ export function compileBridge( document: BridgeDocument, options: CompileOptions, @@ -206,27 +89,20 @@ export function compileBridge( ); const type = operation.substring(0, dotIdx); const field = operation.substring(dotIdx + 1); - const bridge = document.instructions.find( (i): i is Bridge => i.kind === "bridge" && i.type === type && i.field === field, ); if (!bridge) throw new Error(`No bridge definition found for operation: ${operation}`); - assertBridgeCompilerCompatible(bridge, options.requestedFields); - - // Collect const definitions from the document const constDefs = new Map(); for (const inst of document.instructions) { if (inst.kind === "const") constDefs.set(inst.name, inst.value); } - - // Collect tool definitions from the document const toolDefs = document.instructions.filter( (i): i is ToolDef => i.kind === "tool", ); - const ctx = new CodegenContext( bridge, constDefs, @@ -236,183 +112,41 @@ export function compileBridge( return ctx.compile(); } -// ── Helpers ───────────────────────────────────────────────────────────────── - -type DetectedControlFlow = { - kind: "break" | "continue" | "throw" | "panic"; - levels: number; -}; - -/** Check if any wire in a set has a control flow instruction (break/continue/throw/panic). */ -function detectControlFlow(wires: Wire[]): DetectedControlFlow | null { - for (const w of wires) { - 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((cc as any).levels) || 1) - : 1; - return { kind, levels }; - } - } - return null; -} - -function splitToolName(name: string): { module: string; fieldName: string } { - const dotIdx = name.lastIndexOf("."); - if (dotIdx === -1) return { module: SELF_MODULE, fieldName: name }; - return { - module: name.substring(0, dotIdx), - fieldName: name.substring(dotIdx + 1), - }; -} - -/** Build a trunk key from a NodeRef (same logic as bridge-core's trunkKey). */ -function refTrunkKey(ref: NodeRef): string { - if (ref.element) return `${ref.module}:${ref.type}:${ref.field}:*`; - return `${ref.module}:${ref.type}:${ref.field}${ref.instance != null ? `:${ref.instance}` : ""}`; -} - -/** - * Emit a coerced constant value as a JavaScript literal. - * Mirrors the runtime's `coerceConstant` semantics. - */ -function emitCoerced(raw: string): string { - const trimmed = raw.trim(); - if (trimmed === "true") return "true"; - if (trimmed === "false") return "false"; - if (trimmed === "null") return "null"; - // JSON-encoded string literal: '"hello"' → "hello" - if ( - trimmed.length >= 2 && - trimmed.charCodeAt(0) === 0x22 && - trimmed.charCodeAt(trimmed.length - 1) === 0x22 - ) { - return trimmed; // already a valid JS string literal - } - // Numeric literal - const num = Number(trimmed); - if (trimmed !== "" && !isNaN(num) && isFinite(num)) return String(num); - // Fallback: raw string - return JSON.stringify(raw); -} - -/** - * Build a nested JS object literal from entries where each entry is - * [remainingPathSegments, expression]. Groups entries by first path segment - * and recurses for deeper nesting. - */ -function emitNestedObjectLiteral(entries: [string[], string][]): string { - const byKey = new Map(); - for (const [path, expr] of entries) { - const key = path[0]!; - if (!byKey.has(key)) byKey.set(key, []); - byKey.get(key)!.push([path.slice(1), expr]); - } - const parts: string[] = []; - for (const [key, subEntries] of byKey) { - if (subEntries.some(([p]) => p.length === 0)) { - const leaf = subEntries.find(([p]) => p.length === 0)!; - parts.push(`${JSON.stringify(key)}: ${leaf[1]}`); - } else { - parts.push( - `${JSON.stringify(key)}: ${emitNestedObjectLiteral(subEntries)}`, - ); - } - } - return `{ ${parts.join(", ")} }`; -} - -/** - * Parse a const value at compile time and emit it as an inline JS literal. - * Since const values are JSON, we can JSON.parse at compile time and - * re-serialize as a JavaScript expression, avoiding runtime JSON.parse. - */ -function emitParsedConst(raw: string): string { - try { - const parsed = JSON.parse(raw); - return JSON.stringify(parsed); - } catch { - // If JSON.parse fails, fall back to runtime parsing - return `JSON.parse(${JSON.stringify(raw)})`; - } -} - -// ── Code-generation context ───────────────────────────────────────────────── +// ── Internal types ────────────────────────────────────────────────────────── -interface ToolInfo { +interface ToolReg { trunkKey: string; toolName: string; + handleName: string; varName: string; + memoize?: boolean; } -/** Set of internal tool field names that can be inlined by the AOT compiler. */ -const INTERNAL_TOOLS = new Set([ - "concat", - "add", - "subtract", - "multiply", - "divide", - "eq", - "neq", - "gt", - "gte", - "lt", - "lte", - "not", - "and", - "or", -]); +interface ExtractedWire { + target: NodeRef; + sources: WireSourceEntry[]; + catch?: WireCatch; + loc?: SourceLocation; +} + +// ── Codegen context ───────────────────────────────────────────────────────── class CodegenContext { private bridge: Bridge; private constDefs: Map; private toolDefs: ToolDef[]; - private selfTrunkKey: string; - private varMap = new Map(); - private tools = new Map(); - private toolCounter = 0; - /** Set of trunk keys for define-in/out virtual containers. */ - private defineContainers = new Set(); - /** Trunk keys of pipe/expression tools that use internal implementations. */ - private internalToolKeys = new Set(); - /** Trunk keys of tools compiled in catch-guarded mode (have a `_err` variable). */ - private catchGuardedTools = new Set(); - /** Trunk keys of tools whose inputs depend on element wires (must be inlined in map callbacks). */ - private elementScopedTools = new Set(); - /** Trunk keys of tools that are only referenced in ternary branches (can be lazily evaluated). */ - private ternaryOnlyTools = new Set(); - /** Map from element-scoped non-internal tool trunk key to loop-local variable name. - * Populated during array body generation to deduplicate tool calls within one element. */ - private elementLocalVars = new Map(); - /** Current element variable name, set during element wire expression generation. */ - private currentElVar: string | undefined; - /** Stack of active element variables from outermost to innermost array scopes. */ - private elementVarStack: string[] = []; - /** Map from ToolDef dependency tool name to its emitted variable name. - * Populated lazily by emitToolDeps to avoid duplicating calls. */ - private toolDepVars = new Map(); - /** Sparse fieldset filter for output wire pruning. */ private requestedFields: string[] | undefined; - /** Per tool signature cursor used to assign distinct wire instances to repeated handle bindings. */ - private toolInstanceCursors = new Map(); - /** Tool trunk keys declared with `memoize`. */ + private tools = new Map(); + private toolInputWires = new Map(); + private outputWires: ExtractedWire[] = []; + private forces: { handle: string; module: string; type: string; field: string; instance?: number; catchError?: true }[] = []; + private aliases = new Map(); + private spreads: { sources: WireSourceEntry[]; catch?: WireCatch; pathPrefix: string[]; loc?: SourceLocation }[] = []; + private catchGuardedTools = new Set(); private memoizedToolKeys = new Set(); - /** Map from tool function name to its upfront-resolved variable name. */ private toolFnVars = new Map(); private toolFnVarCounter = 0; + private toolCounter = 0; constructor( bridge: Bridge, @@ -423,553 +157,599 @@ class CodegenContext { this.bridge = bridge; this.constDefs = constDefs; this.toolDefs = toolDefs; - this.selfTrunkKey = `${SELF_MODULE}:${bridge.type}:${bridge.field}`; - this.requestedFields = requestedFields?.length - ? requestedFields - : undefined; - - for (const h of bridge.handles) { - switch (h.kind) { - case "input": - case "output": - // Input and output share the self trunk key; distinguished by wire direction - break; - case "context": - this.varMap.set(`${SELF_MODULE}:Context:context`, "context"); - break; - case "const": - // Constants are inlined directly - break; - case "define": { - // Define blocks are inlined at parse time. The parser creates - // __define_in_ and __define_out_ modules that act - // as virtual data containers for routing data in/out of the define. - const inModule = `__define_in_${h.handle}`; - const outModule = `__define_out_${h.handle}`; - const inTk = `${inModule}:${bridge.type}:${bridge.field}`; - const outTk = `${outModule}:${bridge.type}:${bridge.field}`; - const inVn = `_d${++this.toolCounter}`; - const outVn = `_d${++this.toolCounter}`; - this.varMap.set(inTk, inVn); - this.varMap.set(outTk, outVn); - this.defineContainers.add(inTk); - this.defineContainers.add(outTk); + this.requestedFields = requestedFields?.length ? requestedFields : undefined; + } + + compile(): CompileResult { + const { bridge } = this; + const fnName = `${bridge.type}_${bridge.field}`; + + this.indexBody(bridge.body, []); + this.validatePaths(); + + const filteredOutputWires = this.requestedFields + ? this.outputWires.filter((w) => { + if (w.target.path.length === 0) return true; + return matchesRequestedFields(w.target.path.join("."), this.requestedFields); + }) + : this.outputWires; + + const orderedOutputWires = this.reorderOverdefinedWires(filteredOutputWires); + + if (orderedOutputWires.length === 0 && this.spreads.length === 0) { + throw new Error(`Bridge ${bridge.type}.${bridge.field} has no output wires`); + } + + this.detectCatchGuardedTools(orderedOutputWires); + const toolLayers = this.topologicalLayers(); + const toolOrder = this.topologicalSort(); + const conditionalTools = this.analyzeOverdefinitionBypass(orderedOutputWires, toolOrder); + const liveTools = this.findLiveTools(orderedOutputWires); + + const lines: string[] = []; + lines.push(`// AOT-compiled bridge: ${bridge.type}.${bridge.field}`); + lines.push(`// Generated by @stackables/bridge-compiler`); + lines.push(""); + lines.push(`export default async function ${fnName}(input, tools, context, __opts) {`); + + this.emitPreamble(lines); + this.emitToolLookups(lines, liveTools); + this.emitToolCalls(lines, toolLayers, conditionalTools, liveTools); + this.emitForceStatements(lines, liveTools); + this.emitOutput(lines, orderedOutputWires); + + lines.push(`}`); + + const code = lines.join("\n"); + const bodyMatch = code.match( + /export default async function \w+\(input, tools, context, __opts\) \{([\s\S]*)\}\s*$/, + ); + return { + code, + functionName: fnName, + functionBody: bodyMatch ? bodyMatch[1]! : code, + }; + } + + // ── Statement tree indexing ───────────────────────────────────────────── + + private isSelfOutput(target: NodeRef): boolean { + return ( + target.module === SELF_MODULE && + target.type === this.bridge.type && + target.field === this.bridge.field && + !target.element && + target.instance == null + ); + } + + private indexBody(stmts: Statement[], pathPrefix: string[]): void { + for (const stmt of stmts) { + switch (stmt.kind) { + case "with": { + const b = stmt.binding; + if (b.kind === "tool") this.registerTool(b); break; } - case "tool": { - const { module, fieldName } = splitToolName(h.name); - // Module-prefixed tools use the bridge's type; self-module tools use "Tools". - // 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 this.bridge.wires) { - if ( - w.to.module === module && - w.to.field === fieldName && - w.to.instance != null - ) { - refType = w.to.type; - break; - } - if ( - isPull(w) && - wRef(w).module === module && - wRef(w).field === fieldName && - wRef(w).instance != null - ) { - refType = wRef(w).type; - break; - } - } - const instance = this.findNextInstance(module, refType, fieldName); - const tk = `${module}:${refType}:${fieldName}:${instance}`; - const vn = `_t${++this.toolCounter}`; - this.varMap.set(tk, vn); - this.tools.set(tk, { trunkKey: tk, toolName: h.name, varName: vn }); - if (h.memoize) { - this.memoizedToolKeys.add(tk); + case "wire": { + const target = stmt.target; + const isToolInput = target.instance != null && !target.element; + if (isToolInput) { + const tk = refTrunkKey(target); + const arr = this.toolInputWires.get(tk) ?? []; + arr.push({ + target: { ...target, path: [...pathPrefix, ...target.path] }, + sources: stmt.sources, + catch: stmt.catch, + loc: stmt.loc, + }); + this.toolInputWires.set(tk, arr); + } else if (this.isSelfOutput(target)) { + this.outputWires.push({ + target: { ...target, path: [...pathPrefix, ...target.path] }, + sources: stmt.sources, + catch: stmt.catch, + loc: stmt.loc, + }); + } else if (target.module.startsWith("__define_")) { + const tk = refTrunkKey(target); + const arr = this.toolInputWires.get(tk) ?? []; + arr.push({ + target: { ...target, path: [...pathPrefix, ...target.path] }, + sources: stmt.sources, + catch: stmt.catch, + loc: stmt.loc, + }); + this.toolInputWires.set(tk, arr); } break; } + case "alias": + this.aliases.set(stmt.name, { sources: stmt.sources, catch: stmt.catch, loc: stmt.loc }); + break; + case "spread": + this.spreads.push({ sources: stmt.sources, catch: stmt.catch, pathPrefix: [...pathPrefix], loc: stmt.loc }); + break; + case "scope": + this.indexBody(stmt.body, [...pathPrefix, ...stmt.target.path]); + break; + case "force": + this.forces.push(stmt); + break; } } + } - // Register pipe handles (synthetic tool instances for interpolation, - // expressions, and explicit pipe operators) - if (bridge.pipeHandles) { - // Build handle→fullName map for resolving dotted tool names (e.g. "std.str.toUpperCase") - const handleToolNames = new Map(); - for (const h of bridge.handles) { - if (h.kind === "tool") handleToolNames.set(h.handle, h.name); + private registerTool(binding: HandleBinding & { kind: "tool" }): void { + const resolved = this.resolveToolDef(binding.name); + const fnName = resolved.fn ?? binding.name; + + let instance = 1; + for (const stmt of this.flattenStatements(this.bridge.body)) { + if (stmt.kind === "wire" && stmt.target.field === binding.name.split(".").pop() && stmt.target.instance != null) { + instance = stmt.target.instance; + break; + } + if (stmt.kind === "force" && stmt.field === binding.name.split(".").pop() && stmt.instance != null) { + instance = stmt.instance; + break; } + } - for (const ph of bridge.pipeHandles) { - // Use the pipe handle's key directly — it already includes the correct instance - const tk = ph.key; - if (!this.tools.has(tk)) { - const vn = `_t${++this.toolCounter}`; - this.varMap.set(tk, vn); - const field = ph.baseTrunk.field; - // Normalise __and/__or → and/or so they match INTERNAL_TOOLS - const normField = field.startsWith("__") ? field.slice(2) : field; - // Use the full tool name from the handle binding (e.g. "std.str.toUpperCase") - // falling back to just the field name for internal/synthetic handles - const fullToolName = handleToolNames.get(ph.handle) ?? normField; - this.tools.set(tk, { - trunkKey: tk, - toolName: fullToolName, - varName: vn, - }); - if (INTERNAL_TOOLS.has(normField)) { - this.internalToolKeys.add(tk); - } - } + const dotIdx = binding.name.lastIndexOf("."); + let module: string, fieldName: string, refType: string; + if (dotIdx === -1) { + module = SELF_MODULE; + fieldName = binding.name; + refType = "Tools"; + } else { + module = binding.name.substring(0, dotIdx); + fieldName = binding.name.substring(dotIdx + 1); + refType = this.bridge.type; + } + + const tk = `${module}:${refType}:${fieldName}:${instance}`; + if (this.tools.has(tk)) return; + + const vn = `_t${++this.toolCounter}`; + this.tools.set(tk, { + trunkKey: tk, + toolName: fnName, + handleName: binding.name, + varName: vn, + memoize: binding.memoize, + }); + if (binding.memoize) this.memoizedToolKeys.add(tk); + } + + private resolveToolDef(name: string): { fn?: string; chain: ToolDef[] } { + const chain: ToolDef[] = []; + let current = name; + const visited = new Set(); + while (true) { + if (visited.has(current)) break; + visited.add(current); + const td = this.toolDefs.find((t) => t.name === current); + if (!td) break; + chain.push(td); + if (td.fn) return { fn: td.fn, chain }; + if (td.extends) { current = td.extends; } else { break; } + } + return { fn: chain[0]?.fn, chain }; + } + + private *flattenStatements(stmts: Statement[]): Generator { + for (const s of stmts) { + yield s; + if (s.kind === "scope") yield* this.flattenStatements(s.body); + if ((s.kind === "wire" || s.kind === "alias") && s.sources[0]?.expr.type === "array") { + yield* this.flattenStatements(s.sources[0].expr.body); } } + } + + // ── Validation ────────────────────────────────────────────────────────── - // Detect alias declarations — wires targeting __local:Shadow: modules. - // These act as virtual containers (like define modules). - for (const w of this.bridge.wires) { - const toTk = refTrunkKey(w.to); - if ( - w.to.module === "__local" && - w.to.type === "Shadow" && - !this.varMap.has(toTk) - ) { - const vn = `_a${++this.toolCounter}`; - this.varMap.set(toTk, vn); - this.defineContainers.add(toTk); + private validatePaths(): void { + const checkRef = (ref: NodeRef) => { + for (const seg of ref.path) { + if (UNSAFE_KEYS.has(seg)) throw new Error(`Unsafe property traversal: ${seg}`); + } + }; + for (const w of this.outputWires) { + for (const seg of w.target.path) { + if (UNSAFE_KEYS.has(seg)) throw new Error(`Unsafe assignment key: ${seg}`); } - if ( - isPull(w) && - wRef(w).module === "__local" && - wRef(w).type === "Shadow" - ) { - const fromTk = refTrunkKey(wRef(w)); - if (!this.varMap.has(fromTk)) { - const vn = `_a${++this.toolCounter}`; - this.varMap.set(fromTk, vn); - this.defineContainers.add(fromTk); + for (const src of w.sources) this.walkExprRefs(src.expr, checkRef); + } + for (const [, wires] of this.toolInputWires) { + for (const w of wires) { + for (const seg of w.target.path) { + if (UNSAFE_KEYS.has(seg)) throw new Error(`Unsafe assignment key: ${seg}`); } + for (const src of w.sources) this.walkExprRefs(src.expr, checkRef); + } + } + for (const h of this.bridge.handles) { + if (h.kind !== "tool") continue; + for (const seg of h.name.split(".")) { + if (UNSAFE_KEYS.has(seg)) + throw new Error(`No tool found for "${h.name}" — prototype-pollution attempt blocked`); } } } - /** Find the instance number for a tool from the wires. */ - private findNextInstance( - module: string, - type: string, - field: string, - ): number { - const sig = `${module}:${type}:${field}`; - const instances: number[] = []; - for (const w of this.bridge.wires) { - if ( - w.to.module === module && - w.to.type === type && - w.to.field === field && - w.to.instance != null - ) - instances.push(w.to.instance); - if ( - isPull(w) && - wRef(w).module === module && - wRef(w).type === type && - wRef(w).field === field && - wRef(w).instance != null - ) - instances.push(wRef(w).instance!); + private walkExprRefs(expr: Expression, fn: (ref: NodeRef) => void): void { + switch (expr.type) { + case "ref": fn(expr.ref); break; + case "ternary": this.walkExprRefs(expr.cond, fn); this.walkExprRefs(expr.then, fn); this.walkExprRefs(expr.else, fn); break; + case "and": case "or": this.walkExprRefs(expr.left, fn); this.walkExprRefs(expr.right, fn); break; + case "binary": this.walkExprRefs(expr.left, fn); this.walkExprRefs(expr.right, fn); break; + case "unary": this.walkExprRefs(expr.operand, fn); break; + case "concat": for (const p of expr.parts) this.walkExprRefs(p, fn); break; + case "pipe": this.walkExprRefs(expr.source, fn); break; + case "array": this.walkExprRefs(expr.source, fn); break; } - const uniqueInstances = [...new Set(instances)].sort((a, b) => a - b); - const nextIndex = this.toolInstanceCursors.get(sig) ?? 0; - this.toolInstanceCursors.set(sig, nextIndex + 1); - if (uniqueInstances[nextIndex] != null) return uniqueInstances[nextIndex]!; - const lastInstance = uniqueInstances.at(-1) ?? 0; - // Some repeated handle bindings are never referenced in wires (for example, - // an unused shadowed tool alias in a nested loop). In that case we still - // need a distinct synthetic instance number so later bindings don't collide - // with earlier tool registrations. - return lastInstance + (nextIndex - uniqueInstances.length) + 1; } - /** - * Get the variable name for an upfront-resolved tool function. - * Registers the tool if not yet seen. - */ - private toolFnVar(fnName: string): string { - let varName = this.toolFnVars.get(fnName); - if (!varName) { - varName = `__fn${++this.toolFnVarCounter}`; - this.toolFnVars.set(fnName, varName); + // ── Overdefinition reordering ───────────────────────────────────────── + + private reorderOverdefinedWires(wires: ExtractedWire[]): ExtractedWire[] { + const groups = new Map(); + for (const w of wires) { + const key = w.target.path.join("."); + const arr = groups.get(key) ?? []; + arr.push(w); + groups.set(key, arr); } - return varName; + const result: ExtractedWire[] = []; + const seen = new Set(); + for (const w of wires) { + const key = w.target.path.join("."); + if (seen.has(key)) continue; + seen.add(key); + const group = groups.get(key)!; + if (group.length <= 1) { + result.push(...group); + } else { + const sorted = [...group].sort((a, b) => this.wireCost(a) - this.wireCost(b)); + result.push(...sorted); + } + } + return result; } - /** - * Generate a static lookup expression for a dotted tool name. - * For "vendor.sub.api" → `tools?.vendor?.sub?.api ?? tools?.["vendor.sub.api"]` - * For "myTool" → `tools?.["myTool"]` - */ - private toolLookupExpr(fnName: string): string { - if (!fnName.includes(".")) { - return `tools?.[${JSON.stringify(fnName)}]`; + private wireCost(w: ExtractedWire): number { + return w.sources[0]?.expr ? this.exprCost(w.sources[0].expr) : 0; + } + + private exprCost(expr: Expression): number { + switch (expr.type) { + case "literal": case "control": return 0; + case "ref": { + const ref = expr.ref; + if (ref.module === "__local") return 0; + if (ref.module === SELF_MODULE && ref.type === "Const") return 0; + if (ref.module === SELF_MODULE && ref.type === "Context") return 0; + if (ref.instance != null) return 1; + if (ref.module === SELF_MODULE && ref.type === this.bridge.type && ref.field === this.bridge.field) return 0; + return 1; + } + case "pipe": return 1; + case "ternary": return Math.max(this.exprCost(expr.cond), this.exprCost(expr.then), this.exprCost(expr.else)); + default: return 0; } - const parts = fnName.split("."); - const nested = - "tools" + parts.map((p) => `?.[${JSON.stringify(p)}]`).join(""); - const flat = `tools?.[${JSON.stringify(fnName)}]`; - return `${nested} ?? ${flat}`; } - // ── Main compilation entry point ────────────────────────────────────────── + // ── Catch-guard detection ───────────────────────────────────────────── - compile(): CompileResult { - const { bridge } = this; - const fnName = `${bridge.type}_${bridge.field}`; + private detectCatchGuardedTools(outputWires: ExtractedWire[]): void { + for (const w of outputWires) { + const needsCatch = w.catch != null || (w.sources[0]?.expr.type === "ref" && w.sources[0].expr.safe); + if (!needsCatch) continue; + if (w.sources[0]?.expr.type === "ref" && w.sources[0].expr.ref.instance != null) { + this.catchGuardedTools.add(refTrunkKey(w.sources[0].expr.ref)); + } + } + } - // ── Prototype pollution guards ────────────────────────────────────── - // Validate all wire paths and tool names at compile time, matching the - // runtime's setNested / pullSingle / lookupToolFn guards. - const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]); + // ── Topological sort ────────────────────────────────────────────────── - // 1. setNested guard — reject unsafe keys in wire target paths - for (const w of bridge.wires) { - for (const seg of w.to.path) { - if (UNSAFE_KEYS.has(seg)) - throw new Error(`Unsafe assignment key: ${seg}`); + private getToolDeps(tk: string): Set { + const deps = new Set(); + const wires = this.toolInputWires.get(tk) ?? []; + for (const w of wires) { + for (const src of w.sources) this.collectToolRefs(src.expr, deps); + } + const tool = this.tools.get(tk); + if (tool) { + const tdWires = this.getToolDefWires(tool.handleName); + for (const w of tdWires) { + for (const src of w.sources) this.collectToolRefs(src.expr, deps); } } + return deps; + } - // 2. pullSingle guard — reject unsafe keys in wire source paths - for (const w of bridge.wires) { - const refs: NodeRef[] = []; - 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) { - if (UNSAFE_KEYS.has(seg)) - throw new Error(`Unsafe property traversal: ${seg}`); + private collectToolRefs(expr: Expression, deps: Set): void { + switch (expr.type) { + case "ref": + if (expr.ref.instance != null) deps.add(refTrunkKey(expr.ref)); + if (expr.ref.module === "__local") { + const alias = this.aliases.get(expr.ref.field); + if (alias) { for (const src of alias.sources) this.collectToolRefs(src.expr, deps); } } + break; + case "pipe": { + this.collectToolRefs(expr.source, deps); + const ptk = this.findToolTkForHandle(expr.handle); + if (ptk) deps.add(ptk); + break; } + case "ternary": this.collectToolRefs(expr.cond, deps); this.collectToolRefs(expr.then, deps); this.collectToolRefs(expr.else, deps); break; + case "and": case "or": this.collectToolRefs(expr.left, deps); this.collectToolRefs(expr.right, deps); break; + case "binary": this.collectToolRefs(expr.left, deps); this.collectToolRefs(expr.right, deps); break; + case "unary": this.collectToolRefs(expr.operand, deps); break; + case "concat": for (const p of expr.parts) this.collectToolRefs(p, deps); break; + case "array": this.collectToolRefs(expr.source, deps); break; } + } - // 3. tool lookup guard — reject unsafe segments in dotted tool names - for (const h of bridge.handles) { - if (h.kind !== "tool") continue; - const segments = h.name.split("."); - for (const seg of segments) { - if (UNSAFE_KEYS.has(seg)) - throw new Error( - `No tool found for "${h.name}" — prototype-pollution attempt blocked`, - ); - } + private findToolTkForHandle(handle: string): string | undefined { + for (const [tk, reg] of this.tools) { + if (reg.handleName === handle) return tk; } + return undefined; + } - // Build a set of force tool trunk keys and their catch behavior - const forceMap = new Map(); - if (bridge.forces) { - for (const f of bridge.forces) { - const tk = `${f.module}:${f.type}:${f.field}:${f.instance ?? 1}`; - forceMap.set(tk, { catchError: f.catchError }); + private topologicalSort(): string[] { + const allTools = [...this.tools.keys()]; + const inDegree = new Map(); + const adjList = new Map(); + for (const tk of allTools) { inDegree.set(tk, 0); adjList.set(tk, []); } + for (const tk of allTools) { + const deps = this.getToolDeps(tk); + for (const dep of deps) { + if (allTools.includes(dep) && dep !== tk) { + adjList.get(dep)!.push(tk); + inDegree.set(tk, (inDegree.get(tk) ?? 0) + 1); + } } } - - // Separate wires into tool inputs, define containers, and output - const allOutputWires: Wire[] = []; - const toolWires = new Map(); - const defineWires = new Map(); - - for (const w of bridge.wires) { - // Element wires (from array mapping) target the output, not a tool - const toKey = refTrunkKey(w.to); - // Output wires target self trunk — including element wires (to.element = true) - // which produce a key like "_:Type:field:*" instead of "_:Type:field" - const toTrunkNoElement = w.to.element - ? `${w.to.module}:${w.to.type}:${w.to.field}` - : toKey; - if (toTrunkNoElement === this.selfTrunkKey) { - allOutputWires.push(w); - } else if (this.defineContainers.has(toKey)) { - // Wire targets a define-in/out container - const arr = defineWires.get(toKey) ?? []; - arr.push(w); - defineWires.set(toKey, arr); - } else { - const arr = toolWires.get(toKey) ?? []; - arr.push(w); - toolWires.set(toKey, arr); + const queue: string[] = []; + for (const [tk, deg] of inDegree) { if (deg === 0) queue.push(tk); } + const result: string[] = []; + while (queue.length > 0) { + const tk = queue.shift()!; + result.push(tk); + for (const next of adjList.get(tk) ?? []) { + const nd = (inDegree.get(next) ?? 1) - 1; + inDegree.set(next, nd); + if (nd === 0) queue.push(next); } } + return result; + } - // ── Sparse fieldset filtering ────────────────────────────────────── - // When requestedFields is provided, drop output wires for fields that - // weren't requested. Kahn's algorithm will then naturally eliminate - // tools that only feed into those dropped wires. - const filteredOutputWires = this.requestedFields - ? allOutputWires.filter((w) => { - // Root wires (path length 0) and element wires are always included - if (w.to.path.length === 0) return true; - const fieldPath = w.to.path.join("."); - return matchesRequestedFields(fieldPath, this.requestedFields); - }) - : allOutputWires; - const outputWires = this.reorderOverdefinedOutputWires(filteredOutputWires); - - // Ensure force-only tools (no wires targeting them from output) are - // still included in the tool map for scheduling - for (const [tk] of forceMap) { - if (!toolWires.has(tk) && this.tools.has(tk)) { - toolWires.set(tk, []); + private topologicalLayers(): string[][] { + const allTools = [...this.tools.keys()]; + const inDegree = new Map(); + const adjList = new Map(); + for (const tk of allTools) { inDegree.set(tk, 0); adjList.set(tk, []); } + for (const tk of allTools) { + const deps = this.getToolDeps(tk); + for (const dep of deps) { + if (allTools.includes(dep) && dep !== tk) { + adjList.get(dep)!.push(tk); + inDegree.set(tk, (inDegree.get(tk) ?? 0) + 1); + } + } + } + const layers: string[][] = []; + const remaining = new Map(inDegree); + while (remaining.size > 0) { + const layer: string[] = []; + for (const [tk, deg] of remaining) { if (deg === 0) layer.push(tk); } + if (layer.length === 0) break; + for (const tk of layer) { + remaining.delete(tk); + for (const next of adjList.get(tk) ?? []) { + if (remaining.has(next)) remaining.set(next, (remaining.get(next) ?? 1) - 1); + } } + layers.push(layer); } + return layers; + } - // Detect tools whose output is only referenced by catch-guarded wires. - // These tools need try/catch wrapping to prevent unhandled rejections. + // ── Dead tool elimination ───────────────────────────────────────────── + + private findLiveTools(outputWires: ExtractedWire[]): Set { + const live = new Set(); + const visit = (tk: string) => { + if (live.has(tk)) return; + live.add(tk); + const deps = this.getToolDeps(tk); + for (const dep of deps) { if (this.tools.has(dep)) visit(dep); } + }; for (const w of outputWires) { - const needsCatch = - hasCatchFallback(w) || - hasCatchControl(w) || - wSafe(w) || - (isAndW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)) || - (isOrW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)); - if (!needsCatch) continue; - if (isPull(w)) { - const srcKey = refTrunkKey(wRef(w)); - this.catchGuardedTools.add(srcKey); + for (const src of w.sources) { + const deps = new Set(); + this.collectToolRefs(src.expr, deps); + for (const d of deps) visit(d); } - 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 (w.catch) { + const deps = new Set(); + if ("ref" in w.catch && w.catch.ref.instance != null) deps.add(refTrunkKey(w.catch.ref)); + if ("expr" in w.catch) this.collectToolRefs(w.catch.expr, deps); + for (const d of deps) visit(d); } - if (isOrW(w)) { - this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).left))); - if (eRef(wAndOr(w).right)) - this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).right))); + } + for (const s of this.spreads) { + for (const src of s.sources) { + const deps = new Set(); + this.collectToolRefs(src.expr, deps); + for (const d of deps) visit(d); } } - // 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) || wSafe(w); - if (!needsCatch) continue; - if (isPull(w)) { - const srcKey = refTrunkKey(wRef(w)); - this.catchGuardedTools.add(srcKey); - } - 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), - ); - } + for (const f of this.forces) { + const tk = `${f.module}:${f.type}:${f.field}:${f.instance ?? 1}`; + if (this.tools.has(tk)) visit(tk); + } + for (const [, a] of this.aliases) { + for (const src of a.sources) { + const deps = new Set(); + this.collectToolRefs(src.expr, deps); + for (const d of deps) visit(d); } } - // Mark tools catch-guarded when pipe wires carry safe/catch modifiers - // (e.g. `api?.score > 5` — the pipe from api to the `>` operator has safe) - for (const [, twires] of toolWires) { - for (const w of twires) { - const isSafe = - wSafe(w) || - (isAndW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)) || - (isOrW(w) && (wAndOr(w).leftSafe || wAndOr(w).rightSafe)); - if (!isSafe) continue; - if (isPull(w)) { - this.catchGuardedTools.add(refTrunkKey(wRef(w))); - } - 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 (isOrW(w)) { - this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).left))); - if (eRef(wAndOr(w).right)) - this.catchGuardedTools.add(refTrunkKey(eRef(wAndOr(w).right))); + return live; + } + + // ── Overdefinition bypass ───────────────────────────────────────────── + + private analyzeOverdefinitionBypass( + outputWires: ExtractedWire[], + _toolOrder: string[], + ): Map { + const groups = new Map(); + for (const w of outputWires) { + const key = w.target.path.join("."); + const arr = groups.get(key) ?? []; + arr.push(w); + groups.set(key, arr); + } + + const toolSecondary = new Map(); + + for (const [pathKey, wires] of groups) { + if (wires.length <= 1) continue; + for (let i = 0; i < wires.length; i++) { + const w = wires[i]!; + const toolTks = new Set(); + this.collectToolRefs(w.sources[0]!.expr, toolTks); + for (const tk of toolTks) { + if (!this.tools.has(tk)) continue; + let pos = toolSecondary.get(tk); + if (!pos) { pos = { primary: false, secondaryPaths: [] }; toolSecondary.set(tk, pos); } + if (i === 0) pos.primary = true; + else pos.secondaryPaths.push(pathKey); } } } - // Detect element-scoped tools/containers: any node that directly receives - // element input, or depends on another element-scoped node, must be emitted - // inside the array callback rather than at the top level. - const elementScopeEntries = [ - ...toolWires.entries(), - ...defineWires.entries(), - ]; - let changed = true; - while (changed) { - changed = false; - for (const [tk, wires] of elementScopeEntries) { - if (this.elementScopedTools.has(tk)) continue; + const forceKeys = new Set(this.forces.map((f) => `${f.module}:${f.type}:${f.field}:${f.instance ?? 1}`)); + const result = new Map(); + + for (const [tk, pos] of toolSecondary) { + if (pos.primary || forceKeys.has(tk)) continue; + // Check if tool has primary contributions on other paths + let hasPrimaryElsewhere = false; + for (const [, wires] of groups) { + const firstToolTks = new Set(); + this.collectToolRefs(wires[0]!.sources[0]!.expr, firstToolTks); + if (firstToolTks.has(tk)) { hasPrimaryElsewhere = true; break; } + } + // Also check single-wire paths + for (const w of outputWires) { + const pathKey = w.target.path.join("."); + const pathGroup = groups.get(pathKey)!; + if (pathGroup.length === 1) { + const wToolTks = new Set(); + this.collectToolRefs(w.sources[0]!.expr, wToolTks); + if (wToolTks.has(tk)) { hasPrimaryElsewhere = true; break; } + } + } + if (hasPrimaryElsewhere) continue; + + const checkExprs: string[] = []; + for (const pathKey of pos.secondaryPaths) { + const wires = groups.get(pathKey)!; for (const w of wires) { - if (isPull(w) && wRef(w).element) { - this.elementScopedTools.add(tk); - changed = true; - break; - } - if ( - this.getSourceTrunks(w).some((srcKey) => - this.elementScopedTools.has(srcKey), - ) - ) { - this.elementScopedTools.add(tk); - changed = true; - break; + const wToolTks = new Set(); + this.collectToolRefs(w.sources[0]!.expr, wToolTks); + if (wToolTks.has(tk)) break; + if (this.wireCost(w) === 0) { + checkExprs.push(this.exprToJs(w.sources[0]!.expr, " ")); } } } + if (checkExprs.length > 0) result.set(tk, { checkExprs }); } + return result; + } - // Merge define container entries into toolWires for topological sorting. - // Define containers are scheduled like tools (they have dependencies and - // dependants) but they emit simple object assignments instead of tool calls. - for (const [tk, wires] of defineWires) { - toolWires.set(tk, wires); - } - - // Topological sort of tool calls (including define containers) - const toolOrder = this.topologicalSort(toolWires); - // Layer-based grouping for parallel emission - const toolLayers = this.topologicalLayers(toolWires); - - // ── Overdefinition bypass analysis ──────────────────────────────────── - // When multiple wires target the same output path ("overdefinition"), - // the runtime's pull-based model skips later tools if earlier sources - // resolve non-null. The compiler replicates this: if a tool's output - // contributions are ALL in secondary (non-first) position, the tool - // call is wrapped in a null-check on the prior sources. - const conditionalTools = this.analyzeOverdefinitionBypass( - outputWires, - toolOrder, - forceMap, - ); + // ── Tool lookup expression ──────────────────────────────────────────── - // ── Lazy ternary analysis ──────────────────────────────────────────── - // Identify tools that are ONLY referenced in ternary branches (thenRef/elseRef) - // and never in regular pull wires. These can be lazily evaluated inline. - this.analyzeTernaryOnlyTools(outputWires, toolWires, defineWires, forceMap); + private toolLookupExpr(fnName: string): string { + if (!fnName.includes(".")) return `tools?.[${JSON.stringify(fnName)}]`; + const parts = fnName.split("."); + const nested = "tools" + parts.map((p) => `?.[${JSON.stringify(p)}]`).join(""); + const flat = `tools?.[${JSON.stringify(fnName)}]`; + return `${nested} ?? ${flat}`; + } - // Build code lines - const lines: string[] = []; - lines.push(`// AOT-compiled bridge: ${bridge.type}.${bridge.field}`); - lines.push(`// Generated by @stackables/bridge-compiler`); - lines.push(""); - lines.push( - `export default async function ${fnName}(input, tools, context, __opts) {`, - ); - lines.push( - ` const __BridgePanicError = __opts?.__BridgePanicError ?? class extends Error { constructor(m) { super(m); this.name = "BridgePanicError"; } };`, - ); - lines.push( - ` const __BridgeAbortError = __opts?.__BridgeAbortError ?? class extends Error { constructor(m) { super(m ?? "Execution aborted by external signal"); this.name = "BridgeAbortError"; } };`, - ); - lines.push( - ` const __BridgeTimeoutError = __opts?.__BridgeTimeoutError ?? class extends Error { constructor(n, ms) { super('Tool "' + n + '" timed out after ' + ms + 'ms'); this.name = "BridgeTimeoutError"; } };`, - ); - lines.push( - ` const __BridgeRuntimeError = __opts?.__BridgeRuntimeError ?? class extends Error { constructor(message, options) { super(message, options && "cause" in options ? { cause: options.cause } : undefined); this.name = "BridgeRuntimeError"; this.bridgeLoc = options?.bridgeLoc; } };`, - ); + private toolFnVar(fnName: string): string { + let varName = this.toolFnVars.get(fnName); + if (!varName) { + varName = `__fn${++this.toolFnVarCounter}`; + this.toolFnVars.set(fnName, varName); + } + return varName; + } + + // ── Preamble ────────────────────────────────────────────────────────── + + private emitPreamble(lines: string[]): void { + lines.push(` const __BridgePanicError = __opts?.__BridgePanicError ?? class extends Error { constructor(m) { super(m); this.name = "BridgePanicError"; } };`); + lines.push(` const __BridgeAbortError = __opts?.__BridgeAbortError ?? class extends Error { constructor(m) { super(m ?? "Execution aborted by external signal"); this.name = "BridgeAbortError"; } };`); + lines.push(` const __BridgeTimeoutError = __opts?.__BridgeTimeoutError ?? class extends Error { constructor(n, ms) { super('Tool "' + n + '" timed out after ' + ms + 'ms'); this.name = "BridgeTimeoutError"; } };`); + lines.push(` const __BridgeRuntimeError = __opts?.__BridgeRuntimeError ?? class extends Error { constructor(message, options) { super(message, options && "cause" in options ? { cause: options.cause } : undefined); this.name = "BridgeRuntimeError"; this.bridgeLoc = options?.bridgeLoc; } };`); lines.push(` const __signal = __opts?.signal;`); lines.push(` const __timeoutMs = __opts?.toolTimeoutMs ?? 0;`); - lines.push( - ` const __ctx = { logger: __opts?.logger ?? {}, signal: __signal };`, - ); - lines.push( - ` const __queueMicrotask = globalThis.queueMicrotask ?? ((fn) => Promise.resolve().then(fn));`, - ); + lines.push(` const __ctx = { logger: __opts?.logger ?? {}, signal: __signal };`); + lines.push(` const __queueMicrotask = globalThis.queueMicrotask ?? ((fn) => Promise.resolve().then(fn));`); lines.push(` const __batchQueues = new Map();`); lines.push(` const __trace = __opts?.__trace;`); lines.push(` function __toolExecutionLogLevel(fn) {`); lines.push(` const log = fn?.bridge?.log;`); lines.push(` if (log === false || log == null) return false;`); lines.push(` if (log === true) return "info";`); - lines.push( - ` return log.execution === "info" ? "info" : log.execution ? "debug" : false;`, - ); + lines.push(` return log.execution === "info" ? "info" : log.execution ? "debug" : false;`); lines.push(` }`); lines.push(` function __toolErrorLogLevel(fn) {`); lines.push(` const log = fn?.bridge?.log;`); lines.push(` if (log === false) return false;`); lines.push(` if (log == null || log === true) return "error";`); - lines.push( - ` return log.errors === false ? false : log.errors === "warn" ? "warn" : "error";`, - ); + lines.push(` return log.errors === false ? false : log.errors === "warn" ? "warn" : "error";`); lines.push(` }`); lines.push(` function __rethrowBridgeError(err, loc) {`); - lines.push( - ` if (err?.name === "BridgePanicError") throw __attachBridgeMeta(err, loc);`, - ); + lines.push(` if (err?.name === "BridgePanicError") throw __attachBridgeMeta(err, loc);`); lines.push(` if (err?.name === "BridgeAbortError") throw err;`); - lines.push( - ` if (err?.name === "BridgeRuntimeError" && err.bridgeLoc != null) throw err;`, - ); - lines.push( - ` throw new __BridgeRuntimeError(err instanceof Error ? err.message : String(err), { cause: err, bridgeLoc: loc });`, - ); + lines.push(` if (err?.name === "BridgeRuntimeError" && err.bridgeLoc != null) throw err;`); + lines.push(` throw new __BridgeRuntimeError(err instanceof Error ? err.message : String(err), { cause: err, bridgeLoc: loc });`); lines.push(` }`); lines.push(` function __wrapBridgeError(fn, loc) {`); - lines.push(` try {`); - lines.push(` return fn();`); - lines.push(` } catch (err) {`); - lines.push(` __rethrowBridgeError(err, loc);`); - lines.push(` }`); + lines.push(` try { return fn(); } catch (err) { __rethrowBridgeError(err, loc); }`); lines.push(` }`); lines.push(` async function __wrapBridgeErrorAsync(fn, loc) {`); - lines.push(` try {`); - lines.push(` return await fn();`); - lines.push(` } catch (err) {`); - lines.push(` __rethrowBridgeError(err, loc);`); - lines.push(` }`); + lines.push(` try { return await fn(); } catch (err) { __rethrowBridgeError(err, loc); }`); lines.push(` }`); lines.push(` function __attachBridgeMeta(err, loc) {`); - lines.push( - ` if (err && (typeof err === "object" || typeof err === "function")) {`, - ); + lines.push(` if (err && (typeof err === "object" || typeof err === "function")) {`); lines.push(` if (err.bridgeLoc === undefined) err.bridgeLoc = loc;`); lines.push(` }`); lines.push(` return err;`); lines.push(` }`); - lines.push( - ` // Single-segment access is split out to preserve the compiled-path recovery documented in packages/bridge-compiler/performance.md (#2).`, - ); - lines.push( - ` function __get(base, segment, accessSafe, allowMissingBase) {`, - ); + lines.push(` function __get(base, segment, accessSafe, allowMissingBase) {`); lines.push(` if (base == null) {`); lines.push(` if (allowMissingBase || accessSafe) return undefined;`); - lines.push( - ` throw new TypeError("Cannot read properties of " + base + " (reading '" + segment + "')");`, - ); + lines.push(` throw new TypeError("Cannot read properties of " + base + " (reading '" + segment + "')");`); lines.push(` }`); lines.push(` const next = base[segment];`); - lines.push( - ` const isPrimitiveBase = base !== null && typeof base !== "object" && typeof base !== "function";`, - ); + lines.push(` const isPrimitiveBase = base !== null && typeof base !== "object" && typeof base !== "function";`); lines.push(` if (isPrimitiveBase && next === undefined) {`); - lines.push( - ` throw new TypeError("Cannot read properties of " + base + " (reading '" + segment + "')");`, - ); + lines.push(` throw new TypeError("Cannot read properties of " + base + " (reading '" + segment + "')");`); lines.push(` }`); lines.push(` return next;`); lines.push(` }`); @@ -979,191 +759,104 @@ class CodegenContext { lines.push(` const segment = path[i];`); lines.push(` const accessSafe = safe?.[i] ?? false;`); lines.push(` if (result == null) {`); - lines.push(` if ((i === 0 && allowMissingBase) || accessSafe) {`); - lines.push(` result = undefined;`); - lines.push(` continue;`); - lines.push(` }`); - lines.push( - ` throw new TypeError("Cannot read properties of " + result + " (reading '" + segment + "')");`, - ); + lines.push(` if ((i === 0 && allowMissingBase) || accessSafe) { result = undefined; continue; }`); + lines.push(` throw new TypeError("Cannot read properties of " + result + " (reading '" + segment + "')");`); lines.push(` }`); lines.push(` const next = result[segment];`); - lines.push( - ` const isPrimitiveBase = result !== null && typeof result !== "object" && typeof result !== "function";`, - ); + lines.push(` const isPrimitiveBase = result !== null && typeof result !== "object" && typeof result !== "function";`); lines.push(` if (isPrimitiveBase && next === undefined) {`); - lines.push( - ` throw new TypeError("Cannot read properties of " + result + " (reading '" + segment + "')");`, - ); + lines.push(` throw new TypeError("Cannot read properties of " + result + " (reading '" + segment + "')");`); lines.push(` }`); lines.push(` result = next;`); lines.push(` }`); lines.push(` return result;`); lines.push(` }`); lines.push(` function __callBatch(fn, input, toolDefName, fnName) {`); - lines.push( - ` if (__signal?.aborted) return Promise.reject(new __BridgeAbortError());`, - ); - lines.push( - ` if (typeof fn !== "function") return Promise.reject(new __BridgeRuntimeError('No tool found for "' + fnName + '"'));`, - ); + lines.push(` if (__signal?.aborted) return Promise.reject(new __BridgeAbortError());`); + lines.push(` if (typeof fn !== "function") return Promise.reject(new __BridgeRuntimeError('No tool found for "' + fnName + '"'));`); lines.push(` let queue = __batchQueues.get(fn);`); lines.push(` if (!queue) {`); - lines.push( - ` queue = { items: [], scheduled: false, toolDefName, fnName, maxBatchSize: typeof fn?.bridge?.batch === "object" && fn?.bridge?.batch?.maxBatchSize > 0 ? Math.floor(fn.bridge.batch.maxBatchSize) : undefined };`, - ); + lines.push(` queue = { items: [], scheduled: false, toolDefName, fnName, maxBatchSize: typeof fn?.bridge?.batch === "object" && fn?.bridge?.batch?.maxBatchSize > 0 ? Math.floor(fn.bridge.batch.maxBatchSize) : undefined };`); lines.push(` __batchQueues.set(fn, queue);`); lines.push(` }`); lines.push(` return new Promise((resolve, reject) => {`); lines.push(` queue.items.push({ input, resolve, reject });`); lines.push(` if (queue.scheduled) return;`); lines.push(` queue.scheduled = true;`); - lines.push( - ` __queueMicrotask(() => { void __flushBatch(fn, queue); });`, - ); + lines.push(` __queueMicrotask(() => { void __flushBatch(fn, queue); });`); lines.push(` });`); lines.push(` }`); lines.push(` async function __flushBatch(fn, queue) {`); - lines.push( - ` const pending = queue.items.splice(0, queue.items.length);`, - ); + lines.push(` const pending = queue.items.splice(0, queue.items.length);`); lines.push(` queue.scheduled = false;`); lines.push(` if (pending.length === 0) return;`); - lines.push(` if (__signal?.aborted) {`); - lines.push(` const err = new __BridgeAbortError();`); - lines.push(` for (const item of pending) item.reject(err);`); - lines.push(` return;`); - lines.push(` }`); - lines.push( - ` const chunkSize = queue.maxBatchSize && queue.maxBatchSize > 0 ? queue.maxBatchSize : pending.length;`, - ); - lines.push( - ` for (let start = 0; start < pending.length; start += chunkSize) {`, - ); + lines.push(` if (__signal?.aborted) { const err = new __BridgeAbortError(); for (const item of pending) item.reject(err); return; }`); + lines.push(` const chunkSize = queue.maxBatchSize && queue.maxBatchSize > 0 ? queue.maxBatchSize : pending.length;`); + lines.push(` for (let start = 0; start < pending.length; start += chunkSize) {`); lines.push(` const chunk = pending.slice(start, start + chunkSize);`); lines.push(` const inputs = chunk.map((item) => item.input);`); - lines.push( - ` const startTime = (__trace || __ctx.logger) ? performance.now() : 0;`, - ); + lines.push(` const startTime = (__trace || __ctx.logger) ? performance.now() : 0;`); lines.push(` try {`); lines.push(` const batchPromise = fn(inputs, __ctx);`); lines.push(` let result;`); - lines.push( - ` if (__timeoutMs > 0 && batchPromise && typeof batchPromise.then === "function") {`, - ); - lines.push( - ` let t; const timeout = new Promise((_, rej) => { t = setTimeout(() => rej(new __BridgeTimeoutError(queue.toolDefName, __timeoutMs)), __timeoutMs); });`, - ); - lines.push( - ` try { result = await Promise.race([batchPromise, timeout]); } finally { clearTimeout(t); }`, - ); - lines.push(` } else {`); - lines.push(` result = await batchPromise;`); - lines.push(` }`); - lines.push( - ` if (__trace && fn?.bridge?.trace !== false) __trace(queue.toolDefName, queue.fnName, startTime, performance.now(), inputs, result, null);`, - ); + lines.push(` if (__timeoutMs > 0 && batchPromise && typeof batchPromise.then === "function") {`); + lines.push(` let t; const timeout = new Promise((_, rej) => { t = setTimeout(() => rej(new __BridgeTimeoutError(queue.toolDefName, __timeoutMs)), __timeoutMs); });`); + lines.push(` try { result = await Promise.race([batchPromise, timeout]); } finally { clearTimeout(t); }`); + lines.push(` } else { result = await batchPromise; }`); + lines.push(` if (__trace && fn?.bridge?.trace !== false) __trace(queue.toolDefName, queue.fnName, startTime, performance.now(), inputs, result, null);`); lines.push(` const __execLevel = __toolExecutionLogLevel(fn);`); - lines.push( - ` if (__execLevel) __ctx.logger?.[__execLevel]?.({ tool: queue.toolDefName, fn: queue.fnName, durationMs: Math.round((performance.now() - startTime) * 1000) / 1000 }, "[bridge] tool completed");`, - ); - lines.push( - ` if (!Array.isArray(result)) throw new Error('Batch tool "' + queue.toolDefName + '" must return an array of results');`, - ); - lines.push( - ` if (result.length !== chunk.length) throw new Error('Batch tool "' + queue.toolDefName + '" returned ' + result.length + ' results for ' + chunk.length + ' queued calls');`, - ); - lines.push( - ` for (let i = 0; i < chunk.length; i++) { const value = result[i]; if (value instanceof Error) chunk[i].reject(value); else chunk[i].resolve(value); }`, - ); + lines.push(` if (__execLevel) __ctx.logger?.[__execLevel]?.({ tool: queue.toolDefName, fn: queue.fnName, durationMs: Math.round((performance.now() - startTime) * 1000) / 1000 }, "[bridge] tool completed");`); + lines.push(` if (!Array.isArray(result)) throw new Error('Batch tool "' + queue.toolDefName + '" must return an array of results');`); + lines.push(` if (result.length !== chunk.length) throw new Error('Batch tool "' + queue.toolDefName + '" returned ' + result.length + ' results for ' + chunk.length + ' queued calls');`); + lines.push(` for (let i = 0; i < chunk.length; i++) { const value = result[i]; if (value instanceof Error) chunk[i].reject(value); else chunk[i].resolve(value); }`); lines.push(` } catch (err) {`); - lines.push( - ` try { __rethrowBridgeError(err, undefined); } catch (_wrapped) { err = _wrapped; }`, - ); - lines.push( - ` if (__trace && fn?.bridge?.trace !== false) __trace(queue.toolDefName, queue.fnName, startTime, performance.now(), inputs, null, err);`, - ); + lines.push(` try { __rethrowBridgeError(err, undefined); } catch (_wrapped) { err = _wrapped; }`); + lines.push(` if (__trace && fn?.bridge?.trace !== false) __trace(queue.toolDefName, queue.fnName, startTime, performance.now(), inputs, null, err);`); lines.push(` const __errorLevel = __toolErrorLogLevel(fn);`); - lines.push( - ` if (__errorLevel) __ctx.logger?.[__errorLevel]?.({ tool: queue.toolDefName, fn: queue.fnName, err: err instanceof Error ? err.message : String(err) }, "[bridge] tool failed");`, - ); + lines.push(` if (__errorLevel) __ctx.logger?.[__errorLevel]?.({ tool: queue.toolDefName, fn: queue.fnName, err: err instanceof Error ? err.message : String(err) }, "[bridge] tool failed");`); lines.push(` for (const item of chunk) item.reject(err);`); lines.push(` }`); lines.push(` }`); lines.push(` }`); - // Sync tool caller — no await, no timeout, enforces no-promise return. lines.push(` function __callSync(fn, input, toolDefName, fnName) {`); lines.push(` if (__signal?.aborted) throw new __BridgeAbortError();`); - lines.push( - ` if (typeof fn !== "function") throw new __BridgeRuntimeError('No tool found for "' + fnName + '"');`, - ); + lines.push(` if (typeof fn !== "function") throw new __BridgeRuntimeError('No tool found for "' + fnName + '"');`); lines.push(` const start = __trace ? performance.now() : 0;`); lines.push(` try {`); lines.push(` const result = fn(input, __ctx);`); - lines.push( - ` if (result && typeof result.then === "function") throw new Error("Tool \\"" + toolDefName + "\\" declared {sync:true} but returned a Promise");`, - ); - lines.push( - ` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, result, null);`, - ); + lines.push(` if (result && typeof result.then === "function") throw new Error("Tool \\"" + toolDefName + "\\" declared {sync:true} but returned a Promise");`); + lines.push(` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, result, null);`); lines.push(` const __execLevel = __toolExecutionLogLevel(fn);`); - lines.push( - ` if (__execLevel) __ctx.logger?.[__execLevel]?.({ tool: toolDefName, fn: fnName, durationMs: Math.round((performance.now() - start) * 1000) / 1000 }, "[bridge] tool completed");`, - ); + lines.push(` if (__execLevel) __ctx.logger?.[__execLevel]?.({ tool: toolDefName, fn: fnName, durationMs: Math.round((performance.now() - start) * 1000) / 1000 }, "[bridge] tool completed");`); lines.push(` return result;`); lines.push(` } catch (err) {`); - lines.push( - ` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, null, err);`, - ); + lines.push(` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, null, err);`); lines.push(` const __errorLevel = __toolErrorLogLevel(fn);`); - lines.push( - ` if (__errorLevel) __ctx.logger?.[__errorLevel]?.({ tool: toolDefName, fn: fnName, err: err instanceof Error ? err.message : String(err) }, "[bridge] tool failed");`, - ); + lines.push(` if (__errorLevel) __ctx.logger?.[__errorLevel]?.({ tool: toolDefName, fn: fnName, err: err instanceof Error ? err.message : String(err) }, "[bridge] tool failed");`); lines.push(` __rethrowBridgeError(err, undefined);`); lines.push(` }`); lines.push(` }`); - lines.push( - ` const __isLoopCtrl = (v) => (v?.__bridgeControl === "break" || v?.__bridgeControl === "continue") && Number.isInteger(v?.levels) && v.levels > 0;`, - ); - lines.push( - ` const __nextLoopCtrl = (v) => ({ __bridgeControl: v.__bridgeControl, levels: v.levels - 1 });`, - ); - // Async tool caller — full promise handling with optional timeout. + lines.push(` const __isLoopCtrl = (v) => (v?.__bridgeControl === "break" || v?.__bridgeControl === "continue") && Number.isInteger(v?.levels) && v.levels > 0;`); + lines.push(` const __nextLoopCtrl = (v) => ({ __bridgeControl: v.__bridgeControl, levels: v.levels - 1 });`); lines.push(` async function __call(fn, input, toolDefName, fnName) {`); lines.push(` if (__signal?.aborted) throw new __BridgeAbortError();`); - lines.push( - ` if (typeof fn !== "function") throw new __BridgeRuntimeError('No tool found for "' + fnName + '"');`, - ); + lines.push(` if (typeof fn !== "function") throw new __BridgeRuntimeError('No tool found for "' + fnName + '"');`); lines.push(` const start = __trace ? performance.now() : 0;`); lines.push(` try {`); lines.push(` const p = fn(input, __ctx);`); lines.push(` let result;`); lines.push(` if (__timeoutMs > 0) {`); - lines.push( - ` let t; const timeout = new Promise((_, rej) => { t = setTimeout(() => rej(new __BridgeTimeoutError(toolDefName, __timeoutMs)), __timeoutMs); });`, - ); - lines.push( - ` try { result = await Promise.race([p, timeout]); } finally { clearTimeout(t); }`, - ); - lines.push(` } else {`); - lines.push(` result = await p;`); - lines.push(` }`); - lines.push( - ` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, result, null);`, - ); + lines.push(` let t; const timeout = new Promise((_, rej) => { t = setTimeout(() => rej(new __BridgeTimeoutError(toolDefName, __timeoutMs)), __timeoutMs); });`); + lines.push(` try { result = await Promise.race([p, timeout]); } finally { clearTimeout(t); }`); + lines.push(` } else { result = await p; }`); + lines.push(` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, result, null);`); lines.push(` const __execLevel = __toolExecutionLogLevel(fn);`); - lines.push( - ` if (__execLevel) __ctx.logger?.[__execLevel]?.({ tool: toolDefName, fn: fnName, durationMs: Math.round((performance.now() - start) * 1000) / 1000 }, "[bridge] tool completed");`, - ); + lines.push(` if (__execLevel) __ctx.logger?.[__execLevel]?.({ tool: toolDefName, fn: fnName, durationMs: Math.round((performance.now() - start) * 1000) / 1000 }, "[bridge] tool completed");`); lines.push(` return result;`); lines.push(` } catch (err) {`); - lines.push( - ` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, null, err);`, - ); + lines.push(` if (__trace && fn?.bridge?.trace !== false) __trace(toolDefName, fnName, start, performance.now(), input, null, err);`); lines.push(` const __errorLevel = __toolErrorLogLevel(fn);`); - lines.push( - ` if (__errorLevel) __ctx.logger?.[__errorLevel]?.({ tool: toolDefName, fn: fnName, err: err instanceof Error ? err.message : String(err) }, "[bridge] tool failed");`, - ); + lines.push(` if (__errorLevel) __ctx.logger?.[__errorLevel]?.({ tool: toolDefName, fn: fnName, err: err instanceof Error ? err.message : String(err) }, "[bridge] tool failed");`); lines.push(` __rethrowBridgeError(err, undefined);`); lines.push(` }`); lines.push(` }`); @@ -1171,3710 +864,586 @@ class CodegenContext { lines.push(` const __toolMemoCache = new Map();`); lines.push(` function __stableMemoizeKey(value) {`); lines.push(` if (value === undefined) return "undefined";`); - lines.push(' if (typeof value === "bigint") return `${value}n`;'); - lines.push( - ` if (value === null || typeof value !== "object") { const serialized = JSON.stringify(value); return serialized ?? String(value); }`, - ); - lines.push(` if (Array.isArray(value)) {`); - lines.push( - ' return `[${value.map((item) => __stableMemoizeKey(item)).join(",")}]`;', - ); - lines.push(` }`); - lines.push( - ` const entries = Object.entries(value).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));`, - ); - lines.push( - ' return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${__stableMemoizeKey(entryValue)}`).join(",")}}`;', - ); + lines.push(" if (typeof value === \"bigint\") return `${value}n`;"); + lines.push(` if (value === null || typeof value !== "object") { const serialized = JSON.stringify(value); return serialized ?? String(value); }`); + lines.push(" if (Array.isArray(value)) { return `[${value.map((item) => __stableMemoizeKey(item)).join(\",\")}]`; }"); + lines.push(` const entries = Object.entries(value).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));`); + lines.push(" return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${__stableMemoizeKey(entryValue)}`).join(\",\")}}`;"); lines.push(` }`); - lines.push( - ` function __callMemoized(fn, input, toolDefName, fnName, memoizeKey) {`, - ); + lines.push(` function __callMemoized(fn, input, toolDefName, fnName, memoizeKey) {`); lines.push(` let toolCache = __toolMemoCache.get(memoizeKey);`); - lines.push(` if (!toolCache) {`); - lines.push(` toolCache = new Map();`); - lines.push(` __toolMemoCache.set(memoizeKey, toolCache);`); - lines.push(` }`); + lines.push(` if (!toolCache) { toolCache = new Map(); __toolMemoCache.set(memoizeKey, toolCache); }`); lines.push(` const cacheKey = __stableMemoizeKey(input);`); lines.push(` const cached = toolCache.get(cacheKey);`); lines.push(` if (cached !== undefined) return cached;`); lines.push(` try {`); - lines.push( - ` const result = fn?.bridge?.batch ? __callBatch(fn, input, toolDefName, fnName) : fn?.bridge?.sync ? __callSync(fn, input, toolDefName, fnName) : __call(fn, input, toolDefName, fnName);`, - ); + lines.push(` const result = fn?.bridge?.batch ? __callBatch(fn, input, toolDefName, fnName) : fn?.bridge?.sync ? __callSync(fn, input, toolDefName, fnName) : __call(fn, input, toolDefName, fnName);`); lines.push(` if (result && typeof result.then === "function") {`); - lines.push( - ` const pending = Promise.resolve(result).catch((error) => {`, - ); - lines.push(` toolCache.delete(cacheKey);`); - lines.push(` throw error;`); - lines.push(` });`); - lines.push(` toolCache.set(cacheKey, pending);`); - lines.push(` return pending;`); + lines.push(` const pending = Promise.resolve(result).catch((error) => { toolCache.delete(cacheKey); throw error; });`); + lines.push(` toolCache.set(cacheKey, pending); return pending;`); lines.push(` }`); - lines.push(` toolCache.set(cacheKey, result);`); - lines.push(` return result;`); - lines.push(` } catch (error) {`); - lines.push(` toolCache.delete(cacheKey);`); - lines.push(` throw error;`); - lines.push(` }`); + lines.push(` toolCache.set(cacheKey, result); return result;`); + lines.push(` } catch (error) { toolCache.delete(cacheKey); throw error; }`); lines.push(` }`); } + } - // Placeholder for upfront tool lookups — replaced after code emission - lines.push(" // __TOOL_LOOKUPS__"); - - // ── Dead tool detection ──────────────────────────────────────────── - // Detect which tools are reachable from the (possibly filtered) output - // wires. Uses a backward reachability analysis: start from tools - // referenced in output wires, then transitively follow tool-input - // wires to discover all upstream dependencies. Tools not in the - // reachable set are dead code and can be skipped. - - /** - * Extract all tool trunk keys referenced as **sources** in a set of - * wires. A "source key" is the trunk key of a node that feeds data - * into a wire (the right-hand side of `target <- source`). This - * includes pull refs, ternary branches, condAnd/condOr operands, - * and all fallback refs. Used by the backward reachability analysis - * to discover which tools are transitively needed by the output. - */ - const collectSourceKeys = (wires: Wire[]): Set => { - const keys = new Set(); - for (const w of wires) { - 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 (isAndW(w)) { - keys.add(refTrunkKey(eRef(wAndOr(w).left))); - if (eRef(wAndOr(w).right)) - keys.add(refTrunkKey(eRef(wAndOr(w).right))); - } - if (isOrW(w)) { - keys.add(refTrunkKey(eRef(wAndOr(w).left))); - if (eRef(wAndOr(w).right)) - keys.add(refTrunkKey(eRef(wAndOr(w).right))); - } - if (hasFallbacks(w)) { - for (const fb of fallbacks(w)) { - if (eRef(fb.expr)) keys.add(refTrunkKey(eRef(fb.expr))); - } - } - if (hasCatchRef(w)) { - keys.add(refTrunkKey(catchRef(w)!)); - } - } - return keys; - }; - - // Seed: tools directly referenced by output wires + forced tools - const referencedToolKeys = collectSourceKeys(outputWires); - for (const tk of forceMap.keys()) referencedToolKeys.add(tk); + // ── Tool lookups ────────────────────────────────────────────────────── - // Transitive closure: walk backward through tool input wires - const visited = new Set(); - const queue = [...referencedToolKeys]; - while (queue.length > 0) { - const tk = queue.pop()!; - if (visited.has(tk)) continue; - visited.add(tk); - const deps = toolWires.get(tk); - if (!deps) continue; - for (const key of collectSourceKeys(deps)) { - if (!visited.has(key)) { - referencedToolKeys.add(key); - queue.push(key); - } - } + private emitToolLookups(lines: string[], liveTools: Set): void { + const emitted = new Set(); + for (const [tk, tool] of this.tools) { + if (!liveTools.has(tk)) continue; + const fnName = tool.toolName; + if (emitted.has(fnName)) continue; + emitted.add(fnName); + const varName = this.toolFnVar(fnName); + lines.push(` const ${varName} = ${this.toolLookupExpr(fnName)};`); } + } - // Emit tool calls and define container assignments - // Tools in the same topological layer have no mutual dependencies and - // 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 sequentialKeys: string[] = []; - - for (const tk of layer) { - if (this.elementScopedTools.has(tk)) continue; - if (this.ternaryOnlyTools.has(tk)) continue; - if ( - !referencedToolKeys.has(tk) && - !forceMap.has(tk) && - !this.defineContainers.has(tk) - ) - continue; - - if (this.isParallelizableTool(tk, conditionalTools, forceMap)) { - const tool = this.tools.get(tk)!; - const wires = toolWires.get(tk) ?? []; - parallelBatch.push({ tk, tool, wires }); - } else { - sequentialKeys.push(tk); - } - } + // ── Tool calls ──────────────────────────────────────────────────────── - // Emit parallelizable tools first so their variables are in scope when - // sequential tools (which may have bypass conditions referencing them) run. - if (parallelBatch.length === 1) { - const { tool, wires } = parallelBatch[0]!; - this.emitToolCall(lines, tool, wires, "normal"); - } else if (parallelBatch.length > 1) { - const varNames = parallelBatch - .map(({ tool }) => tool.varName) - .join(", "); - lines.push(` const [${varNames}] = await Promise.all([`); - for (const { tool, wires } of parallelBatch) { - const callExpr = this.buildNormalCallExpr(tool, wires); - lines.push(` ${callExpr},`); - } - lines.push(` ]);`); - } + private emitToolCalls( + lines: string[], + layers: string[][], + conditionalTools: Map, + liveTools: Set, + ): void { + for (const layer of layers) { + const liveInLayer = layer.filter((tk) => liveTools.has(tk)); + if (liveInLayer.length === 0) continue; - // Emit sequential (complex) tools one by one — same logic as before - for (const tk of sequentialKeys) { - if (this.defineContainers.has(tk)) { - const wires = defineWires.get(tk) ?? []; - const varName = this.varMap.get(tk)!; - if (wires.length === 0) { - lines.push(` const ${varName} = undefined;`); - } else if (wires.length === 1 && wires[0]!.to.path.length === 0) { - const w = wires[0]!; - let expr = this.wireToExpr(w); - if (wSafe(w)) { - const errFlags: string[] = []; - if (isPull(w)) { - const ef = this.getSourceErrorFlag(w); - if (ef) errFlags.push(ef); - } - if (isTern(w)) { - const tern = wTern(w); - const condEf = this.getErrorFlagForRef(eRef(tern.cond)); - if (condEf) errFlags.push(condEf); - if (tern.then.type === "ref") { - const ef = this.getErrorFlagForRef( - (tern.then as RefExpr).ref, - ); - if (ef) errFlags.push(ef); - } - if (tern.else.type === "ref") { - const ef = this.getErrorFlagForRef( - (tern.else as RefExpr).ref, - ); - if (ef) errFlags.push(ef); - } - } - if (errFlags.length > 0) { - const errCheck = errFlags - .map((f) => `${f} !== undefined`) - .join(" || "); - expr = `(${errCheck} ? undefined : ${expr})`; - } - } - lines.push(` const ${varName} = ${expr};`); - } else { - const inputObj = this.buildObjectLiteral( - wires, - (w) => w.to.path, - 4, - ); - lines.push(` const ${varName} = ${inputObj};`); - } - continue; - } - const tool = this.tools.get(tk)!; - const wires = toolWires.get(tk) ?? []; - const forceInfo = forceMap.get(tk); - const bypass = conditionalTools.get(tk); - if (bypass && !forceInfo && !this.catchGuardedTools.has(tk)) { - const condition = bypass.checkExprs - .map((expr) => `(${expr}) == null`) - .join(" || "); - lines.push(` let ${tool.varName};`); - lines.push(` if (${condition}) {`); - const buf: string[] = []; - this.emitToolCall(buf, tool, wires, "normal"); - for (const line of buf) { - lines.push( - " " + - line.replace(`const ${tool.varName} = `, `${tool.varName} = `), - ); - } - lines.push(` }`); - } else if (forceInfo?.catchError) { - this.emitToolCall(lines, tool, wires, "fire-and-forget"); - } else if (this.catchGuardedTools.has(tk)) { - this.emitToolCall(lines, tool, wires, "catch-guarded"); + if (liveInLayer.length === 1) { + this.emitSingleToolCall(lines, liveInLayer[0]!, conditionalTools); + } else { + const hasConditionals = liveInLayer.some((tk) => conditionalTools.has(tk)); + if (hasConditionals) { + for (const tk of liveInLayer) this.emitSingleToolCall(lines, tk, conditionalTools); } else { - this.emitToolCall(lines, tool, wires, "normal"); + const vars = liveInLayer.map((tk) => this.tools.get(tk)!.varName); + lines.push(` const [${vars.join(", ")}] = await Promise.all([`); + for (let i = 0; i < liveInLayer.length; i++) { + const tk = liveInLayer[i]!; + const tool = this.tools.get(tk)!; + const inputObj = this.buildToolInputObj(tk, " "); + const callExpr = this.buildToolCallExpr(tool, inputObj); + const suffix = i < liveInLayer.length - 1 ? "," : ""; + lines.push(` ${callExpr}${suffix}`); + } + lines.push(` ]);`); } } } + } - // Emit output - this.emitOutput(lines, outputWires); - - lines.push("}"); - lines.push(""); + private emitSingleToolCall( + lines: string[], + tk: string, + conditionalTools: Map, + ): void { + const tool = this.tools.get(tk)!; + const inputObj = this.buildToolInputObj(tk, " "); + const callExpr = this.buildToolCallExpr(tool, inputObj); + const cond = conditionalTools.get(tk); - // Insert upfront tool function lookups right after the preamble. - // The toolFnVars map is fully populated at this point from tool emission. - if (this.toolFnVars.size > 0) { - const placeholderIdx = lines.indexOf(" // __TOOL_LOOKUPS__"); - if (placeholderIdx !== -1) { - const lookupLines: string[] = []; - for (const [fnName, varName] of this.toolFnVars) { - lookupLines.push( - ` const ${varName} = ${this.toolLookupExpr(fnName)};`, - ); - } - lines.splice(placeholderIdx, 1, ...lookupLines); + if (cond) { + const check = cond.checkExprs.map((e) => `(${e}) == null`).join(" || "); + lines.push(` let ${tool.varName};`); + lines.push(` if (${check}) {`); + if (this.catchGuardedTools.has(tk)) { + lines.push(` let _err_${tool.varName};`); + lines.push(` try { ${tool.varName} = await ${callExpr}; } catch (e) { _err_${tool.varName} = e; }`); + } else { + lines.push(` ${tool.varName} = await ${callExpr};`); } + lines.push(` }`); + } else if (this.catchGuardedTools.has(tk)) { + lines.push(` let ${tool.varName}, _err_${tool.varName};`); + lines.push(` try { ${tool.varName} = await ${callExpr}; } catch (e) { _err_${tool.varName} = e; }`); + } else { + lines.push(` const ${tool.varName} = await ${callExpr};`); } + } - // Extract function body (lines after the signature, before the closing brace) - const signatureIdx = lines.findIndex((l) => - l.startsWith("export default async function"), - ); - const closingIdx = lines.lastIndexOf("}"); - const bodyLines = lines.slice(signatureIdx + 1, closingIdx); - const functionBody = bodyLines.join("\n"); + private buildToolInputObj(tk: string, indent: string): string { + const wires = this.toolInputWires.get(tk) ?? []; + const tool = this.tools.get(tk)!; + const toolDefWires = this.getToolDefWires(tool.handleName); - return { code: lines.join("\n"), functionName: fnName, functionBody }; - } + const allWires = new Map(); + for (const w of toolDefWires) allWires.set(w.target.path.join("."), w); + for (const w of wires) allWires.set(w.target.path.join("."), w); - // ── Tool call emission ───────────────────────────────────────────────────── - - /** - * Generate a tool call expression that uses __callSync for sync tools at runtime, - * falling back to `await __call` for async tools. Used at individual call sites. - */ - private syncAwareCall( - fnName: string, - inputObj: string, - memoizeTrunkKey?: string, - toolDefName?: string, - ): string { - const fn = this.toolFnVar(fnName); - const defName = JSON.stringify(toolDefName ?? fnName); - const name = JSON.stringify(fnName); - if (memoizeTrunkKey && this.memoizedToolKeys.has(memoizeTrunkKey)) { - return `await __callMemoized(${fn}, ${inputObj}, ${defName}, ${name}, ${JSON.stringify(memoizeTrunkKey)})`; - } - return `(${fn}?.bridge?.batch ? await __callBatch(${fn}, ${inputObj}, ${defName}, ${name}) : ${fn}?.bridge?.sync ? __callSync(${fn}, ${inputObj}, ${defName}, ${name}) : await __call(${fn}, ${inputObj}, ${defName}, ${name}))`; - } + if (allWires.size === 0) return "{}"; - /** - * Same as syncAwareCall but without await — for use inside Promise.all() and - * in sync array map bodies. Returns a value for sync tools, a Promise for async. - */ - private syncAwareCallNoAwait( - fnName: string, - inputObj: string, - memoizeTrunkKey?: string, - toolDefName?: string, - ): string { - const fn = this.toolFnVar(fnName); - const defName = JSON.stringify(toolDefName ?? fnName); - const name = JSON.stringify(fnName); - if (memoizeTrunkKey && this.memoizedToolKeys.has(memoizeTrunkKey)) { - return `__callMemoized(${fn}, ${inputObj}, ${defName}, ${name}, ${JSON.stringify(memoizeTrunkKey)})`; + const entries: [string[], string][] = []; + for (const [, w] of allWires) { + const expr = this.sourceChainToJs(w.sources, w.catch, indent); + entries.push([w.target.path, expr]); } - return `(${fn}?.bridge?.batch ? __callBatch(${fn}, ${inputObj}, ${defName}, ${name}) : ${fn}?.bridge?.sync ? __callSync(${fn}, ${inputObj}, ${defName}, ${name}) : __call(${fn}, ${inputObj}, ${defName}, ${name}))`; + return this.emitNestedObjectLiteral(entries); } - /** - * Emit a tool call with ToolDef wire merging and onError support. - * - * If a ToolDef exists for the tool: - * 1. Apply ToolDef constant wires as base input - * 2. Apply ToolDef pull wires (resolved at runtime from tool deps) - * 3. Apply bridge wires on top (override) - * 4. Call the ToolDef's fn function (not the tool name) - * 5. Wrap in try/catch if onError wire exists - */ - private emitToolCall( - lines: string[], - tool: ToolInfo, - bridgeWires: Wire[], - mode: "normal" | "fire-and-forget" | "catch-guarded" = "normal", - ): void { - const toolDef = this.resolveToolDef(tool.toolName); - - if (!toolDef) { - // Check if this is an internal pipe tool (expressions, interpolation) - if (this.internalToolKeys.has(tool.trunkKey)) { - this.emitInternalToolCall(lines, tool, bridgeWires); - return; - } - // Simple tool call — no ToolDef - const inputObj = this.buildObjectLiteral( - bridgeWires, - (w) => w.to.path, - 4, - ); - if (mode === "fire-and-forget") { - lines.push( - ` try { ${this.syncAwareCall(tool.toolName, inputObj, tool.trunkKey)}; } catch (_e) {}`, - ); - lines.push(` const ${tool.varName} = undefined;`); - } else if (mode === "catch-guarded") { - // Catch-guarded: store result AND the actual error so unguarded wires can re-throw. - lines.push(` let ${tool.varName}, ${tool.varName}_err;`); - lines.push( - ` try { ${tool.varName} = ${this.syncAwareCall(tool.toolName, inputObj, tool.trunkKey)}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; ${tool.varName}_err = _e; }`, - ); - } else { - const callExpr = this.syncAwareCall( - tool.toolName, - inputObj, - tool.trunkKey, - ); - const pullingLoc = this.findPullingWireLoc(tool.trunkKey); - if (pullingLoc) { - lines.push( - ` const ${tool.varName} = ${this.wrapExprWithLoc(callExpr, pullingLoc)};`, - ); - } else { - lines.push( - ` const ${tool.varName} = await __wrapBridgeErrorAsync(async () => (${callExpr}), null);`, - ); - } - } - return; + private getToolDefWires(handleName: string): ExtractedWire[] { + const resolved = this.resolveToolDef(handleName); + const result: ExtractedWire[] = []; + for (const td of resolved.chain) { + if (!td.body) continue; + this.collectToolDefBodyWires(td.body, [], result); } + return result; + } - // ToolDef-backed tool call - const fnName = toolDef.fn ?? tool.toolName; - - // Build input: ToolDef wires first, then bridge wires override - // Track entries by key for precise override matching - const inputEntries = new Map(); - - // Emit ToolDef-level tool dependency calls (e.g. `with authService as auth`) - // These must be emitted before building the input so their vars are in scope. - this.emitToolDeps(lines, toolDef); - - // ── ToolDef pipe forks (expressions, interpolation) ───────────── - // When a ToolDef has pipeHandles, some wires target internal fork tools - // (e.g., add:100000). Compute their results as inline expressions before - // processing the main tool's input wires. - const forkKeys = new Set(); - const forkExprs = new Map(); - if (toolDef.pipeHandles && toolDef.pipeHandles.length > 0) { - for (const ph of toolDef.pipeHandles) { - forkKeys.add(ph.key); + private collectToolDefBodyWires(body: Statement[], pathPrefix: string[], result: ExtractedWire[]): void { + for (const stmt of body) { + if (stmt.kind === "wire") { + result.push({ + target: { ...stmt.target, path: [...pathPrefix, ...stmt.target.path] }, + sources: stmt.sources, + catch: stmt.catch, + loc: stmt.loc, + }); } - // Process forks in instance order (expressions may chain) - const sortedPH = [...toolDef.pipeHandles].sort((a, b) => { - const ai = a.baseTrunk.instance ?? 0; - const bi = b.baseTrunk.instance ?? 0; - return ai - bi; - }); - for (const ph of sortedPH) { - const forkKey = ph.key; - const forkField = ph.baseTrunk.field; - // Collect fork input wires - const forkInputs = new Map(); - for (const tw of toolDef.wires) { - if (refTrunkKey(tw.to) !== forkKey) continue; - const path = tw.to.path.join("."); - 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 wRef(tw).path) { - expr += `[${JSON.stringify(p)}]`; - } - forkInputs.set(path, expr); - } else { - forkInputs.set(path, this.resolveToolWireSource(tw, toolDef)); - } - } - } - // Inline the internal tool operation - forkExprs.set(forkKey, this.inlineForkExpr(forkField, forkInputs)); + if (stmt.kind === "scope") { + this.collectToolDefBodyWires(stmt.body, [...pathPrefix, ...stmt.target.path], result); } } + } - // Accumulate nested ToolDef wire targets (path.length > 1) - // Maps top-level key -> [[remainingPath, expression]] - const nestedInputEntries = new Map(); - const addNestedEntry = (path: string[], expr: string) => { - const topKey = path[0]!; - if (!nestedInputEntries.has(topKey)) nestedInputEntries.set(topKey, []); - nestedInputEntries.get(topKey)!.push([path.slice(1), expr]); - }; + private buildToolCallExpr(tool: ToolReg, inputObj: string): string { + const fnVar = this.toolFnVar(tool.toolName); + const defName = JSON.stringify(tool.handleName); + const fnNameStr = JSON.stringify(tool.toolName); - // ToolDef constant wires (skip fork-targeted wires) - for (const tw of toolDef.wires) { - if (isLit(tw) && !isTern(tw)) { - if (forkKeys.has(refTrunkKey(tw.to))) continue; - const path = tw.to.path; - const expr = emitCoerced(wVal(tw)); - if (path.length > 1) { - addNestedEntry(path, expr); - } else { - inputEntries.set(path[0]!, ` ${JSON.stringify(path[0])}: ${expr}`); - } - } + if (tool.memoize) { + const memoKey = JSON.stringify(tool.trunkKey); + return `__callMemoized(${fnVar}, ${inputObj}, ${defName}, ${fnNameStr}, ${memoKey})`; } + return `(${fnVar}?.bridge?.batch ? __callBatch(${fnVar}, ${inputObj}, ${defName}, ${fnNameStr}) : ${fnVar}?.bridge?.sync ? __callSync(${fnVar}, ${inputObj}, ${defName}, ${fnNameStr}) : __call(${fnVar}, ${inputObj}, ${defName}, ${fnNameStr}))`; + } - // ToolDef pull wires — resolved from tool handles (skip fork-targeted wires) - for (const tw of toolDef.wires) { - if (!isPull(tw)) continue; - if (forkKeys.has(refTrunkKey(tw.to))) continue; - // Skip wires with fallbacks — handled below - if (hasFallbacks(tw)) continue; - const path = tw.to.path; - 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 wRef(tw).path) { - expr = `(${expr})[${JSON.stringify(p)}]`; - } - } else { - expr = this.resolveToolWireSource(tw, toolDef); - } - if (path.length > 1) { - addNestedEntry(path, expr); - } else { - inputEntries.set(path[0]!, ` ${JSON.stringify(path[0])}: ${expr}`); - } + private emitNestedObjectLiteral(entries: [string[], string][]): string { + const byKey = new Map(); + for (const [path, expr] of entries) { + const key = path[0]!; + if (!byKey.has(key)) byKey.set(key, []); + byKey.get(key)!.push([path.slice(1), expr]); } - - // ToolDef ternary wires - for (const tw of toolDef.wires) { - if (!isTern(tw)) continue; - if (forkKeys.has(refTrunkKey(tw.to))) continue; - const path = tw.to.path; - const tern = wTern(tw); - const condExpr = this.resolveToolDefRef( - eRef(tern.cond), - toolDef, - forkExprs, - ); - 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 as string) - : "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 as string) - : "undefined"; - const expr = `(${condExpr} ? ${thenExpr} : ${elseExpr})`; - if (path.length > 1) { - addNestedEntry(path, expr); + const parts: string[] = []; + for (const [key, subEntries] of byKey) { + if (subEntries.some(([p]) => p.length === 0)) { + const leaf = subEntries.find(([p]) => p.length === 0)!; + parts.push(`${JSON.stringify(key)}: ${leaf[1]}`); } else { - inputEntries.set(path[0]!, ` ${JSON.stringify(path[0])}: ${expr}`); + parts.push(`${JSON.stringify(key)}: ${this.emitNestedObjectLiteral(subEntries)}`); } } + return `{ ${parts.join(", ")} }`; + } - // ToolDef fallback/coalesce wires (pull wires with fallbacks array) - for (const tw of toolDef.wires) { - 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; - 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})`; - } - } - if (path.length > 1) { - addNestedEntry(path, expr); + // ── Force statements ────────────────────────────────────────────────── + + private emitForceStatements(lines: string[], liveTools: Set): void { + for (const f of this.forces) { + const tk = `${f.module}:${f.type}:${f.field}:${f.instance ?? 1}`; + const tool = this.tools.get(tk); + if (!tool) continue; + if (f.catchError) { + lines.push(` try { await ${tool.varName}; } catch (_) {}`); } else { - inputEntries.set(path[0]!, ` ${JSON.stringify(path[0])}: ${expr}`); + lines.push(` await ${tool.varName};`); } } + } - // Emit nested ToolDef inputs as nested object literals - for (const [topKey, entries] of nestedInputEntries) { - if (!inputEntries.has(topKey)) { - inputEntries.set( - topKey, - ` ${JSON.stringify(topKey)}: ${emitNestedObjectLiteral(entries)}`, - ); - } + // ── Output generation ───────────────────────────────────────────────── + + private emitOutput(lines: string[], outputWires: ExtractedWire[]): void { + const rootWire = outputWires.find((w) => w.target.path.length === 0); + + if (rootWire && outputWires.length === 1 && this.spreads.length === 0) { + const expr = this.sourceChainToJs(rootWire.sources, rootWire.catch, " "); + lines.push(` return ${expr};`); + return; } - // Bridge wires override ToolDef wires - let spreadExprForToolDef: string | undefined; - for (const bw of bridgeWires) { - const path = bw.to.path; - if (path.length === 0) { - // Spread wire: ...sourceExpr — captures all fields from source - spreadExprForToolDef = this.wireToExpr(bw); - } else if (path.length >= 1) { - const key = path[0]!; - inputEntries.set( - key, - ` ${JSON.stringify(key)}: ${this.wireToExpr(bw)}`, - ); + lines.push(` const __output = {};`); + + for (const s of this.spreads) { + const expr = this.sourceChainToJs(s.sources, s.catch, " "); + if (s.pathPrefix.length > 0) { + const path = s.pathPrefix.map((p) => `[${JSON.stringify(p)}]`).join(""); + lines.push(` if (__output${path} == null || typeof __output${path} !== "object") __output${path} = {};`); + lines.push(` Object.assign(__output${path}, ${expr});`); + } else { + lines.push(` Object.assign(__output, ${expr});`); } } - const inputParts = [...inputEntries.values()]; + if (rootWire) { + const expr = this.sourceChainToJs(rootWire.sources, rootWire.catch, " "); + lines.push(` const __root = ${expr};`); + lines.push(` if (__root != null && typeof __root === "object") Object.assign(__output, __root);`); + } - let inputObj: string; - if (spreadExprForToolDef !== undefined) { - // Spread wire present: { ...spreadExpr, field1: ..., field2: ... } - const spreadEntry = ` ...${spreadExprForToolDef}`; - const allParts = [spreadEntry, ...inputParts]; - inputObj = `{\n${allParts.join(",\n")},\n }`; - } else { - inputObj = - inputParts.length > 0 ? `{\n${inputParts.join(",\n")},\n }` : "{}"; + const wiresByPath = new Map(); + for (const w of outputWires) { + if (w.target.path.length === 0) continue; + const key = w.target.path.join("."); + const arr = wiresByPath.get(key) ?? []; + arr.push(w); + wiresByPath.set(key, arr); } - if (toolDef.onError) { - // Wrap in try/catch for onError - lines.push(` let ${tool.varName};`); - lines.push(` try {`); - lines.push( - ` ${tool.varName} = ${this.syncAwareCall(fnName, inputObj, tool.trunkKey, tool.toolName)};`, - ); - lines.push(` } catch (_e) {`); - if ("value" in toolDef.onError) { - lines.push( - ` ${tool.varName} = JSON.parse(${JSON.stringify(toolDef.onError.value)});`, - ); - } else { - const fallbackExpr = this.resolveToolDepSource( - toolDef.onError.source, - toolDef, - ); - lines.push(` ${tool.varName} = ${fallbackExpr};`); + for (const [, wires] of wiresByPath) { + const path = wires[0]!.target.path; + for (let i = 1; i < path.length; i++) { + const parentPath = path.slice(0, i).map((p) => `[${JSON.stringify(p)}]`).join(""); + lines.push(` if (__output${parentPath} == null) __output${parentPath} = {};`); } - lines.push(` }`); - } else if (mode === "fire-and-forget") { - lines.push( - ` try { ${this.syncAwareCall(fnName, inputObj, tool.trunkKey, tool.toolName)}; } catch (_e) {}`, - ); - lines.push(` const ${tool.varName} = undefined;`); - } else if (mode === "catch-guarded") { - // Catch-guarded: store result AND the actual error so unguarded wires can re-throw. - lines.push(` let ${tool.varName}, ${tool.varName}_err;`); - lines.push( - ` try { ${tool.varName} = ${this.syncAwareCall(fnName, inputObj, tool.trunkKey, tool.toolName)}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; ${tool.varName}_err = _e; }`, - ); - } else { - const callExpr = this.syncAwareCall( - fnName, - inputObj, - tool.trunkKey, - tool.toolName, - ); - const pullingLoc = this.findPullingWireLoc(tool.trunkKey); - if (pullingLoc) { - lines.push( - ` const ${tool.varName} = ${this.wrapExprWithLoc(callExpr, pullingLoc)};`, - ); + const targetExpr = `__output${path.map((p) => `[${JSON.stringify(p)}]`).join("")}`; + + if (wires.length === 1) { + const expr = this.sourceChainToJs(wires[0]!.sources, wires[0]!.catch, " "); + lines.push(` ${targetExpr} = ${expr};`); } else { - lines.push( - ` const ${tool.varName} = await __wrapBridgeErrorAsync(async () => (${callExpr}), null);`, - ); + const varName = `__od_${path.join("_")}`; + const firstExpr = this.sourceChainToJs(wires[0]!.sources, wires[0]!.catch, " "); + lines.push(` let ${varName} = ${firstExpr};`); + for (let i = 1; i < wires.length; i++) { + const nextExpr = this.sourceChainToJs(wires[i]!.sources, wires[i]!.catch, " "); + lines.push(` if (${varName} == null) ${varName} = ${nextExpr};`); + } + lines.push(` ${targetExpr} = ${varName};`); } } + + lines.push(` return __output;`); } - /** - * Emit an inlined internal tool call (expressions, string interpolation). - * - * Instead of calling through the tools map, these are inlined as direct - * JavaScript operations — e.g., multiply becomes `Number(a) * Number(b)`. - */ - private emitInternalToolCall( - lines: string[], - tool: ToolInfo, - bridgeWires: Wire[], - ): void { - const fieldName = tool.toolName; - - // Collect input wires by their target path - const inputs = new Map(); - for (const w of bridgeWires) { - const path = w.to.path; - const key = path.join("."); - inputs.set(key, this.wireToExpr(w)); + // ── Expression compilation ──────────────────────────────────────────── + + exprToJs(expr: Expression, indent: string, elementVar?: string): string { + switch (expr.type) { + case "ref": return this.refToJs(expr, elementVar); + case "literal": return JSON.stringify(expr.value); + case "ternary": { + const cond = this.exprToJs(expr.cond, indent, elementVar); + const then = this.exprToJs(expr.then, indent, elementVar); + const elseE = this.exprToJs(expr.else, indent, elementVar); + return `(${cond} ? ${then} : ${elseE})`; + } + case "and": { + const left = expr.leftSafe ? this.safeExprToJs(expr.left, indent, elementVar) : this.exprToJs(expr.left, indent, elementVar); + const right = (expr.right.type === "literal" && expr.right.value === true) ? null + : (expr.rightSafe ? this.safeExprToJs(expr.right, indent, elementVar) : this.exprToJs(expr.right, indent, elementVar)); + if (right == null) return `Boolean(${left})`; + return `(${left} ? Boolean(${right}) : false)`; + } + case "or": { + const left = expr.leftSafe ? this.safeExprToJs(expr.left, indent, elementVar) : this.exprToJs(expr.left, indent, elementVar); + const right = (expr.right.type === "literal" && expr.right.value === true) ? null + : (expr.rightSafe ? this.safeExprToJs(expr.right, indent, elementVar) : this.exprToJs(expr.right, indent, elementVar)); + if (right == null) return `Boolean(${left})`; + return `(${left} ? true : Boolean(${right}))`; + } + case "control": return this.controlFlowToJs(expr.control, expr.loc); + case "binary": { + const left = this.exprToJs(expr.left, indent, elementVar); + const right = this.exprToJs(expr.right, indent, elementVar); + switch (expr.op) { + case "add": return `(Number(${left}) + Number(${right}))`; + case "sub": return `(Number(${left}) - Number(${right}))`; + case "mul": return `(Number(${left}) * Number(${right}))`; + case "div": return `(Number(${left}) / Number(${right}))`; + case "eq": return `(${left} === ${right})`; + case "neq": return `(${left} !== ${right})`; + case "gt": return `(Number(${left}) > Number(${right}))`; + case "gte": return `(Number(${left}) >= Number(${right}))`; + case "lt": return `(Number(${left}) < Number(${right}))`; + case "lte": return `(Number(${left}) <= Number(${right}))`; + } + break; + } + case "unary": return `(!${this.exprToJs(expr.operand, indent, elementVar)})`; + case "concat": { + const parts = expr.parts.map((p) => { + const js = this.exprToJs(p, indent, elementVar); + return `(${js} == null ? "" : String(${js}))`; + }); + return parts.join(" + "); + } + case "pipe": return this.pipeToJs(expr, indent, elementVar); + case "array": return this.arrayToJs(expr, indent, elementVar); } + return "undefined"; + } - let expr: string; + private safeExprToJs(expr: Expression, indent: string, elementVar?: string): string { + const inner = this.exprToJs(expr, indent, elementVar); + return `(() => { try { return ${inner}; } catch (_) { return undefined; } })()`; + } - // condAnd/condOr wires target the root path and already contain the full - // inlined expression (e.g. `(Boolean(left) && Boolean(right))`). - const rootExpr = inputs.get(""); - if (rootExpr !== undefined && (fieldName === "and" || fieldName === "or")) { - expr = rootExpr; - } else { - const a = inputs.get("a") ?? "undefined"; - const b = inputs.get("b") ?? "undefined"; + private refToJs(expr: Extract, elementVar?: string): string { + const ref = expr.ref; + const safe = expr.safe; - switch (fieldName) { - case "add": - expr = `(Number(${a}) + Number(${b}))`; - break; - case "subtract": - expr = `(Number(${a}) - Number(${b}))`; - break; - case "multiply": - expr = `(Number(${a}) * Number(${b}))`; - break; - case "divide": - expr = `(Number(${a}) / Number(${b}))`; - break; - case "eq": - expr = `(${a} === ${b})`; - break; - case "neq": - expr = `(${a} !== ${b})`; - break; - case "gt": - expr = `(Number(${a}) > Number(${b}))`; - break; - case "gte": - expr = `(Number(${a}) >= Number(${b}))`; - break; - case "lt": - expr = `(Number(${a}) < Number(${b}))`; - break; - case "lte": - expr = `(Number(${a}) <= Number(${b}))`; - break; - case "not": - expr = `(!${a})`; - break; - case "and": - expr = `(Boolean(${a}) && Boolean(${b}))`; - break; - case "or": - expr = `(Boolean(${a}) || Boolean(${b}))`; - break; - case "concat": { - const parts: string[] = []; - for (let i = 0; ; i++) { - const partExpr = inputs.get(`parts.${i}`); - if (partExpr === undefined) break; - parts.push(partExpr); - } - // concat returns { value: string } — same as the runtime internal tool - const concatParts = parts - .map((p) => `(${p} == null ? "" : String(${p}))`) - .join(" + "); - expr = `{ value: ${concatParts || '""'} }`; - break; - } - default: { - // Unknown internal tool — fall back to tools map call - const inputObj = this.buildObjectLiteral( - bridgeWires, - (w) => w.to.path, - 4, - ); - lines.push( - ` const ${tool.varName} = ${this.syncAwareCall(tool.toolName, inputObj, tool.trunkKey)};`, - ); - return; - } - } + if (ref.element && elementVar) { + if (ref.path.length === 0) return elementVar; + return this.emitPathAccess(elementVar, ref.path, ref.pathSafe, false); } - lines.push(` const ${tool.varName} = ${expr};`); - } + if (ref.module === SELF_MODULE && ref.type === this.bridge.type && ref.field === this.bridge.field && !ref.instance) { + if (ref.path.length === 0) return "input"; + return this.emitPathAccess("input", ref.path, ref.pathSafe, false); + } - /** - * Emit ToolDef-level dependency tool calls. - * - * When a ToolDef declares `with authService as auth`, the auth handle - * references a separate tool that must be called before the main tool. - * This method recursively resolves the dependency chain, emitting calls - * in dependency order. Independent deps are parallelized with Promise.all. - * - * Results are cached in `toolDepVars` so each dep is called at most once. - */ - private emitToolDeps(lines: string[], toolDef: ToolDef): void { - // Collect tool-kind handles that haven't been emitted yet - const pendingDeps: { handle: string; toolName: string }[] = []; - for (const h of toolDef.handles) { - if (h.kind === "tool" && !this.toolDepVars.has(h.name)) { - pendingDeps.push({ handle: h.handle, toolName: h.name }); - } + if (ref.module === SELF_MODULE && ref.type === "Context") { + if (ref.path.length === 0) return "context"; + return this.emitPathAccess("context", ref.path, ref.pathSafe, false); } - if (pendingDeps.length === 0) return; - - // Recursively emit transitive deps first - for (const pd of pendingDeps) { - const depToolDef = this.resolveToolDef(pd.toolName); - if (depToolDef) { - // Check for patterns the compiler can't handle in tool deps - if (depToolDef.onError) { - throw new BridgeCompilerIncompatibleError( - `${this.bridge.type}.${this.bridge.field}`, - "ToolDef on-error fallback in tool dependencies is not yet supported by the compiler.", - ); - } - for (const tw of depToolDef.wires) { - if ((isLit(tw) || isPull(tw)) && !isTern(tw)) { - if (tw.to.path.length > 1) { - throw new BridgeCompilerIncompatibleError( - `${this.bridge.type}.${this.bridge.field}`, - "Nested wire paths in tool dependencies are not yet supported by the compiler.", - ); - } - } - } - this.emitToolDeps(lines, depToolDef); + if (ref.module === SELF_MODULE && ref.type === "Const") { + const constName = ref.path[0]; + if (constName && this.constDefs.has(constName)) { + const raw = this.constDefs.get(constName)!; + try { + const parsed = JSON.parse(raw); + if (ref.path.length === 1) return JSON.stringify(parsed); + let val: any = parsed; + for (let i = 1; i < ref.path.length; i++) val = val?.[ref.path[i]!]; + return JSON.stringify(val); + } catch { return `JSON.parse(${JSON.stringify(raw)})`; } } + return "undefined"; } - // Now emit the current level deps — only the ones still not emitted - const toEmit = pendingDeps.filter( - (pd) => !this.toolDepVars.has(pd.toolName), - ); - if (toEmit.length === 0) return; - - // Build call expressions for each dep - const depCalls: { toolName: string; varName: string; callExpr: string }[] = - []; - for (const pd of toEmit) { - const depToolDef = this.resolveToolDef(pd.toolName); - if (!depToolDef) continue; - - const fnName = depToolDef.fn ?? pd.toolName; - const varName = `_td${++this.toolCounter}`; - - // Build input from the dep's ToolDef wires - const inputParts: string[] = []; - - // Constant wires - for (const tw of depToolDef.wires) { - if (isLit(tw) && !isTern(tw)) { - inputParts.push( - ` ${JSON.stringify(tw.to.path.join("."))}: ${emitCoerced(wVal(tw))}`, - ); - } - } + if (ref.module === "__local") { + const aliasVar = `__alias_${ref.field}`; + if (ref.path.length === 0) return aliasVar; + return this.emitPathAccess(aliasVar, ref.path, ref.pathSafe, false); + } - // Pull wires — resolved from the dep's own handles - for (const tw of depToolDef.wires) { - if (isPull(tw)) { - const source = this.resolveToolWireSource(tw, depToolDef); - inputParts.push( - ` ${JSON.stringify(tw.to.path.join("."))}: ${source}`, - ); - } + if (ref.instance != null) { + const tk = refTrunkKey(ref); + const tool = this.tools.get(tk); + if (!tool) return "undefined"; + const base = tool.varName; + if (safe) { + if (ref.path.length === 0) return base; + return this.emitSafePathAccess(base, ref.path, ref.pathSafe, ref.rootSafe); } + if (ref.path.length === 0) return base; + return this.emitPathAccess(base, ref.path, ref.pathSafe, false); + } - const inputObj = - inputParts.length > 0 ? `{\n${inputParts.join(",\n")},\n }` : "{}"; - - // Build call expression (without `const X = await`) - const callExpr = this.syncAwareCallNoAwait( - fnName, - inputObj, - undefined, - pd.toolName, - ); + return "undefined"; + } - depCalls.push({ toolName: pd.toolName, varName, callExpr }); - this.toolDepVars.set(pd.toolName, varName); + private emitPathAccess(base: string, path: string[], pathSafe?: boolean[], allowMissing?: boolean): string { + if (path.length === 0) return base; + if (path.length === 1 && !pathSafe?.[0]) { + return `__get(${base}, ${JSON.stringify(path[0])}, false, ${allowMissing ?? false})`; + } + if (pathSafe && pathSafe.some(Boolean)) { + return `__path(${base}, ${JSON.stringify(path)}, ${JSON.stringify(pathSafe)}, ${allowMissing ?? false})`; } + let expr = base; + for (const seg of path) expr = `__get(${expr}, ${JSON.stringify(seg)}, false, false)`; + return expr; + } - if (depCalls.length === 0) return; + private emitSafePathAccess(base: string, path: string[], pathSafe?: boolean[], rootSafe?: boolean): string { + return `(() => { try { return ${this.emitPathAccess(base, path, pathSafe, rootSafe)}; } catch (_) { return undefined; } })()`; + } - if (depCalls.length === 1) { - const dc = depCalls[0]!; - lines.push(` const ${dc.varName} = await ${dc.callExpr};`); - } else { - // Parallel: independent deps resolve concurrently - const varNames = depCalls.map((dc) => dc.varName).join(", "); - lines.push(` const [${varNames}] = await Promise.all([`); - for (const dc of depCalls) { - lines.push(` ${dc.callExpr},`); - } - lines.push(` ]);`); + private controlFlowToJs(ctrl: ControlFlowInstruction, loc?: SourceLocation): string { + const locStr = loc ? JSON.stringify(loc) : "undefined"; + switch (ctrl.kind) { + case "throw": return `(() => { throw new __BridgeRuntimeError(${JSON.stringify(ctrl.message)}, { bridgeLoc: ${locStr} }); })()`; + case "panic": return `(() => { const e = new __BridgePanicError(${JSON.stringify(ctrl.message)}); e.bridgeLoc = ${locStr}; throw e; })()`; + case "continue": return `({ __bridgeControl: "continue", levels: ${ctrl.levels ?? 1} })`; + case "break": return `({ __bridgeControl: "break", levels: ${ctrl.levels ?? 1} })`; } } - /** - * 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, toolDef: ToolDef): string { - const ref = wRef(wire); - // Match the ref against tool handles - const h = toolDef.handles.find((handle) => { - if (handle.kind === "context") { - return ( - ref.module === SELF_MODULE && - ref.type === "Context" && - ref.field === "context" - ); - } - if (handle.kind === "const") { - return ( - ref.module === SELF_MODULE && - ref.type === "Const" && - ref.field === "const" - ); - } - if (handle.kind === "tool") { - return ( - ref.module === SELF_MODULE && - ref.type === "Tools" && - ref.field === handle.name - ); - } - return false; - }); + private pipeToJs(expr: Extract, indent: string, elementVar?: string): string { + const sourceJs = this.exprToJs(expr.source, indent, elementVar); + const tk = this.findToolTkForHandle(expr.handle); + let fnVar: string, defName: string, fnNameStr: string; + if (tk) { + const tool = this.tools.get(tk)!; + fnVar = this.toolFnVar(tool.toolName); + defName = JSON.stringify(tool.handleName); + fnNameStr = JSON.stringify(tool.toolName); + } else { + fnVar = this.toolFnVar(expr.handle); + defName = JSON.stringify(expr.handle); + fnNameStr = JSON.stringify(expr.handle); + } + const pipeInput = expr.path ? this.emitNestedObjectLiteral([[expr.path, sourceJs]]) : `{ "in": ${sourceJs} }`; + return `await (${fnVar}?.bridge?.sync ? __callSync(${fnVar}, ${pipeInput}, ${defName}, ${fnNameStr}) : __call(${fnVar}, ${pipeInput}, ${defName}, ${fnNameStr}))`; + } - if (!h) return "undefined"; + private arrayToJs(expr: Extract, indent: string, parentElementVar?: string): string { + const sourceJs = this.exprToJs(expr.source, indent, parentElementVar); + const iterVar = `__el_${expr.iteratorName}`; + const hasElementTools = this.bodyHasElementTools(expr.body); + const hasControlFlow = this.bodyHasControlFlow(expr.body); - // Reconstruct the string-based source for resolveToolDepSource - const pathParts = ref.path.length > 0 ? "." + ref.path.join(".") : ""; - return this.resolveToolDepSource(h.handle + pathParts, toolDef); + if (hasElementTools || hasControlFlow) { + return this.arrayToJsAsync(sourceJs, iterVar, expr.body, indent, hasControlFlow); + } + return this.arrayToJsSync(sourceJs, iterVar, expr.body, indent); } - /** - * Resolve a NodeRef within a ToolDef context to a JS expression. - * Like resolveToolWireSource but also checks fork expression results. - */ - private resolveToolDefRef( - ref: NodeRef, - toolDef: ToolDef, - forkExprs: Map, - ): string { - const key = refTrunkKey(ref); - if (forkExprs.has(key)) { - let expr = forkExprs.get(key)!; - for (const p of ref.path) { - expr = `(${expr})[${JSON.stringify(p)}]`; + private bodyHasElementTools(body: Statement[]): boolean { + for (const s of body) { + if (s.kind === "with" && s.binding.kind === "tool") return true; + if (s.kind === "scope" && this.bodyHasElementTools(s.body)) return true; + if (s.kind === "wire" || s.kind === "alias" || s.kind === "spread") { + for (const src of s.sources) { if (this.exprHasPipeOrToolCall(src.expr)) return true; } + if (s.catch && "expr" in s.catch) { if (this.exprHasPipeOrToolCall(s.catch.expr)) return true; } } - return expr; } - // Delegate to resolveToolWireSource via a synthetic wire - return this.resolveToolWireSource( - { to: ref, sources: [{ expr: { type: "ref" as const, ref: ref } }] }, - toolDef, - ); + return false; } - /** - * Inline an internal fork tool operation as a JS expression. - * Used for ToolDef pipe forks — mirrors emitInternalToolCall logic. - */ - private inlineForkExpr( - forkField: string, - inputs: Map, - ): string { - const a = inputs.get("a") ?? "undefined"; - const b = inputs.get("b") ?? "undefined"; - switch (forkField) { - case "add": - return `(Number(${a}) + Number(${b}))`; - case "subtract": - return `(Number(${a}) - Number(${b}))`; - case "multiply": - return `(Number(${a}) * Number(${b}))`; - case "divide": - return `(Number(${a}) / Number(${b}))`; - case "eq": - return `(${a} === ${b})`; - case "neq": - return `(${a} !== ${b})`; - case "gt": - return `(Number(${a}) > Number(${b}))`; - case "gte": - return `(Number(${a}) >= Number(${b}))`; - case "lt": - return `(Number(${a}) < Number(${b}))`; - case "lte": - return `(Number(${a}) <= Number(${b}))`; - case "not": - return `(!${a})`; - case "and": - return `(Boolean(${a}) && Boolean(${b}))`; - case "or": - return `(Boolean(${a}) || Boolean(${b}))`; - case "concat": { - const parts: string[] = []; - for (let i = 0; ; i++) { - const partExpr = inputs.get(`parts.${i}`); - if (partExpr === undefined) break; - parts.push(partExpr); - } - const concatParts = parts - .map((p) => `(${p} == null ? "" : String(${p}))`) - .join(" + "); - return `{ value: ${concatParts || '""'} }`; - } - default: - return "undefined"; + private exprHasPipeOrToolCall(expr: Expression): boolean { + switch (expr.type) { + case "pipe": return true; + case "ternary": return this.exprHasPipeOrToolCall(expr.cond) || this.exprHasPipeOrToolCall(expr.then) || this.exprHasPipeOrToolCall(expr.else); + case "and": case "or": return this.exprHasPipeOrToolCall(expr.left) || this.exprHasPipeOrToolCall(expr.right); + case "binary": return this.exprHasPipeOrToolCall(expr.left) || this.exprHasPipeOrToolCall(expr.right); + case "unary": return this.exprHasPipeOrToolCall(expr.operand); + case "concat": return expr.parts.some((p) => this.exprHasPipeOrToolCall(p)); + case "array": return this.exprHasPipeOrToolCall(expr.source) || this.bodyHasElementTools(expr.body); + default: return false; } } - /** - * Resolve a ToolDef source reference (e.g. "ctx.apiKey") to a JS expression. - * Handles context, const, and tool dependencies. - */ - private resolveToolDepSource(source: string, toolDef: ToolDef): string { - const dotIdx = source.indexOf("."); - const handle = dotIdx === -1 ? source : source.substring(0, dotIdx); - const restPath = - dotIdx === -1 ? [] : source.substring(dotIdx + 1).split("."); - - const h = toolDef.handles.find((d) => d.handle === handle); - if (!h) return "undefined"; - - let baseExpr: string; - if (h.kind === "context") { - baseExpr = "context"; - } else if (h.kind === "const") { - // Resolve from the const definitions — inline parsed value - if (restPath.length > 0) { - const constName = restPath[0]!; - const val = this.constDefs.get(constName); - if (val != null) { - const base = emitParsedConst(val); - if (restPath.length === 1) return base; - const tail = restPath - .slice(1) - .map((p) => `[${JSON.stringify(p)}]`) - .join(""); - return `(${base})${tail}`; - } + private bodyHasControlFlow(body: Statement[]): boolean { + for (const s of body) { + if (s.kind === "wire" || s.kind === "alias" || s.kind === "spread") { + for (const src of s.sources) { if (this.exprHasControlFlow(src.expr)) return true; } } - return "undefined"; - } else if (h.kind === "tool") { - // Tool dependency — first check ToolDef-level dep vars (emitted by emitToolDeps), - // then fall back to bridge-level tool handles - const depVar = this.toolDepVars.get(h.name); - if (depVar) { - baseExpr = depVar; - } else { - const depToolInfo = this.findToolByName(h.name); - if (depToolInfo) { - baseExpr = depToolInfo.varName; - } else { - return "undefined"; - } - } - } else { - return "undefined"; + if (s.kind === "scope" && this.bodyHasControlFlow(s.body)) return true; } + return false; + } - if (restPath.length === 0) return baseExpr; - let expr = - baseExpr + restPath.map((p) => `[${JSON.stringify(p)}]`).join(""); - - // If reading from a tool dep, check if the dep has a constant wire for - // this path — if so, add a ?? fallback so the constant is visible even - // though the tool function may not have returned it. - if (h.kind === "tool" && restPath.length > 0) { - const depToolDef = this.resolveToolDef(h.name); - if (depToolDef) { - const pathKey = restPath.join("."); - for (const tw of depToolDef.wires) { - if (isLit(tw) && !isTern(tw) && tw.to.path.join(".") === pathKey) { - expr = `(${expr} ?? ${emitCoerced(wVal(tw))})`; - break; - } - } - } + private exprHasControlFlow(expr: Expression): boolean { + switch (expr.type) { + case "control": return expr.control.kind === "continue" || expr.control.kind === "break"; + case "ternary": return this.exprHasControlFlow(expr.cond) || this.exprHasControlFlow(expr.then) || this.exprHasControlFlow(expr.else); + case "and": case "or": return this.exprHasControlFlow(expr.left) || this.exprHasControlFlow(expr.right); + case "binary": return this.exprHasControlFlow(expr.left) || this.exprHasControlFlow(expr.right); + case "unary": return this.exprHasControlFlow(expr.operand); + case "concat": return expr.parts.some((p) => this.exprHasControlFlow(p)); + default: return false; } + } - return expr; + private arrayToJsSync(sourceJs: string, iterVar: string, body: Statement[], indent: string): string { + const bodyExpr = this.arrayBodyToJs(body, iterVar, indent + " "); + return `(() => { const __src = ${sourceJs}; return __src == null ? null : __src.map((${iterVar}) => { ${bodyExpr} }); })()`; } - /** Find a tool info by tool name. */ - private findToolByName(name: string): ToolInfo | undefined { - for (const [, info] of this.tools) { - if (info.toolName === name) return info; + private arrayToJsAsync(sourceJs: string, iterVar: string, body: Statement[], indent: string, hasControlFlow: boolean): string { + const bodyExpr = this.arrayBodyToJs(body, iterVar, indent + " "); + if (hasControlFlow) { + return `await (async () => { const __src = ${sourceJs}; if (__src == null) return null; const __result = []; for (const ${iterVar} of __src) { ${bodyExpr.replace("return {", "const __elem = {")} if (__isLoopCtrl(__elem)) { if (__elem.__bridgeControl === "continue") { if (__elem.levels <= 1) continue; return __nextLoopCtrl(__elem); } if (__elem.__bridgeControl === "break") { if (__elem.levels <= 1) break; return __nextLoopCtrl(__elem); } } __result.push(__elem); } return __result; })()`; } - return undefined; + return `await (async () => { const __src = ${sourceJs}; if (__src == null) return null; return Promise.all(__src.map(async (${iterVar}) => { ${bodyExpr} })); })()`; } - /** - * Resolve a ToolDef by name, merging the extends chain. - * Mirrors the runtime's resolveToolDefByName logic. - */ - private resolveToolDef(name: string): ToolDef | undefined { - const base = this.toolDefs.find((t) => t.name === name); - if (!base) return undefined; - - // Build extends chain: root → ... → leaf - const chain: ToolDef[] = [base]; - let current = base; - while (current.extends) { - const parent = this.toolDefs.find((t) => t.name === current.extends); - if (!parent) break; - chain.unshift(parent); - current = parent; - } - - // Merge: root provides base, each child overrides - const merged: ToolDef = { - kind: "tool", - name, - fn: chain[0]!.fn, - handles: [], - wires: [], - }; - - for (const def of chain) { - for (const h of def.handles) { - if (!merged.handles.some((mh) => mh.handle === h.handle)) { - merged.handles.push(h); - } - } - for (const wire of def.wires) { - const wireKey = wire.to.path.join("."); - const idx = merged.wires.findIndex( - (w) => w.to.path.join(".") === wireKey, - ); - if (idx >= 0) merged.wires[idx] = wire; - else merged.wires.push(wire); - } - if (def.onError) merged.onError = def.onError; - // Merge pipeHandles — child overrides parent by key - if (def.pipeHandles) { - if (!merged.pipeHandles) merged.pipeHandles = []; - for (const ph of def.pipeHandles) { - if (!merged.pipeHandles.some((mph) => mph.key === ph.key)) { - merged.pipeHandles.push(ph); - } - } - } - } - - return merged; - } - - // ── Output generation ──────────────────────────────────────────────────── - - private emitOutput(lines: string[], outputWires: Wire[]): void { - if (outputWires.length === 0) { - // Match the runtime's error when no wires target the output - const { type, field } = this.bridge; - const hasForce = this.bridge.forces && this.bridge.forces.length > 0; - if (!hasForce) { - lines.push( - ` throw new Error(${JSON.stringify(`Bridge "${type}.${field}" has no output wires. Ensure at least one wire targets the output (e.g. \`o.field <- ...\`).`)});`, - ); - } else { - lines.push(" return {};"); - } - return; - } - - // Detect array iterators - const arrayIterators = this.bridge.arrayIterators ?? {}; - const isRootArray = "" in arrayIterators; - - // Separate root wires into passthrough vs spread - const rootWires = outputWires.filter((w) => w.to.path.length === 0); - const spreadRootWires = rootWires.filter( - (w) => isPull(w) && w.spread && w.spread, - ); - const passthroughRootWire = rootWires.find( - (w) => !(isPull(w) && w.spread && w.spread), - ); - - // Passthrough (non-spread root wire) — return directly - if (passthroughRootWire && !isRootArray) { - lines.push(` return ${this.wireToExpr(passthroughRootWire)};`); - return; - } - - // Check for root passthrough (wire with empty path) — but not if it's a root array source - const rootWire = rootWires[0]; // for backwards compat with array handling below - - // Handle root array output (o <- src.items[] as item { ... }) - if (isRootArray && rootWire) { - const elemWires = outputWires.filter( - (w) => w !== rootWire && w.to.path.length > 0, - ); - let arrayExpr = this.wireToExpr(rootWire); - // Check for catch control on root wire (e.g., `catch continue` returns []) - const rootCatchCtrl = hasCatchControl(rootWire) - ? catchControl(rootWire) - : undefined; - if ( - rootCatchCtrl && - (rootCatchCtrl.kind === "continue" || rootCatchCtrl.kind === "break") - ) { - arrayExpr = `await (async () => { try { return ${arrayExpr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; return null; } })()`; - } - // Only check control flow on direct element wires, not sub-array element wires - const directElemWires = elemWires.filter((w) => w.to.path.length === 1); - const currentScopeElemWires = this.filterCurrentElementWires( - elemWires, - arrayIterators, - ); - const cf = detectControlFlow(directElemWires); - const anyCf = detectControlFlow(elemWires); - const requiresLabeledLoop = !cf && !!anyCf && anyCf.levels > 1; - // Check if any element wire generates `await` (element-scoped tools or catch fallbacks) - const needsAsync = elemWires.some((w) => this.wireNeedsAwait(w)); - - if (needsAsync) { - // Check if async is only from element-scoped tools (no catch fallbacks). - // If so, generate a dual sync/async path with a runtime check. - const canDualPath = - !cf && !requiresLabeledLoop && this.asyncOnlyFromTools(elemWires); - const toolRefs = canDualPath - ? this.collectElementToolRefs(currentScopeElemWires) - : []; - const hasDualPath = canDualPath && toolRefs.length > 0; - - if (hasDualPath) { - // ── Dual path: sync .map() when all element tools are sync ── - const syncCheck = toolRefs - .map((r) => `${r}.bridge?.sync`) - .join(" && "); - - // Sync branch — .map() with __callSync - const syncPreamble: string[] = []; - this.elementLocalVars.clear(); - this.collectElementPreamble( - currentScopeElemWires, - "_el0", - syncPreamble, - true, - ); - const syncBody = this.buildElementBody( - elemWires, - arrayIterators, - 0, - 6, - ); - lines.push(` if (${syncCheck}) {`); - if (syncPreamble.length > 0) { - lines.push( - ` return (${arrayExpr} ?? []).map((_el0) => { ${syncPreamble.join(" ")} return ${syncBody}; });`, - ); - } else { - lines.push( - ` return (${arrayExpr} ?? []).map((_el0) => (${syncBody}));`, - ); - } - lines.push(` }`); - this.elementLocalVars.clear(); - } - - // Async branch — Promise.all over async element callbacks so batched - // tool calls can coalesce before the first microtask flush. Control - // flow still requires an explicit loop. - const preambleLines: string[] = []; - this.elementLocalVars.clear(); - this.collectElementPreamble( - currentScopeElemWires, - "_el0", - preambleLines, - ); - - if ( - cf?.kind === "break" || - cf?.kind === "continue" || - requiresLabeledLoop - ) { - const body = cf - ? this.buildElementBodyWithControlFlow( - elemWires, - arrayIterators, - 0, - 4, - cf.kind === "continue" ? "for-continue" : "break", - ) - : ` _result.push(${this.buildElementBody(elemWires, arrayIterators, 0, 4)});`; - - lines.push(` const _result = [];`); - lines.push(` __loop0: for (const _el0 of (${arrayExpr} ?? [])) {`); - lines.push(` try {`); - for (const pl of preambleLines) { - lines.push(` ${pl}`); - } - lines.push(` ${body.trimStart()}`); - lines.push(` } catch (_ctrl) {`); - lines.push( - ` if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; }`, - ); - lines.push(` throw _ctrl;`); - lines.push(` }`); - lines.push(` }`); - lines.push(` return _result;`); - } else { - lines.push( - ` return await Promise.all((${arrayExpr} ?? []).map(async (_el0) => {`, - ); - for (const pl of preambleLines) { - lines.push(` ${pl}`); - } - lines.push( - ` return ${this.buildElementBody(elemWires, arrayIterators, 0, 4)};`, - ); - lines.push(` }));`); - } - this.elementLocalVars.clear(); - } else if (cf?.kind === "continue" && cf.levels === 1) { - // Use flatMap — skip elements that trigger continue (sync only) - const body = this.buildElementBodyWithControlFlow( - elemWires, - arrayIterators, - 0, - 4, - "continue", - ); - lines.push(` return (${arrayExpr} ?? []).flatMap((_el0) => {`); - lines.push(body); - lines.push(` });`); - } else if ( - cf?.kind === "break" || - cf?.kind === "continue" || - requiresLabeledLoop - ) { - // Use an explicit loop for: - // - direct break/continue control - // - nested multilevel control (e.g. break 2 / continue 2) that must - // escape from sub-array IIFEs through throw/catch propagation. - // Use a loop with early break (sync) - const body = cf - ? this.buildElementBodyWithControlFlow( - elemWires, - arrayIterators, - 0, - 4, - cf.kind === "continue" ? "for-continue" : "break", - ) - : ` _result.push(${this.buildElementBody(elemWires, arrayIterators, 0, 4)});`; - lines.push(` const _result = [];`); - lines.push(` __loop0: for (const _el0 of (${arrayExpr} ?? [])) {`); - lines.push(` try {`); - lines.push(` ${body.trimStart()}`); - lines.push(` } catch (_ctrl) {`); - lines.push( - ` if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; }`, - ); - lines.push(` throw _ctrl;`); - lines.push(` }`); - lines.push(` }`); - lines.push(` return _result;`); - } else { - const body = this.buildElementBody(elemWires, arrayIterators, 0, 4); - lines.push(` return (${arrayExpr} ?? []).map((_el0) => (${body}));`); - } - return; - } - - const arrayFields = new Set(Object.keys(arrayIterators)); - - // Separate element wires from scalar wires - const elementWires = new Map(); - const scalarWires: Wire[] = []; - const arraySourceWires = new Map(); - - for (const w of outputWires) { - const topField = w.to.path[0]!; - const isElementWire = - (isPull(w) && - (wRef(w).element || - w.to.element || - 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 && (isLit(w) || isTern(w))) || - // Cond wires targeting a field inside an array mapping are element wires - (isTern(w) && arrayFields.has(topField) && w.to.path.length > 1) || - // Const wires targeting a field inside an array mapping are element wires - (isLit(w) && arrayFields.has(topField) && w.to.path.length > 1); - if (isElementWire) { - // Element wire — belongs to an array mapping - const arr = elementWires.get(topField) ?? []; - arr.push(w); - elementWires.set(topField, arr); - } else if (arrayFields.has(topField) && w.to.path.length === 1) { - // Root wire for an array field - arraySourceWires.set(topField, w); - } else if (isPull(w) && w.spread && w.spread && w.to.path.length === 0) { - // Spread root wire — handled separately via spreadRootWires - } else { - scalarWires.push(w); - } - } - - // Build a nested tree from scalar wires using their full output path - interface TreeNode { - expr?: string; - terminal?: boolean; - spreadExprs?: string[]; - children: Map; - } - const tree: TreeNode = { children: new Map() }; - - // First pass: handle nested spread wires (spread with path.length > 0) - const nestedSpreadWires = scalarWires.filter( - (w) => isPull(w) && w.spread && w.spread && w.to.path.length > 0, - ); - const normalScalarWires = scalarWires.filter( - (w) => !(isPull(w) && w.spread && w.spread), - ); - - // Add nested spread expressions to tree nodes - for (const w of nestedSpreadWires) { - const path = w.to.path; - let current = tree; - // Navigate to parent of the target - for (let i = 0; i < path.length - 1; i++) { - const seg = path[i]!; - if (!current.children.has(seg)) { - current.children.set(seg, { children: new Map() }); - } - current = current.children.get(seg)!; - } - const lastSeg = path[path.length - 1]!; - if (!current.children.has(lastSeg)) { - current.children.set(lastSeg, { children: new Map() }); - } - const node = current.children.get(lastSeg)!; - // Add spread expression to this node - if (!node.spreadExprs) node.spreadExprs = []; - node.spreadExprs.push(this.wireToExpr(w)); - } - - for (const w of normalScalarWires) { - const path = w.to.path; - let current = tree; - for (let i = 0; i < path.length - 1; i++) { - const seg = path[i]!; - if (!current.children.has(seg)) { - current.children.set(seg, { children: new Map() }); - } - current = current.children.get(seg)!; - } - const lastSeg = path[path.length - 1]!; - if (!current.children.has(lastSeg)) { - current.children.set(lastSeg, { children: new Map() }); - } - const node = current.children.get(lastSeg)!; - this.mergeOverdefinedExpr(node, w); - } + private arrayBodyToJs(body: Statement[], elementVar: string, indent: string): string { + const elementOutputs: { path: string[]; expr: string }[] = []; + const parts: string[] = []; - // Emit array-mapped fields into the tree as well - for (const [arrayField] of Object.entries(arrayIterators)) { - if (arrayField === "") continue; // root array handled above - const sourceW = arraySourceWires.get(arrayField); - const elemWires = elementWires.get(arrayField) ?? []; - if (!sourceW || elemWires.length === 0) continue; - - // Strip the array field prefix from element wire paths - const shifted: Wire[] = elemWires.map((w) => ({ - ...w, - to: { ...w.to, path: w.to.path.slice(1) }, - })); - - const arrayExpr = this.wireToExpr(sourceW); - // Only check control flow on direct element wires (not sub-array element wires) - const directShifted = shifted.filter((w) => w.to.path.length === 1); - const currentScopeShifted = this.filterCurrentElementWires( - shifted, - this.relativeArrayIterators(arrayIterators, arrayField), - ); - const cf = detectControlFlow(directShifted); - const anyCf = detectControlFlow(shifted); - const requiresLabeledLoop = !cf && !!anyCf && anyCf.levels > 1; - // Check if any element wire generates `await` (element-scoped tools or catch fallbacks) - const needsAsync = shifted.some((w) => this.wireNeedsAwait(w)); - let mapExpr: string; - if (needsAsync) { - // Check if we can generate a dual sync/async path - const canDualPath = - !cf && !requiresLabeledLoop && this.asyncOnlyFromTools(shifted); - const toolRefs = canDualPath - ? this.collectElementToolRefs(currentScopeShifted) - : []; - const hasDualPath = canDualPath && toolRefs.length > 0; - - if (hasDualPath) { - // Sync branch — .map() with __callSync - const syncCheck = toolRefs - .map((r) => `${r}.bridge?.sync`) - .join(" && "); - const syncPreamble: string[] = []; - this.elementLocalVars.clear(); - this.collectElementPreamble( - currentScopeShifted, - "_el0", - syncPreamble, - true, - ); - const shiftedIterators = this.relativeArrayIterators( - arrayIterators, - arrayField, - ); - const syncMapExpr = - syncPreamble.length > 0 - ? `(${arrayExpr})?.map((_el0) => { ${syncPreamble.join(" ")} return ${this.buildElementBody(shifted, shiftedIterators, 0, 6)}; }) ?? null` - : `(${arrayExpr})?.map((_el0) => (${this.buildElementBody(shifted, shiftedIterators, 0, 6)})) ?? null`; - this.elementLocalVars.clear(); - - // Async branch — for...of inside an async IIFE - const preambleLines: string[] = []; - this.elementLocalVars.clear(); - this.collectElementPreamble( - currentScopeShifted, - "_el0", - preambleLines, - ); - const preamble = preambleLines.map((l) => ` ${l}`).join("\n"); - const asyncExpr = `await ((async (__s) => Array.isArray(__s) ? Promise.all(__s.map(async (_el0) => {\n${preamble}${preamble ? "\n" : ""} return ${this.buildElementBody(shifted, shiftedIterators, 0, 8)};\n })) : null)(${arrayExpr}))`; - this.elementLocalVars.clear(); - - mapExpr = `(${syncCheck}) ? ${syncMapExpr} : ${asyncExpr}`; - } else { - // Standard async path — Promise.all over async element callbacks so - // batched tools can queue together before the first flush. Control - // flow still requires an explicit loop. - const preambleLines: string[] = []; - this.elementLocalVars.clear(); - this.collectElementPreamble( - currentScopeShifted, - "_el0", - preambleLines, - ); - const shiftedIterators = this.relativeArrayIterators( - arrayIterators, - arrayField, - ); - - const preamble = preambleLines.map((l) => ` ${l}`).join("\n"); - if ( - cf?.kind === "break" || - cf?.kind === "continue" || - requiresLabeledLoop - ) { - const asyncBody = cf - ? this.buildElementBodyWithControlFlow( - shifted, - shiftedIterators, - 0, - 8, - cf.kind === "continue" ? "for-continue" : "break", - ) - : ` _result.push(${this.buildElementBody(shifted, shiftedIterators, 0, 8)});`; - mapExpr = `await (async () => { const _src = ${arrayExpr}; if (_src == null) return null; const _result = []; __loop0: for (const _el0 of _src) {\n try {\n${preamble}\n${asyncBody}\n } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n } return _result; })()`; - } else { - mapExpr = `await ((async (__s) => Array.isArray(__s) ? Promise.all(__s.map(async (_el0) => {\n${preamble}${preamble ? "\n" : ""} return ${this.buildElementBody(shifted, shiftedIterators, 0, 8)};\n })) : null)(${arrayExpr}))`; - } - this.elementLocalVars.clear(); + for (const stmt of body) { + if (stmt.kind === "with") { + if (stmt.binding.kind === "tool") { + const b = stmt.binding; + const resolved = this.resolveToolDef(b.name); + const fnName = resolved.fn ?? b.name; + if (!this.toolFnVars.has(fnName)) this.toolFnVars.set(fnName, `__fn${++this.toolFnVarCounter}`); } - } else if (cf?.kind === "continue" && cf.levels === 1) { - const cfBody = this.buildElementBodyWithControlFlow( - shifted, - this.relativeArrayIterators(arrayIterators, arrayField), - 0, - 6, - "continue", - ); - mapExpr = `((__s) => Array.isArray(__s) ? __s.flatMap((_el0) => {\n${cfBody}\n }) ?? null : null)(${arrayExpr})`; - } else if ( - cf?.kind === "break" || - cf?.kind === "continue" || - requiresLabeledLoop - ) { - // Same rationale as root array handling above: nested multilevel - // control requires for-loop + throw/catch propagation instead of map. - const loopBody = cf - ? this.buildElementBodyWithControlFlow( - shifted, - this.relativeArrayIterators(arrayIterators, arrayField), - 0, - 8, - cf.kind === "continue" ? "for-continue" : "break", - ) - : ` _result.push(${this.buildElementBody(shifted, this.relativeArrayIterators(arrayIterators, arrayField), 0, 8)});`; - mapExpr = `(() => { const _src = ${arrayExpr}; if (!Array.isArray(_src)) return null; const _result = []; __loop0: for (const _el0 of _src) {\n try {\n${loopBody}\n } catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n } return _result; })()`; - } else { - const body = this.buildElementBody( - shifted, - this.relativeArrayIterators(arrayIterators, arrayField), - 0, - 6, - ); - mapExpr = `((__s) => Array.isArray(__s) ? __s.map((_el0) => (${body})) ?? null : null)(${arrayExpr})`; - } - - if (!tree.children.has(arrayField)) { - tree.children.set(arrayField, { children: new Map() }); - } - tree.children.get(arrayField)!.expr = mapExpr; - } - - // Serialize the tree to a return statement - // Include spread expressions at the start if present - const spreadExprs = spreadRootWires.map((w) => this.wireToExpr(w)); - const objStr = this.serializeOutputTree(tree, 4, spreadExprs); - lines.push(` return ${objStr};`); - } - - /** Serialize an output tree node into a JS object literal. */ - private serializeOutputTree( - node: { - children: Map }>; - }, - indent: number, - spreadExprs?: string[], - ): string { - const pad = " ".repeat(indent); - const entries: string[] = []; - - // Add spread expressions first (they come before field overrides) - if (spreadExprs) { - for (const expr of spreadExprs) { - entries.push(`${pad}...${expr}`); - } - } - - for (const [key, child] of node.children) { - // Check if child has spread expressions - const childSpreadExprs = (child as { spreadExprs?: string[] }) - .spreadExprs; - - if ( - child.expr != null && - child.children.size === 0 && - !childSpreadExprs - ) { - // Simple leaf with just an expression - entries.push(`${pad}${JSON.stringify(key)}: ${child.expr}`); - } else if (childSpreadExprs || child.children.size > 0) { - // Nested object: may have spreads, children, or both - const nested = this.serializeOutputTree( - child, - indent + 2, - childSpreadExprs, - ); - entries.push(`${pad}${JSON.stringify(key)}: ${nested}`); - } else { - // Has both expr and children — use expr (children override handled elsewhere) - entries.push( - `${pad}${JSON.stringify(key)}: ${child.expr ?? "undefined"}`, - ); + continue; } - } - - const innerPad = " ".repeat(indent - 2); - return `{\n${entries.join(",\n")},\n${innerPad}}`; - } - - private reorderOverdefinedOutputWires(outputWires: Wire[]): Wire[] { - if (outputWires.length < 2) return outputWires; - - const groups = new Map(); - for (const wire of outputWires) { - const pathKey = wire.to.path.join("."); - const group = groups.get(pathKey) ?? []; - group.push(wire); - groups.set(pathKey, group); - } - - const emitted = new Set(); - const reordered: Wire[] = []; - let changed = false; - - for (const wire of outputWires) { - const pathKey = wire.to.path.join("."); - if (emitted.has(pathKey)) continue; - emitted.add(pathKey); - - const group = groups.get(pathKey)!; - if (group.length < 2) { - reordered.push(...group); + if (stmt.kind === "alias") { + const aliasExpr = this.sourceChainToJs(stmt.sources, stmt.catch, indent, elementVar); + parts.push(`const __alias_${stmt.name} = ${aliasExpr};`); continue; } - - const ranked = group.map((candidate, index) => ({ - wire: candidate, - index, - cost: this.classifyOverdefinitionWire(candidate), - })); - ranked.sort((left, right) => { - if (left.cost !== right.cost) { - changed = true; - return left.cost - right.cost; - } - return left.index - right.index; - }); - reordered.push(...ranked.map((entry) => entry.wire)); - } - - return changed ? reordered : outputWires; - } - - private classifyOverdefinitionWire( - wire: Wire, - visited = new Set(), - ): number { - // Optimistic cost — cost of the first source only. - return this.computeExprCost(wire.sources[0]!.expr, visited); - } - - /** - * Pessimistic wire cost — sum of all source expression costs plus catch. - * Represents worst-case cost when all fallback sources fire. - */ - private computeWireCost(wire: Wire, visited: Set): number { - let cost = 0; - for (const source of wire.sources) { - cost += this.computeExprCost(source.expr, visited); - } - if (catchRef(wire)) { - cost += this.computeRefCost(catchRef(wire)!, visited); - } - return cost; - } - - private computeExprCost(expr: Expression, visited: Set): number { - switch (expr.type) { - case "literal": - case "control": - return 0; - case "ref": - return this.computeRefCost(expr.ref, visited); - case "ternary": - return Math.max( - this.computeExprCost(expr.cond, visited), - this.computeExprCost(expr.then, visited), - this.computeExprCost(expr.else, visited), - ); - case "and": - case "or": - return Math.max( - this.computeExprCost(expr.left, visited), - this.computeExprCost(expr.right, visited), - ); - case "array": - return this.computeExprCost(expr.source, visited); - case "pipe": - return this.computeExprCost(expr.source, visited); - case "binary": - return Math.max( - this.computeExprCost(expr.left, visited), - this.computeExprCost(expr.right, visited), - ); - case "unary": - return this.computeExprCost(expr.operand, visited); - case "concat": { - let max = 0; - for (const part of expr.parts) { - max = Math.max(max, this.computeExprCost(part, visited)); + if (stmt.kind === "wire") { + if (stmt.target.element) { + const expr = this.sourceChainToJs(stmt.sources, stmt.catch, indent, elementVar); + elementOutputs.push({ path: stmt.target.path, expr }); } - return max; - } - } - } - - private computeRefCost(ref: NodeRef, visited: Set): number { - if (ref.element) return 0; - // Self-module input/context/const — free - if ( - ref.module === SELF_MODULE && - ((ref.type === this.bridge.type && ref.field === this.bridge.field) || - (ref.type === "Context" && ref.field === "context") || - (ref.type === "Const" && ref.field === "const")) - ) { - return 0; - } - - const key = refTrunkKey(ref); - if (visited.has(key)) return Infinity; - visited.add(key); - - // Define — recursive, cheapest incoming wire wins - if (ref.module.startsWith("__define_")) { - const incoming = this.bridge.wires.filter( - (wire) => refTrunkKey(wire.to) === key, - ); - let best = Infinity; - for (const wire of incoming) { - best = Math.min(best, this.computeWireCost(wire, visited)); + continue; } - return best === Infinity ? 2 : best; } - // Local alias — recursive, cheapest incoming wire wins - if (ref.module === "__local") { - const incoming = this.bridge.wires.filter( - (wire) => refTrunkKey(wire.to) === key, - ); - let best = Infinity; - for (const wire of incoming) { - best = Math.min(best, this.computeWireCost(wire, visited)); - } - return best === Infinity ? 2 : best; - } + if (elementOutputs.length === 0) return parts.join(" ") + " return {};"; - // External tool — compiler has no metadata, default to async cost - return 2; - } + const rootOutput = elementOutputs.find((o) => o.path.length === 0); + if (rootOutput && elementOutputs.length === 1) return parts.join(" ") + " return " + rootOutput.expr + ";"; - /** - * Build the body of a `.map()` callback from element wires. - * - * Handles nested array iterators: if an element wire targets a field that - * is itself an array iterator, a nested `.map()` is generated. - */ - private buildElementBody( - elemWires: Wire[], - arrayIterators: Record, - depth: number, - indent: number, - ): string { - const elVar = `_el${depth}`; - - // Separate into scalar element wires and sub-array source/element wires - interface TreeNode { - expr?: string; - children: Map; - } - const tree: TreeNode = { children: new Map() }; - - // Group wires by whether they target a sub-array field - const subArraySources = new Map(); // field → source wire - const subArrayElements = new Map(); // field → element wires - - for (const ew of elemWires) { - const topField = ew.to.path[0]!; - - if ( - topField in arrayIterators && - ew.to.path.length === 1 && - !subArraySources.has(topField) - ) { - // This is the source wire for a sub-array (e.g., .legs <- c.sections[]) - subArraySources.set(topField, ew); - } else if (topField in arrayIterators && ew.to.path.length > 1) { - // This is an element wire for a sub-array (e.g., .legs.trainName <- s.name) - const arr = subArrayElements.get(topField) ?? []; - arr.push(ew); - subArrayElements.set(topField, arr); - } else { - // Regular scalar element wire — add to tree using full path - const path = ew.to.path; - let current = tree; - for (let i = 0; i < path.length - 1; i++) { - const seg = path[i]!; - if (!current.children.has(seg)) { - current.children.set(seg, { children: new Map() }); - } - current = current.children.get(seg)!; - } - const lastSeg = path[path.length - 1]!; - if (!current.children.has(lastSeg)) { - current.children.set(lastSeg, { children: new Map() }); - } - current.children.get(lastSeg)!.expr = this.elementWireToExpr(ew, elVar); + let hasCtrl = false; + for (const stmt of body) { + if (stmt.kind === "wire" && stmt.target.element) { + for (const src of stmt.sources) { if (this.exprHasControlFlow(src.expr)) hasCtrl = true; } } } - // Handle sub-array fields - for (const [field, sourceW] of subArraySources) { - const innerElems = subArrayElements.get(field) ?? []; - if (innerElems.length === 0) continue; - - // Shift inner element paths: remove the first segment (the sub-array field name) - const shifted: Wire[] = innerElems.map((w) => ({ - ...w, - to: { ...w.to, path: w.to.path.slice(1) }, - })); - - const srcExpr = this.elementWireToExpr(sourceW, elVar); - const innerElVar = `_el${depth + 1}`; - const innerArrayIterators = this.relativeArrayIterators( - arrayIterators, - field, - ); - const innerCf = detectControlFlow(shifted); - // Check if inner loop needs async (element-scoped tools or catch fallbacks) - const innerNeedsAsync = shifted.some((w) => this.wireNeedsAwait(w)); - let mapExpr: string; - if (innerNeedsAsync) { - mapExpr = this.withElementLocalVarScope(() => { - const innerCurrentScope = this.filterCurrentElementWires( - shifted, - innerArrayIterators, - ); - const innerPreambleLines: string[] = []; - this.collectElementPreamble( - innerCurrentScope, - innerElVar, - innerPreambleLines, - ); - const innerBody = innerCf - ? this.buildElementBodyWithControlFlow( - shifted, - innerArrayIterators, - depth + 1, - indent + 4, - innerCf.kind === "continue" ? "for-continue" : "break", - ) - : `${" ".repeat(indent + 4)}_result.push(${this.buildElementBody(shifted, innerArrayIterators, depth + 1, indent + 4)});`; - const innerPreamble = innerPreambleLines - .map((line) => `${" ".repeat(indent + 4)}${line}`) - .join("\n"); - return `await (async () => { const _src = ${srcExpr}; if (!Array.isArray(_src)) return null; const _result = []; __loop${depth + 1}: for (const ${innerElVar} of _src) {\n${" ".repeat(indent + 4)}try {\n${innerPreamble}${innerPreamble ? "\n" : ""}${innerBody}\n${" ".repeat(indent + 4)}} catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n${" ".repeat(indent + 2)}} return _result; })()`; - }); - } else if (innerCf?.kind === "continue" && innerCf.levels === 1) { - const cfBody = this.buildElementBodyWithControlFlow( - shifted, - innerArrayIterators, - depth + 1, - indent + 2, - "continue", - ); - mapExpr = `((__s) => Array.isArray(__s) ? __s.flatMap((${innerElVar}) => {\n${cfBody}\n${" ".repeat(indent + 2)}}) ?? null : null)(${srcExpr})`; - } else if (innerCf?.kind === "break" || innerCf?.kind === "continue") { - const cfBody = this.buildElementBodyWithControlFlow( - shifted, - innerArrayIterators, - depth + 1, - indent + 4, - innerCf.kind === "continue" ? "for-continue" : "break", - ); - mapExpr = `(() => { const _src = ${srcExpr}; if (!Array.isArray(_src)) return null; const _result = []; __loop${depth + 1}: for (const ${innerElVar} of _src) {\n${" ".repeat(indent + 4)}try {\n${cfBody}\n${" ".repeat(indent + 4)}} catch (_ctrl) { if (__isLoopCtrl(_ctrl)) { if (_ctrl.levels > 1) throw __nextLoopCtrl(_ctrl); if (_ctrl.__bridgeControl === "break") break; continue; } throw _ctrl; }\n${" ".repeat(indent + 2)}} return _result; })()`; - } else { - const innerBody = this.buildElementBody( - shifted, - innerArrayIterators, - depth + 1, - indent + 2, - ); - mapExpr = `((__s) => Array.isArray(__s) ? __s.map((${innerElVar}) => (${innerBody})) ?? null : null)(${srcExpr})`; - } + const objEntries = elementOutputs.filter((o) => o.path.length > 0) + .map((o) => `${JSON.stringify(o.path[0])}: ${o.expr}`); - if (!tree.children.has(field)) { - tree.children.set(field, { children: new Map() }); + if (hasCtrl) { + const stmtLines: string[] = []; + for (const o of elementOutputs.filter((x) => x.path.length > 0)) { + const varName = `__f_${o.path.join("_")}`; + stmtLines.push(`const ${varName} = ${o.expr};`); + stmtLines.push(`if (__isLoopCtrl(${varName})) return ${varName};`); } - tree.children.get(field)!.expr = mapExpr; + const assignments = elementOutputs.filter((o) => o.path.length > 0) + .map((o) => `${JSON.stringify(o.path[0])}: __f_${o.path.join("_")}`); + stmtLines.push(`return { ${assignments.join(", ")} };`); + return parts.join(" ") + " " + stmtLines.join(" "); } - return this.serializeOutputTree(tree, indent); + return parts.join(" ") + ` return { ${objEntries.join(", ")} };`; } - /** - * Build the body of a loop/flatMap callback with break/continue support. - * - * For "continue": generates flatMap body that returns [] to skip elements - * For "break": generates loop body that pushes to _result and breaks - */ - private buildElementBodyWithControlFlow( - elemWires: Wire[], - arrayIterators: Record, - depth: number, - indent: number, - mode: "break" | "continue" | "for-continue", - ): string { - const elVar = `_el${depth}`; - const pad = " ".repeat(indent); - - // Find the wire with control flow at the current depth level only - // (not sub-array element wires) - const controlWire = elemWires.find( - (w) => - w.to.path.length === 1 && - (fallbacks(w).some((fb) => fb.expr.type === "control") || - hasCatchControl(w)), - ); + // ── Source chain compilation ─────────────────────────────────────────── - if (!controlWire || !isPull(controlWire)) { - // No control flow found — fall back to simple body - const body = this.buildElementBody( - elemWires, - arrayIterators, - depth, - indent, - ); - if (mode === "continue") { - return `${pad} return [${body}];`; - } - return `${pad} _result.push(${body});`; - } + sourceChainToJs(sources: WireSourceEntry[], catchHandler?: WireCatch, indent: string = " ", elementVar?: string): string { + if (sources.length === 0) return "undefined"; - // Build the check expression using elementWireToExpr to include fallbacks - const checkExpr = this.elementWireToExpr(controlWire, elVar); + let expr = this.exprToJs(sources[0]!.expr, indent, elementVar); - // Determine the check type - 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") - ? Math.max(1, Number(ctrl.levels) || 1) - : 1; - const controlStatement = - controlLevels > 1 - ? `throw { __bridgeControl: ${JSON.stringify(controlKind)}, levels: ${controlLevels} };` - : controlKind === "continue" - ? "continue;" - : "break;"; - - if (mode === "continue") { - if (isNullish) { - return `${pad} if (${checkExpr} == null) return [];\n${pad} return [${this.buildElementBody(elemWires, arrayIterators, depth, indent)}];`; - } - // falsy fallback control - return `${pad} if (!${checkExpr}) return [];\n${pad} return [${this.buildElementBody(elemWires, arrayIterators, depth, indent)}];`; + for (let i = 1; i < sources.length; i++) { + const fb = sources[i]!; + const fbExpr = this.exprToJs(fb.expr, indent, elementVar); + if (fb.gate === "nullish") expr = `(${expr}) ?? (${fbExpr})`; + else if (fb.gate === "falsy") expr = `(${expr}) || (${fbExpr})`; } - // mode === "for-continue" — same as break but uses native 'continue' keyword - if (mode === "for-continue") { - if (isNullish) { - return `${pad} if (${checkExpr} == null) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`; - } - return `${pad} if (!${checkExpr}) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`; + if (catchHandler) { + const catchExpr = this.catchToJs(catchHandler, indent, elementVar); + expr = `(() => { try { return ${expr}; } catch (__catchErr) { return ${catchExpr}; } })()`; } - // mode === "break" - if (isNullish) { - return `${pad} if (${checkExpr} == null) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`; - } - return `${pad} if (!${checkExpr}) ${controlStatement}\n${pad} _result.push(${this.buildElementBody(elemWires, arrayIterators, depth, indent)});`; + return expr; } - // ── Wire → expression ──────────────────────────────────────────────────── - - /** Convert a wire to a JavaScript expression string. */ - wireToExpr(w: Wire): string { - // Constant wire - if (isLit(w)) return emitCoerced(wVal(w)); - - // Pull wire - 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 (isTern(w)) { - const condExpr = this.wrapExprWithLoc( - this.refToExpr(eRef(wTern(w).cond)), - wTern(w).condLoc ?? w.loc, - ); - const thenExpr = - (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 as string) - : "undefined"; - const elseExpr = - (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 as string) - : "undefined"; - let expr = `(${condExpr} ? ${thenExpr} : ${elseExpr})`; - expr = this.applyFallbacks(w, expr); - return this.wrapWireExpr(w, expr); - } - - // Logical AND - 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 (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}))`; - } else if (rightValue !== undefined) - expr = `(Boolean(${left}) && Boolean(${emitCoerced(rightValue)}))`; - else expr = `Boolean(${left})`; - expr = this.applyFallbacks(w, expr); - return this.wrapWireExpr(w, expr); - } - - // Logical OR - 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 (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(${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); - } - + private catchToJs(c: WireCatch, indent: string, elementVar?: string): string { + if ("value" in c) return JSON.stringify(c.value); + if ("control" in c) return this.controlFlowToJs(c.control, c.loc); + if ("ref" in c) return this.refToJs({ type: "ref", ref: c.ref }, elementVar); + if ("expr" in c) return this.exprToJs(c.expr, indent, elementVar); return "undefined"; } - - /** Convert an element wire (inside array mapping) to an expression. */ - private elementWireToExpr(w: Wire, elVar = "_el0"): string { - const prevElVar = this.currentElVar; - this.elementVarStack.push(elVar); - this.currentElVar = elVar; - try { - return this.wrapWireExpr(w, this._elementWireToExprInner(w, elVar)); - } finally { - this.elementVarStack.pop(); - this.currentElVar = prevElVar; - } - } - - private wrapWireExpr(w: Wire, expr: string): string { - const loc = this.serializeLoc(w.loc); - if (expr.includes("await ")) { - return `await __wrapBridgeErrorAsync(async () => (${expr}), ${loc})`; - } - return `__wrapBridgeError(() => (${expr}), ${loc})`; - } - - /** - * Find the source location of the closest wire that pulls FROM a tool. - * Used to attach `bridgeLoc` to tool execution errors. - */ - private findPullingWireLoc(trunkKey: string): SourceLocation | undefined { - for (const w of this.bridge.wires) { - if (isPull(w)) { - const srcKey = refTrunkKey(wRef(w)); - if (srcKey === trunkKey) return wRefLoc(w) ?? 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; - } - - private serializeLoc(loc?: SourceLocation): string { - return JSON.stringify(loc ?? null); - } - - private wrapExprWithLoc(expr: string, loc?: SourceLocation): string { - if (!loc) return expr; - const serializedLoc = this.serializeLoc(loc); - if (expr.includes("await ")) { - return `await __wrapBridgeErrorAsync(async () => (${expr}), ${serializedLoc})`; - } - return `__wrapBridgeError(() => (${expr}), ${serializedLoc})`; - } - - private refToElementExpr(ref: NodeRef): string { - const depth = ref.elementDepth ?? 0; - const stackIndex = this.elementVarStack.length - 1 - depth; - const elVar = - stackIndex >= 0 ? this.elementVarStack[stackIndex] : this.currentElVar; - if (!elVar) { - throw new Error(`Missing element variable for ${JSON.stringify(ref)}`); - } - if (ref.path.length === 0) return elVar; - return this.appendPathExpr(elVar, ref, true); - } - - private _elementWireToExprInner(w: Wire, elVar: string): string { - if (isLit(w)) return emitCoerced(wVal(w)); - - // Handle ternary (conditional) wires inside array mapping - if (isTern(w)) { - const condRef = eRef(wTern(w).cond); - let condExpr: string; - if (condRef.element) { - condExpr = this.refToElementExpr(condRef); - } else { - const condKey = refTrunkKey(condRef); - if (this.elementScopedTools.has(condKey)) { - condExpr = this.buildInlineToolExpr(condKey, elVar); - if (condRef.path.length > 0) { - condExpr = this.appendPathExpr(`(${condExpr})`, condRef); - } - } else { - condExpr = this.refToExpr(condRef); - } - } - condExpr = this.wrapExprWithLoc(condExpr, wTern(w).condLoc ?? w.loc); - const resolveBranch = ( - ref: NodeRef | undefined, - val: string | undefined, - loc: SourceLocation | undefined, - ): string => { - if (ref !== undefined) { - if (ref.element) { - return this.wrapExprWithLoc(this.refToElementExpr(ref), loc); - } - const branchKey = refTrunkKey(ref); - if (this.elementScopedTools.has(branchKey)) { - let e = this.buildInlineToolExpr(branchKey, elVar); - if (ref.path.length > 0) e = this.appendPathExpr(`(${e})`, ref); - return this.wrapExprWithLoc(e, loc); - } - return this.wrapExprWithLoc(this.refToExpr(ref), loc); - } - return val !== undefined ? emitCoerced(val as string) : "undefined"; - }; - const thenExpr = resolveBranch( - (wTern(w).then as RefExpr).ref, - (wTern(w).then as LitExpr).value as string | undefined, - wTern(w).thenLoc, - ); - const elseExpr = resolveBranch( - (wTern(w).else as RefExpr).ref, - (wTern(w).else as LitExpr).value as string | undefined, - wTern(w).elseLoc, - ); - let expr = `(${condExpr} ? ${thenExpr} : ${elseExpr})`; - expr = this.applyFallbacks(w, expr); - return expr; - } - - if (isPull(w)) { - // Check if the source is an element-scoped tool (needs inline computation) - if (!wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if (this.elementScopedTools.has(srcKey)) { - let expr = this.buildInlineToolExpr(srcKey, elVar); - if (wRef(w).path.length > 0) { - expr = this.appendPathExpr(`(${expr})`, wRef(w)); - } - 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(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 = 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, wRef(w), true); - expr = this.wrapExprWithLoc(expr, wRefLoc(w)); - expr = this.applyFallbacks(w, expr); - return expr; - } - return this.wireToExpr(w); - } - - /** - * Build an inline expression for an element-scoped tool. - * Used when internal tools or define containers depend on element wires. - */ - private buildInlineToolExpr(trunkKey: string, elVar: string): string { - // If we have a loop-local variable for this tool, just reference it - const localVar = this.elementLocalVars.get(trunkKey); - if (localVar) return localVar; - - // Check if it's a define container (alias) - if (this.defineContainers.has(trunkKey)) { - // Find the wires that target this define container - const wires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === trunkKey, - ); - if (wires.length === 0) return "undefined"; - // A single root wire can be inlined directly. Field wires must preserve - // the define container object shape for later path access. - if (wires.length === 1 && wires[0]!.to.path.length === 0) { - const w = wires[0]!; - // Check if the wire itself is element-scoped - if (isPull(w) && wRef(w).element) { - return this.elementWireToExpr(w, elVar); - } - if (isPull(w) && !wRef(w).element) { - // Check if the source is another element-scoped tool - const srcKey = refTrunkKey(wRef(w)); - if (this.elementScopedTools.has(srcKey)) { - return this.elementWireToExpr(w, elVar); - } - } - // Check if this is a pipe tool call (alias name <- tool:source) - if (isPull(w) && w.pipe) { - return this.elementWireToExpr(w, elVar); - } - return this.wireToExpr(w); - } - return this.buildElementContainerExpr(wires, elVar); - } - - // Internal tool — rebuild inline - const tool = this.tools.get(trunkKey); - if (!tool) return "undefined"; - - const fieldName = tool.toolName; - const toolWires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === trunkKey, - ); - - // Check if it's an internal tool we can inline - if (this.internalToolKeys.has(trunkKey)) { - const inputs = new Map(); - for (const tw of toolWires) { - const path = tw.to.path; - const key = path.join("."); - inputs.set(key, this.elementWireToExpr(tw, elVar)); - } - - const a = inputs.get("a") ?? "undefined"; - const b = inputs.get("b") ?? "undefined"; - - switch (fieldName) { - case "concat": { - const parts: string[] = []; - for (let i = 0; ; i++) { - const partExpr = inputs.get(`parts.${i}`); - if (partExpr === undefined) break; - parts.push(partExpr); - } - const concatParts = parts - .map((p) => `(${p} == null ? "" : String(${p}))`) - .join(" + "); - return `{ value: ${concatParts || '""'} }`; - } - case "add": - return `(Number(${a}) + Number(${b}))`; - case "subtract": - return `(Number(${a}) - Number(${b}))`; - case "multiply": - return `(Number(${a}) * Number(${b}))`; - case "divide": - return `(Number(${a}) / Number(${b}))`; - case "eq": - return `(${a} === ${b})`; - case "neq": - return `(${a} !== ${b})`; - case "gt": - return `(Number(${a}) > Number(${b}))`; - case "gte": - return `(Number(${a}) >= Number(${b}))`; - case "lt": - return `(Number(${a}) < Number(${b}))`; - case "lte": - return `(Number(${a}) <= Number(${b}))`; - case "not": - return `(!${a})`; - case "and": - return `(Boolean(${a}) && Boolean(${b}))`; - case "or": - return `(Boolean(${a}) || Boolean(${b}))`; - } - } - - // Non-internal tool in element scope — inline as an await __call - const inputObj = this.buildElementToolInput(toolWires, elVar); - const fnName = this.resolveToolDef(tool.toolName)?.fn ?? tool.toolName; - const fn = this.toolFnVar(fnName); - return this.memoizedToolKeys.has(trunkKey) - ? `await __callMemoized(${fn}, ${inputObj}, ${JSON.stringify(tool.toolName)}, ${JSON.stringify(fnName)}, ${JSON.stringify(trunkKey)})` - : `await __call(${fn}, ${inputObj}, ${JSON.stringify(tool.toolName)}, ${JSON.stringify(fnName)})`; - } - - /** - * Check if a wire's generated expression would contain `await`. - * Used to determine whether array loops must be async (for...of) instead of .map()/.flatMap(). - */ - private wireNeedsAwait(w: Wire): boolean { - // Element-scoped non-internal tool reference generates await __call() - if (isPull(w) && !wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) - return true; - if ( - this.elementScopedTools.has(srcKey) && - this.defineContainers.has(srcKey) - ) { - return this.hasAsyncElementDeps(srcKey); - } - } - // Catch fallback/control without errFlag → applyFallbacks generates await (async () => ...)() - if ( - (hasCatchFallback(w) || hasCatchControl(w)) && - !this.getSourceErrorFlag(w) - ) - return true; - return false; - } - - /** - * Returns true when all async needs in the given wires come ONLY from - * element-scoped tool calls (no catch fallback/control). - * When this is true, the array map can be made sync if all tools declare - * `{ sync: true }` — we generate a dual sync/async path at runtime. - */ - private asyncOnlyFromTools(wires: Wire[]): boolean { - for (const w of wires) { - if ( - (hasCatchFallback(w) || hasCatchControl(w)) && - !this.getSourceErrorFlag(w) - ) - return false; - } - return true; - } - - /** Check if an element-scoped tool has transitive async dependencies. */ - private hasAsyncElementDeps(trunkKey: string): boolean { - const wires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === trunkKey, - ); - for (const w of wires) { - if (isPull(w) && !wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) && - !this.defineContainers.has(srcKey) - ) - return true; - if ( - this.elementScopedTools.has(srcKey) && - this.defineContainers.has(srcKey) - ) { - return this.hasAsyncElementDeps(srcKey); - } - } - if (isPull(w) && w.pipe) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) - return true; - } - } - return false; - } - - /** - * Collect preamble lines for element-scoped tool calls that should be - * computed once per element and stored in loop-local variables. - * - * @param syncOnly When true, emits `__callSync()` calls (no await) — used - * inside the sync `.map()` branch of the dual-path array map optimisation. - */ - private collectElementPreamble( - elemWires: Wire[], - elVar: string, - lines: string[], - syncOnly = false, - ): void { - // Find all element-scoped non-internal tools referenced by element wires - const needed = new Set(); - const collectDeps = (tk: string) => { - if (needed.has(tk)) return; - needed.add(tk); - // Check if this container depends on other element-scoped tools - const depWires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === tk, - ); - for (const w of depWires) { - if (isPull(w) && !wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) { - collectDeps(srcKey); - } - } - if (isPull(w) && w.pipe) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) { - collectDeps(srcKey); - } - } - } - }; - - for (const w of elemWires) { - if (isPull(w) && !wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) { - collectDeps(srcKey); - } - } - } - - for (const tk of this.topologicalSortSubset(needed)) { - const vn = `_el_${this.elementLocalVars.size}`; - this.elementLocalVars.set(tk, vn); - - if (this.defineContainers.has(tk)) { - // Define container — build inline object/value - const wires = this.bridge.wires.filter((w) => refTrunkKey(w.to) === tk); - if (wires.length === 1 && wires[0]!.to.path.length === 0) { - const w = wires[0]!; - const hasCatch = hasCatchFallback(w) || hasCatchControl(w); - const hasSafe = isPull(w) && wSafe(w); - const expr = this.elementWireToExpr(w, elVar); - if (hasCatch || hasSafe) { - lines.push( - `let ${vn}; try { ${vn} = ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; ${vn} = undefined; }`, - ); - } else { - lines.push(`const ${vn} = ${expr};`); - } - } else { - lines.push( - `const ${vn} = ${this.buildElementContainerExpr(wires, elVar)};`, - ); - } - } else { - // Real tool — emit tool call - const tool = this.tools.get(tk); - if (!tool) continue; - const toolWires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === tk, - ); - const inputObj = this.buildElementToolInput(toolWires, elVar); - const fnName = this.resolveToolDef(tool.toolName)?.fn ?? tool.toolName; - const isCatchGuarded = this.catchGuardedTools.has(tk); - if (syncOnly) { - const fn = this.toolFnVar(fnName); - const syncExpr = this.memoizedToolKeys.has(tk) - ? `__callMemoized(${fn}, ${inputObj}, ${JSON.stringify(tool.toolName)}, ${JSON.stringify(fnName)}, ${JSON.stringify(tk)})` - : `__callSync(${fn}, ${inputObj}, ${JSON.stringify(tool.toolName)}, ${JSON.stringify(fnName)})`; - if (isCatchGuarded) { - lines.push(`let ${vn}, ${vn}_err;`); - lines.push( - `try { ${vn} = ${syncExpr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; ${vn}_err = _e; }`, - ); - } else { - lines.push(`const ${vn} = ${syncExpr};`); - } - } else { - const asyncExpr = this.syncAwareCall( - fnName, - inputObj, - tk, - tool.toolName, - ); - if (isCatchGuarded) { - lines.push(`let ${vn}, ${vn}_err;`); - lines.push( - `try { ${vn} = ${asyncExpr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; ${vn}_err = _e; }`, - ); - } else { - lines.push(`const ${vn} = ${asyncExpr};`); - } - } - } - } - } - - private topologicalSortSubset(keys: Iterable): string[] { - const needed = new Set(keys); - const orderedKeys = [...this.tools.keys(), ...this.defineContainers].filter( - (key) => needed.has(key), - ); - const orderIndex = new Map(orderedKeys.map((key, index) => [key, index])); - const adj = new Map>(); - const inDegree = new Map(); - - for (const key of orderedKeys) { - adj.set(key, new Set()); - inDegree.set(key, 0); - } - - for (const key of orderedKeys) { - const wires = this.bridge.wires.filter((w) => refTrunkKey(w.to) === key); - for (const w of wires) { - for (const src of this.getSourceTrunks(w)) { - if (src === key) { - const err = new BridgePanicError( - `Circular dependency detected: "${key}" depends on itself`, - ); - (err as any).bridgeLoc = isPull(w) ? wRefLoc(w) : w.loc; - throw err; - } - if (!needed.has(src)) continue; - const neighbors = adj.get(src); - if (!neighbors || neighbors.has(key)) continue; - neighbors.add(key); - inDegree.set(key, (inDegree.get(key) ?? 0) + 1); - } - } - } - - const ready = orderedKeys.filter((key) => (inDegree.get(key) ?? 0) === 0); - const sorted: string[] = []; - - while (ready.length > 0) { - ready.sort( - (left, right) => - (orderIndex.get(left) ?? 0) - (orderIndex.get(right) ?? 0), - ); - const key = ready.shift()!; - sorted.push(key); - for (const neighbor of adj.get(key) ?? []) { - const nextDegree = (inDegree.get(neighbor) ?? 1) - 1; - inDegree.set(neighbor, nextDegree); - if (nextDegree === 0) { - ready.push(neighbor); - } - } - } - - return sorted.length === orderedKeys.length ? sorted : orderedKeys; - } - - private filterCurrentElementWires( - elemWires: Wire[], - arrayIterators: Record, - ): Wire[] { - return elemWires.filter( - (w) => !(w.to.path.length > 1 && w.to.path[0]! in arrayIterators), - ); - } - - private relativeArrayIterators( - arrayIterators: Record, - prefix: string, - ): Record { - const relative: Record = {}; - const prefixWithDot = `${prefix}.`; - - for (const [path, alias] of Object.entries(arrayIterators)) { - if (path === prefix) { - relative[""] = alias; - } else if (path.startsWith(prefixWithDot)) { - relative[path.slice(prefixWithDot.length)] = alias; - } - } - - return relative; - } - - private withElementLocalVarScope(fn: () => T): T { - const previous = this.elementLocalVars; - this.elementLocalVars = new Map(previous); - try { - return fn(); - } finally { - this.elementLocalVars = previous; - } - } - - /** - * Collect the tool function references (as JS expressions) for all - * element-scoped non-internal tools used by the given element wires. - * Used to build runtime sync-check expressions for array map optimisation. - */ - private collectElementToolRefs(elemWires: Wire[]): string[] { - const needed = new Set(); - const collectDeps = (tk: string) => { - if (needed.has(tk)) return; - needed.add(tk); - const depWires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === tk, - ); - for (const w of depWires) { - if (isPull(w) && !wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) { - collectDeps(srcKey); - } - } - if (isPull(w) && w.pipe) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) { - collectDeps(srcKey); - } - } - } - }; - for (const w of elemWires) { - if (isPull(w) && !wRef(w).element) { - const srcKey = refTrunkKey(wRef(w)); - if ( - this.elementScopedTools.has(srcKey) && - !this.internalToolKeys.has(srcKey) - ) { - collectDeps(srcKey); - } - } - } - - const refs: string[] = []; - for (const tk of needed) { - if (this.defineContainers.has(tk)) continue; - const tool = this.tools.get(tk); - if (!tool) continue; - const fnName = this.resolveToolDef(tool.toolName)?.fn ?? tool.toolName; - refs.push(this.toolFnVar(fnName)); - } - return refs; - } - - /** Build an input object for a tool call inside an array map callback. */ - private buildElementToolInput(wires: Wire[], elVar: string): string { - if (wires.length === 0) return "{}"; - const entries: string[] = []; - for (const w of wires) { - const path = w.to.path; - const key = path[path.length - 1]!; - entries.push( - `${JSON.stringify(key)}: ${this.elementWireToExpr(w, elVar)}`, - ); - } - return `{ ${entries.join(", ")} }`; - } - - private buildElementContainerExpr(wires: Wire[], elVar: string): string { - if (wires.length === 0) return "undefined"; - - let rootExpr: string | undefined; - const fieldWires: Wire[] = []; - - for (const w of wires) { - if (w.to.path.length === 0) { - rootExpr = this.elementWireToExpr(w, elVar); - } else { - fieldWires.push(w); - } - } - - if (rootExpr !== undefined && fieldWires.length === 0) { - return rootExpr; - } - - interface TreeNode { - expr?: string; - children: Map; - } - - const root: TreeNode = { children: new Map() }; - - for (const w of fieldWires) { - let current = root; - for (let index = 0; index < w.to.path.length - 1; index++) { - const segment = w.to.path[index]!; - if (!current.children.has(segment)) { - current.children.set(segment, { children: new Map() }); - } - current = current.children.get(segment)!; - } - const lastSegment = w.to.path[w.to.path.length - 1]!; - if (!current.children.has(lastSegment)) { - current.children.set(lastSegment, { children: new Map() }); - } - current.children.get(lastSegment)!.expr = this.elementWireToExpr( - w, - elVar, - ); - } - - return this.serializeTreeNode(root, 4, rootExpr); - } - - /** Apply falsy (||), nullish (??) and catch fallback chains to an expression. */ - private applyFallbacks(w: Wire, expr: string): string { - // 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 = 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 wireHasFallbacks = hasFallbacks(w); - if ( - wireHasFallbacks && - wireSafe && - !hasCatchFallback(w) && - !hasCatchControl(w) - ) { - const earlyErrFlag = this.getSourceErrorFlag(w); - if (earlyErrFlag) { - expr = `(${earlyErrFlag} !== undefined ? undefined : ${expr})`; // lgtm [js/code-injection] - } - } - - 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] - } else if (ctrl.kind === "panic") { - expr = `(${expr} || (() => { const _e = new __BridgePanicError(${JSON.stringify(ctrl.message)}); _e.bridgeLoc = ${fbLoc}; throw _e; })())`; // lgtm [js/code-injection] - } - } - } else { - // nullish - 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] - } else if (ctrl.kind === "panic") { - expr = `(${expr} ?? (() => { const _e = new __BridgePanicError(${JSON.stringify(ctrl.message)}); _e.bridgeLoc = ${fbLoc}; throw _e; })())`; // lgtm [js/code-injection] - } - } - } - } - } - - // Catch fallback — use error flag from catch-guarded tool call - const errFlag = this.getSourceErrorFlag(w); - - if (hasCatchFallback(w)) { - let catchExpr: string; - if (hasCatchRef(w)) { - catchExpr = this.wrapExprWithLoc( - this.refToExpr(catchRef(w)!), - catchLoc(w), - ); - } else if (hasCatchValue(w)) { - catchExpr = emitCoerced(catchValue(w)!); - } else { - catchExpr = "undefined"; - } - - if (errFlag) { - expr = `(${errFlag} !== undefined ? ${catchExpr} : ${expr})`; // lgtm [js/code-injection] - } else { - // Fallback: wrap in IIFE with try/catch (re-throw fatal errors) - expr = `await (async () => { try { return ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; return ${catchExpr}; } })()`; // lgtm [js/code-injection] - } - } else if (wireSafe && !hasCatchControl(w)) { - // Safe navigation (?.) without catch — return undefined on error. - // When fallbacks are present, the early conversion already happened above. - if (!wireHasFallbacks) { - if (errFlag) { - expr = `(${errFlag} !== undefined ? undefined : ${expr})`; // lgtm [js/code-injection] - } else { - expr = `await (async () => { try { return ${expr}; } catch (_e) { if (_e?.name === "BridgePanicError" || _e?.name === "BridgeAbortError") throw _e; return undefined; } })()`; // lgtm [js/code-injection] - } - } - } else if (errFlag) { - // condAnd/condOr with nested safe flag — the inner refs have rootSafe/pathSafe - // 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 = - (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 - // returning undefined — swallowing the error here would be a silent data bug. - expr = `(${errFlag} !== undefined ? (() => { throw ${errFlag}; })() : ${expr})`; // lgtm [js/code-injection] - } - } - - // Catch control flow (throw/panic on catch gate) - 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: ${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: ${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 = ${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 = ${cLoc}; throw _pe; } })()`; // lgtm [js/code-injection] - } - } - } - - return expr; - } - - /** 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 (isPull(w)) { - return this.getErrorFlagForRef(wRef(w)); - } - // For ternary wires, check all referenced tools - if (isTern(w)) { - const flags: string[] = []; - const cf = this.getErrorFlagForRef(eRef(wTern(w).cond)); - if (cf) flags.push(cf); - 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 ((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 (isAndW(w)) { - const flags: string[] = []; - const lf = this.getErrorFlagForRef(eRef(wAndOr(w).left)); - if (lf) flags.push(lf); - 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 (isOrW(w)) { - const flags: string[] = []; - const lf = this.getErrorFlagForRef(eRef(wAndOr(w).left)); - if (lf) flags.push(lf); - 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(" ?? "); - } - return undefined; - } - - /** Get error flag for a specific NodeRef (used by define container emission). */ - private getErrorFlagForRef(ref: NodeRef): string | undefined { - const srcKey = refTrunkKey(ref); - if (!this.catchGuardedTools.has(srcKey)) return undefined; - if (this.internalToolKeys.has(srcKey) || this.defineContainers.has(srcKey)) - return undefined; - const localVar = this.elementLocalVars.get(srcKey); - if (localVar) return `${localVar}_err`; - const tool = this.tools.get(srcKey); - if (!tool) return undefined; - return `${tool.varName}_err`; - } - - // ── NodeRef → expression ────────────────────────────────────────────────── - - /** Convert a NodeRef to a JavaScript expression. */ - private refToExpr(ref: NodeRef): string { - // Const access: parse the JSON value at runtime, then access path - if (ref.type === "Const" && ref.field === "const" && ref.path.length > 0) { - const constName = ref.path[0]!; - const val = this.constDefs.get(constName); - if (val != null) { - const base = emitParsedConst(val); - if (ref.path.length === 1) return base; - // Delegate sub-path to appendPathExpr so pathSafe flags are respected. - const subRef: NodeRef = { - ...ref, - path: ref.path.slice(1), - rootSafe: ref.pathSafe?.[1] ?? false, - pathSafe: ref.pathSafe?.slice(1), - }; - return this.appendPathExpr(`(${base})`, subRef); - } - } - - // Self-module input reference - if ( - ref.module === SELF_MODULE && - ref.type === this.bridge.type && - ref.field === this.bridge.field && - !ref.element - ) { - if (ref.path.length === 0) return "input"; - return this.appendPathExpr("input", ref); - } - - // Tool result reference - const key = refTrunkKey(ref); - - // Handle element-scoped tools when in array context - if (this.elementScopedTools.has(key) && this.currentElVar) { - let expr = this.buildInlineToolExpr(key, this.currentElVar); - if (ref.path.length > 0) { - expr = this.appendPathExpr(`(${expr})`, ref); - } - return expr; - } - - // Handle element refs (from.element = true) - if (ref.element) { - return this.refToElementExpr(ref); - } - - const varName = this.varMap.get(key); - if (!varName) - throw new BridgeCompilerIncompatibleError( - `${this.bridge.type}.${this.bridge.field}`, - `Unsupported reference: ${key}.`, - ); - if (ref.path.length === 0) return varName; - return this.appendPathExpr(varName, ref); - } - - private appendPathExpr( - baseExpr: string, - ref: NodeRef, - allowMissingBase = false, - ): string { - if (ref.path.length === 0) return baseExpr; - - const safeFlags = ref.path.map( - (_, i) => - ref.pathSafe?.[i] ?? (i === 0 ? (ref.rootSafe ?? false) : false), - ); - // Prefer the dedicated single-segment helper on the dominant case. - // See packages/bridge-compiler/performance.md (#2). - if (ref.path.length === 1) { - return `__get(${baseExpr}, ${JSON.stringify(ref.path[0])}, ${safeFlags[0] ? "true" : "false"}, ${allowMissingBase ? "true" : "false"})`; - } - return `__path(${baseExpr}, ${JSON.stringify(ref.path)}, ${JSON.stringify(safeFlags)}, ${allowMissingBase ? "true" : "false"})`; - } - - /** - * Like refToExpr, but for ternary-only tools, inlines the tool call. - * This ensures lazy evaluation — only the chosen branch's tool is called. - */ - private lazyRefToExpr(ref: NodeRef): string { - const key = refTrunkKey(ref); - if (this.ternaryOnlyTools.has(key)) { - const tool = this.tools.get(key); - if (tool) { - const toolWires = this.bridge.wires.filter( - (w) => refTrunkKey(w.to) === key, - ); - const toolDef = this.resolveToolDef(tool.toolName); - const fnName = toolDef?.fn ?? tool.toolName; - - // Build input object - let inputObj: string; - if (toolDef) { - const inputEntries = new Map(); - for (const tw of toolDef.wires) { - if (isLit(tw) && !isTern(tw)) { - const target = tw.to.path.join("."); - inputEntries.set( - target, - `${JSON.stringify(target)}: ${emitCoerced(wVal(tw))}`, - ); - } - } - for (const tw of toolDef.wires) { - if (isPull(tw)) { - const target = tw.to.path.join("."); - const expr = this.resolveToolWireSource(tw, toolDef); - inputEntries.set(target, `${JSON.stringify(target)}: ${expr}`); - } - } - for (const bw of toolWires) { - const path = bw.to.path; - if (path.length >= 1) { - const bKey = path[0]!; - inputEntries.set( - bKey, - `${JSON.stringify(bKey)}: ${this.wireToExpr(bw)}`, - ); - } - } - const parts = [...inputEntries.values()]; - inputObj = parts.length > 0 ? `{ ${parts.join(", ")} }` : "{}"; - } else { - inputObj = this.buildObjectLiteral(toolWires, (w) => w.to.path, 4); - } - - const fn = this.toolFnVar(fnName); - let expr = this.memoizedToolKeys.has(key) - ? `(await __callMemoized(${fn}, ${inputObj}, ${JSON.stringify(tool.toolName)}, ${JSON.stringify(fnName)}, ${JSON.stringify(key)}))` - : `(await __call(${fn}, ${inputObj}, ${JSON.stringify(tool.toolName)}, ${JSON.stringify(fnName)}))`; - if (ref.path.length > 0) { - expr = this.appendPathExpr(expr, ref); - } - return expr; - } - } - return this.refToExpr(ref); - } - - /** - * Analyze which tools are only referenced in ternary branches (thenRef/elseRef) - * and can be lazily evaluated inline instead of eagerly called. - */ - private analyzeTernaryOnlyTools( - outputWires: Wire[], - toolWires: Map, - defineWires: Map, - forceMap: Map, - ): void { - // Collect all tool trunk keys referenced in any wire position - const allRefs = new Set(); - const ternaryBranchRefs = new Set(); - - const processWire = (w: Wire) => { - 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 (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 (hasCatchRef(w)) allRefs.add(refTrunkKey(catchRef(w)!)); - }; - - for (const w of outputWires) processWire(w); - for (const [, wires] of toolWires) { - for (const w of wires) processWire(w); - } - for (const [, wires] of defineWires) { - for (const w of wires) processWire(w); - } - - // A tool is ternary-only if: - // 1. It's a real tool (not define/internal) - // 2. It appears ONLY in ternaryBranchRefs, never in allRefs (from regular pull wires, cond refs, etc.) - // 3. It has no force statement - // 4. It has no input wires from other ternary-only tools (simple first pass) - for (const tk of ternaryBranchRefs) { - if (!this.tools.has(tk)) continue; - if (this.defineContainers.has(tk)) continue; - if (this.internalToolKeys.has(tk)) continue; - if (forceMap.has(tk)) continue; - if (allRefs.has(tk)) continue; // Referenced outside ternary branches - this.ternaryOnlyTools.add(tk); - } - } - - // ── Nested object literal builder ───────────────────────────────────────── - - private mergeOverdefinedExpr( - node: { expr?: string; terminal?: boolean }, - wire: Wire, - ): void { - const nextExpr = this.wireToExpr(wire); - const nextIsConstant = isLit(wire); - - if (node.expr == null) { - node.expr = nextExpr; - node.terminal = nextIsConstant; - return; - } - - if (node.terminal) return; - - if (nextIsConstant) { - node.expr = `((__v) => (__v != null ? __v : ${nextExpr}))(${node.expr})`; - node.terminal = true; - return; - } - - node.expr = `(${node.expr} ?? ${nextExpr})`; - } - - /** - * Build a JavaScript object literal from a set of wires. - * Handles nested paths by creating nested object literals. - */ - private buildObjectLiteral( - wires: Wire[], - getPath: (w: Wire) => string[], - indent: number, - ): string { - if (wires.length === 0) return "{}"; - - // Separate root wire (path=[]) from field-specific wires - let rootExpr: string | undefined; - const fieldWires: Wire[] = []; - - for (const w of wires) { - const path = getPath(w); - if (path.length === 0) { - rootExpr = this.wireToExpr(w); - } else { - fieldWires.push(w); - } - } - - // Only a root wire — simple passthrough expression - if (rootExpr !== undefined && fieldWires.length === 0) { - return rootExpr; - } - - // Build tree from field-specific wires - interface TreeNode { - expr?: string; - terminal?: boolean; - children: Map; - } - const root: TreeNode = { children: new Map() }; - - for (const w of fieldWires) { - const path = getPath(w); - let current = root; - for (let i = 0; i < path.length - 1; i++) { - const seg = path[i]!; - if (!current.children.has(seg)) { - current.children.set(seg, { children: new Map() }); - } - current = current.children.get(seg)!; - } - const lastSeg = path[path.length - 1]!; - if (!current.children.has(lastSeg)) { - current.children.set(lastSeg, { children: new Map() }); - } - const node = current.children.get(lastSeg)!; - this.mergeOverdefinedExpr(node, w); - } - - // Spread + field overrides: { ...rootExpr, field1: ..., field2: ... } - return this.serializeTreeNode(root, indent, rootExpr); - } - - private serializeTreeNode( - node: { - children: Map }>; - }, - indent: number, - spreadExpr?: string, - ): string { - const pad = " ".repeat(indent); - const entries: string[] = []; - - if (spreadExpr !== undefined) { - entries.push(`${pad}...${spreadExpr}`); - } - - for (const [key, child] of node.children) { - if (child.children.size === 0) { - entries.push( - `${pad}${JSON.stringify(key)}: ${child.expr ?? "undefined"}`, - ); - } else if (child.expr != null) { - entries.push(`${pad}${JSON.stringify(key)}: ${child.expr}`); - } else { - const nested = this.serializeTreeNode(child as typeof node, indent + 2); - entries.push(`${pad}${JSON.stringify(key)}: ${nested}`); - } - } - - const innerPad = " ".repeat(indent - 2); - return `{\n${entries.join(",\n")},\n${innerPad}}`; - } - - // ── Overdefinition bypass ─────────────────────────────────────────────── - - /** - * Analyze output wires to identify tools that can be conditionally - * skipped ("overdefinition bypass"). - * - * When multiple wires target the same output path, the runtime's - * pull-based model evaluates them in authored order and returns the - * first non-null result — later tools are never called. - * - * This method detects tools whose output contributions are ALL in - * secondary (non-first) position and returns check expressions that - * the caller uses to wrap the tool call in a null-guarded `if` block. - * - * Returns a Map from tool trunk key → { checkExprs: string[] }. - * The tool should only be called if ANY check expression is null. - */ - private analyzeOverdefinitionBypass( - outputWires: Wire[], - toolOrder: string[], - forceMap: Map, - ): Map { - const result = new Map(); - - // Step 1: Group scalar output wires by path, preserving authored order. - // Skip root wires (empty path) and element wires (array mapping). - const outputByPath = new Map(); - for (const w of outputWires) { - if (w.to.path.length === 0) continue; - if (isPull(w) && wRef(w).element) continue; - const pathKey = w.to.path.join("."); - const arr = outputByPath.get(pathKey) ?? []; - arr.push(w); - outputByPath.set(pathKey, arr); - } - - // Step 2: For each overdefined path, track tool positions. - // toolTk → { secondaryPaths, hasPrimary } - const toolInfo = new Map< - string, - { - secondaryPaths: { pathKey: string; priorExpr: string }[]; - hasPrimary: boolean; - } - >(); - - // Memoize tool sources referenced in prior chains per tool - const priorToolDeps = new Map>(); - - for (const [pathKey, wires] of outputByPath) { - if (wires.length < 2) continue; // no overdefinition - - // Build progressive prior expression chain - let priorExpr: string | null = null; - const priorToolsForPath = new Set(); - - for (let i = 0; i < wires.length; i++) { - const w = wires[i]!; - const wireExpr = this.wireToExpr(w); - - // Check if this wire pulls from a tool - 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 }); - } - const info = toolInfo.get(srcTk)!; - - if (i === 0) { - info.hasPrimary = true; - } else { - info.secondaryPaths.push({ - pathKey, - priorExpr: priorExpr!, - }); - // Record which tools are referenced in prior expressions - if (!priorToolDeps.has(srcTk)) - priorToolDeps.set(srcTk, new Set()); - for (const dep of priorToolsForPath) { - priorToolDeps.get(srcTk)!.add(dep); - } - } - } - } - - // Track tools referenced in this wire (for cascading conditionals) - if (isPull(w) && !wRef(w).element) { - const refTk = refTrunkKey(wRef(w)); - if (this.tools.has(refTk)) priorToolsForPath.add(refTk); - } - - // Extend prior expression chain - if (i === 0) { - priorExpr = wireExpr; - } else { - priorExpr = `(${priorExpr} ?? ${wireExpr})`; - } - } - } - - // Step 3: Build topological order index for dependency checking - const topoIndex = new Map(toolOrder.map((tk, i) => [tk, i])); - - // Step 4: Determine which tools qualify for bypass - for (const [toolTk, info] of toolInfo) { - // Must be fully secondary (no primary contributions) - if (info.hasPrimary) continue; - if (info.secondaryPaths.length === 0) continue; - - // Exclude force tools, catch-guarded tools, internal tools - if (forceMap.has(toolTk)) continue; - if (this.catchGuardedTools.has(toolTk)) continue; - if (this.internalToolKeys.has(toolTk)) continue; - - // Exclude tools with onError in their ToolDef - const tool = this.tools.get(toolTk); - if (tool) { - const toolDef = this.resolveToolDef(tool.toolName); - if (toolDef?.onError) continue; - } - - // Check that all prior tool dependencies appear earlier in topological order - const thisIdx = topoIndex.get(toolTk) ?? Infinity; - const deps = priorToolDeps.get(toolTk); - let valid = true; - if (deps) { - for (const dep of deps) { - if ((topoIndex.get(dep) ?? Infinity) >= thisIdx) { - valid = false; - break; - } - } - } - if (!valid) continue; - - // Check that the tool has no uncaptured output contributions - // (e.g., root wires or element wires that we skipped in analysis) - let hasUncaptured = false; - const capturedPaths = new Set( - info.secondaryPaths.map((sp) => sp.pathKey), - ); - for (const w of outputWires) { - 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; - break; - } - const pk = w.to.path.join("."); - if (!capturedPaths.has(pk)) { - hasUncaptured = true; - break; - } - } - if (hasUncaptured) continue; - - // All checks passed — this tool can be conditionally skipped - const checkExprs = info.secondaryPaths.map((sp) => sp.priorExpr); - const uniqueChecks = [...new Set(checkExprs)]; - result.set(toolTk, { checkExprs: uniqueChecks }); - } - - return result; - } - - // ── Dependency analysis & topological sort ──────────────────────────────── - - /** Get all source trunk keys a wire depends on. */ - private getSourceTrunks(w: Wire): string[] { - const trunks: string[] = []; - const collectTrunk = (ref: NodeRef) => trunks.push(refTrunkKey(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 (hasCatchRef(w)) collectTrunk(catchRef(w)!); - } - 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 (isAndW(w)) { - collectTrunk(eRef(wAndOr(w).left)); - if (eRef(wAndOr(w).right)) collectTrunk(eRef(wAndOr(w).right)); - } - if (isOrW(w)) { - collectTrunk(eRef(wAndOr(w).left)); - if (eRef(wAndOr(w).right)) collectTrunk(eRef(wAndOr(w).right)); - } - return trunks; - } - - /** - * Returns true if the tool can safely participate in a Promise.all() batch: - * plain normal-mode call with no bypass condition, no catch guard, no - * fire-and-forget, no onError ToolDef, and not an internal (sync) tool. - */ - private isParallelizableTool( - tk: string, - conditionalTools: Map, - forceMap: Map, - ): boolean { - if (this.defineContainers.has(tk)) return false; - if (this.internalToolKeys.has(tk)) return false; - if (this.catchGuardedTools.has(tk)) return false; - if (forceMap.get(tk)?.catchError) return false; - if (conditionalTools.has(tk)) return false; - const tool = this.tools.get(tk); - if (!tool) return false; - const toolDef = this.resolveToolDef(tool.toolName); - if (toolDef?.onError) return false; - // Tools with ToolDef-level tool deps need their deps emitted first - if (toolDef?.handles.some((h) => h.kind === "tool")) return false; - return true; - } - - /** - * Build a raw `__call(__fnX, {...}, ...)` expression suitable for use - * inside `Promise.all([...])` — no `await`, no `const` declaration. - * Only call this for tools where `isParallelizableTool` returns true. - */ - private buildNormalCallExpr(tool: ToolInfo, bridgeWires: Wire[]): string { - const toolDef = this.resolveToolDef(tool.toolName); - - if (!toolDef) { - const inputObj = this.buildObjectLiteral( - bridgeWires, - (w) => w.to.path, - 4, - ); - return this.syncAwareCallNoAwait(tool.toolName, inputObj, tool.trunkKey); - } - - const fnName = toolDef.fn ?? tool.toolName; - const inputEntries = new Map(); - for (const tw of toolDef.wires) { - if (isLit(tw) && !isTern(tw)) { - const target = tw.to.path.join("."); - inputEntries.set( - target, - ` ${JSON.stringify(target)}: ${emitCoerced(wVal(tw))}`, - ); - } - } - for (const tw of toolDef.wires) { - if (isPull(tw)) { - const target = tw.to.path.join("."); - const expr = this.resolveToolWireSource(tw, toolDef); - inputEntries.set(target, ` ${JSON.stringify(target)}: ${expr}`); - } - } - for (const bw of bridgeWires) { - const path = bw.to.path; - if (path.length >= 1) { - const key = path[0]!; - inputEntries.set( - key, - ` ${JSON.stringify(key)}: ${this.wireToExpr(bw)}`, - ); - } - } - const inputParts = [...inputEntries.values()]; - const inputObj = - inputParts.length > 0 ? `{\n${inputParts.join(",\n")},\n }` : "{}"; - return this.syncAwareCallNoAwait( - fnName, - inputObj, - tool.trunkKey, - tool.toolName, - ); - } - - private topologicalLayers(toolWires: Map): string[][] { - const toolKeys = [...this.tools.keys()]; - const allKeys = [...toolKeys, ...this.defineContainers]; - const adj = new Map>(); - - for (const key of allKeys) { - adj.set(key, new Set()); - } - - for (const key of allKeys) { - const wires = toolWires.get(key) ?? []; - for (const w of wires) { - for (const src of this.getSourceTrunks(w)) { - if (src === key) { - const err = new BridgePanicError( - `Circular dependency detected: "${key}" depends on itself`, - ); - (err as any).bridgeLoc = isPull(w) ? wRefLoc(w) : w.loc; - throw err; - } - if (adj.has(src)) { - adj.get(src)!.add(key); - } - } - } - } - - const inDegree = new Map(); - for (const key of allKeys) inDegree.set(key, 0); - for (const [, neighbors] of adj) { - for (const n of neighbors) { - inDegree.set(n, (inDegree.get(n) ?? 0) + 1); - } - } - - const layers: string[][] = []; - let frontier = allKeys.filter((k) => (inDegree.get(k) ?? 0) === 0); - - while (frontier.length > 0) { - layers.push([...frontier]); - const next: string[] = []; - for (const node of frontier) { - for (const neighbor of adj.get(node) ?? []) { - const newDeg = (inDegree.get(neighbor) ?? 1) - 1; - inDegree.set(neighbor, newDeg); - if (newDeg === 0) next.push(neighbor); - } - } - frontier = next; - } - - return layers; - } - - private topologicalSort(toolWires: Map): string[] { - // All node keys: tools + define containers - const toolKeys = [...this.tools.keys()]; - const allKeys = [...toolKeys, ...this.defineContainers]; - const adj = new Map>(); - - for (const key of allKeys) { - adj.set(key, new Set()); - } - - // Build adjacency: src → dst edges (deduplicated via Set) - for (const key of allKeys) { - const wires = toolWires.get(key) ?? []; - for (const w of wires) { - for (const src of this.getSourceTrunks(w)) { - if (src === key) { - const err = new BridgePanicError( - `Circular dependency detected: "${key}" depends on itself`, - ); - (err as any).bridgeLoc = isPull(w) ? wRefLoc(w) : w.loc; - throw err; - } - if (adj.has(src)) { - adj.get(src)!.add(key); - } - } - } - } - - // Compute in-degree from the adjacency sets (avoids double-counting) - const inDegree = new Map(); - for (const key of allKeys) inDegree.set(key, 0); - for (const [, neighbors] of adj) { - for (const n of neighbors) { - inDegree.set(n, (inDegree.get(n) ?? 0) + 1); - } - } - - // Kahn's algorithm - const queue: string[] = []; - for (const [key, deg] of inDegree) { - if (deg === 0) queue.push(key); - } - - const sorted: string[] = []; - while (queue.length > 0) { - const node = queue.shift()!; - sorted.push(node); - for (const neighbor of adj.get(node) ?? []) { - const newDeg = (inDegree.get(neighbor) ?? 1) - 1; - inDegree.set(neighbor, newDeg); - if (newDeg === 0) queue.push(neighbor); - } - } - - if (sorted.length !== allKeys.length) { - const err = new Error("Circular dependency detected in tool calls"); - err.name = "BridgePanicError"; - throw err; - } - - return sorted; - } }