diff --git a/.changeset/cute-states-wash.md b/.changeset/cute-states-wash.md new file mode 100644 index 00000000..c615f0a6 --- /dev/null +++ b/.changeset/cute-states-wash.md @@ -0,0 +1,6 @@ +--- +"@stackables/bridge-compiler": minor +"@stackables/bridge-parser": minor +--- + +Support object spread in path-scoped scope blocks diff --git a/packages/bridge-compiler/src/codegen.ts b/packages/bridge-compiler/src/codegen.ts index 0c40409b..14e9076b 100644 --- a/packages/bridge-compiler/src/codegen.ts +++ b/packages/bridge-compiler/src/codegen.ts @@ -961,9 +961,13 @@ class CodegenContext { } // Bridge wires override ToolDef wires + let spreadExprForToolDef: string | undefined; for (const bw of bridgeWires) { const path = bw.to.path; - if (path.length >= 1) { + 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, @@ -974,8 +978,16 @@ class CodegenContext { const inputParts = [...inputEntries.values()]; - const inputObj = - inputParts.length > 0 ? `{\n${inputParts.join(",\n")},\n }` : "{}"; + 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 }` : "{}"; + } if (onErrorWire) { // Wrap in try/catch for onError @@ -2584,7 +2596,25 @@ class CodegenContext { ): string { if (wires.length === 0) return "{}"; - // Build tree + // 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; @@ -2592,9 +2622,8 @@ class CodegenContext { } const root: TreeNode = { children: new Map() }; - for (const w of wires) { + for (const w of fieldWires) { const path = getPath(w); - if (path.length === 0) return this.wireToExpr(w); let current = root; for (let i = 0; i < path.length - 1; i++) { const seg = path[i]!; @@ -2611,7 +2640,8 @@ class CodegenContext { this.mergeOverdefinedExpr(node, w); } - return this.serializeTreeNode(root, indent); + // Spread + field overrides: { ...rootExpr, field1: ..., field2: ... } + return this.serializeTreeNode(root, indent, rootExpr); } private serializeTreeNode( @@ -2619,10 +2649,15 @@ class CodegenContext { 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( diff --git a/packages/bridge-parser/src/parser/lexer.ts b/packages/bridge-parser/src/parser/lexer.ts index 80856cdc..6fcfcccb 100644 --- a/packages/bridge-parser/src/parser/lexer.ts +++ b/packages/bridge-parser/src/parser/lexer.ts @@ -188,6 +188,7 @@ export const RCurly = createToken({ name: "RCurly", pattern: /\}/ }); export const LSquare = createToken({ name: "LSquare", pattern: /\[/ }); export const RSquare = createToken({ name: "RSquare", pattern: /\]/ }); export const Equals = createToken({ name: "Equals", pattern: /=/ }); +export const Spread = createToken({ name: "Spread", pattern: /\.\.\./ }); export const Dot = createToken({ name: "Dot", pattern: /\./ }); export const Colon = createToken({ name: "Colon", pattern: /:/ }); export const Comma = createToken({ name: "Comma", pattern: /,/ }); @@ -258,6 +259,7 @@ export const allTokens = [ LSquare, RSquare, Equals, + Spread, Dot, Colon, Comma, diff --git a/packages/bridge-parser/src/parser/parser.ts b/packages/bridge-parser/src/parser/parser.ts index daa4339d..97e43183 100644 --- a/packages/bridge-parser/src/parser/parser.ts +++ b/packages/bridge-parser/src/parser/parser.ts @@ -42,6 +42,7 @@ import { LSquare, RSquare, Equals, + Spread, Dot, Colon, Comma, @@ -547,7 +548,7 @@ class BridgeParser extends CstParser { }, }, { - // Path scoping block: target { .field <- source | .field = value | .field { ... } | alias ... as ... } + // Path scoping block: target { lines: .field <- source, .field = value, .field { ... }, alias ... as ..., ...source } ALT: () => { this.CONSUME(LCurly, { LABEL: "scopeBlock" }); this.MANY3(() => @@ -557,6 +558,7 @@ class BridgeParser extends CstParser { this.SUBRULE(this.bridgeNodeAlias, { LABEL: "scopeAlias" }), }, { ALT: () => this.SUBRULE(this.pathScopeLine) }, + { ALT: () => this.SUBRULE(this.scopeSpreadLine) }, ]), ); this.CONSUME(RCurly); @@ -681,11 +683,22 @@ class BridgeParser extends CstParser { }, }, { - // Path scope block: .field { .subField <- source | .subField = value | ... } + // Path scope block: .field { lines: .subField <- source, ...source, .subField = value, ... } ALT: () => { this.CONSUME(LCurly, { LABEL: "elemScopeBlock" }); this.MANY3(() => - this.SUBRULE(this.pathScopeLine, { LABEL: "elemScopeLine" }), + this.OR3([ + { + ALT: () => + this.SUBRULE(this.pathScopeLine, { LABEL: "elemScopeLine" }), + }, + { + ALT: () => + this.SUBRULE(this.scopeSpreadLine, { + LABEL: "elemSpreadLine", + }), + }, + ]), ); this.CONSUME(RCurly); }, @@ -787,6 +800,7 @@ class BridgeParser extends CstParser { this.SUBRULE(this.bridgeNodeAlias, { LABEL: "scopeAlias" }), }, { ALT: () => this.SUBRULE(this.pathScopeLine) }, + { ALT: () => this.SUBRULE(this.scopeSpreadLine) }, ]), ); this.CONSUME(RCurly); @@ -795,6 +809,18 @@ class BridgeParser extends CstParser { ]); }); + /** + * Spread line inside a path scope block: + * ...sourceExpr + * + * Wires all fields of the source to the current scope target path. + * Equivalent to writing `target <- sourceExpr` at the outer level. + */ + public scopeSpreadLine = this.RULE("scopeSpreadLine", () => { + this.CONSUME(Spread); + this.SUBRULE(this.sourceExpr, { LABEL: "spreadSource" }); + }); + /** A coalesce alternative: either a JSON literal or a source expression */ public coalesceAlternative = this.RULE("coalesceAlternative", () => { // Need to distinguish literal values from source references. @@ -2172,8 +2198,32 @@ function processElementLines( wires.push(...nullishFallbackInternalWires); wires.push(...catchFallbackInternalWires); } else if (elemC.elemScopeBlock) { - // ── Path scope block inside array mapping: .field { .sub <- ... } ── + // ── Path scope block inside array mapping: .field { lines: .sub <- ..., ...source } ── const scopeLines = subs(elemLine, "elemScopeLine"); + // Process spread lines at the top level of this scope block + const spreadLines = subs(elemLine, "elemSpreadLine"); + for (const spreadLine of spreadLines) { + const spreadLineNum = line(findFirstToken(spreadLine)); + const sourceNode = sub(spreadLine, "spreadSource")!; + const fromRef = buildSourceExpr(sourceNode, spreadLineNum, iterName); + // Propagate safe navigation (?.) flag from source expression + const headNode = sub(sourceNode, "head")!; + const pipeNodes = subs(sourceNode, "pipeSegment"); + const actualNode = + pipeNodes.length > 0 ? pipeNodes[pipeNodes.length - 1]! : headNode; + const { safe: spreadSafe } = extractAddressPath(actualNode); + wires.push({ + from: fromRef, + to: { + module: SELF_MODULE, + type: bridgeType, + field: bridgeField, + element: true, + path: elemToPath, + }, + ...(spreadSafe ? { safe: true as const } : {}), + }); + } processElementScopeLines( scopeLines, elemToPath, @@ -2291,7 +2341,36 @@ function processElementScopeLines( // ── Nested scope: .field { ... } ── const nestedScopeLines = subs(scopeLine, "pathScopeLine"); - if (nestedScopeLines.length > 0 && !sc.scopeEquals && !sc.scopeArrow) { + const nestedSpreadLines = subs(scopeLine, "scopeSpreadLine"); + if ( + (nestedScopeLines.length > 0 || nestedSpreadLines.length > 0) && + !sc.scopeEquals && + !sc.scopeArrow + ) { + // Process spread lines inside this nested scope block: ...sourceExpr + const spreadToPath = [...arrayToPath, ...fullSegs]; + for (const spreadLine of nestedSpreadLines) { + const spreadLineNum = line(findFirstToken(spreadLine)); + const sourceNode = sub(spreadLine, "spreadSource")!; + const fromRef = buildSourceExpr(sourceNode, spreadLineNum, iterName); + // Propagate safe navigation (?.) flag from source expression + const headNode = sub(sourceNode, "head")!; + const pipeNodes = subs(sourceNode, "pipeSegment"); + const actualNode = + pipeNodes.length > 0 ? pipeNodes[pipeNodes.length - 1]! : headNode; + const { safe: spreadSafe } = extractAddressPath(actualNode); + wires.push({ + from: fromRef, + to: { + module: SELF_MODULE, + type: bridgeType, + field: bridgeField, + element: true, + path: spreadToPath, + }, + ...(spreadSafe ? { safe: true as const } : {}), + }); + } processElementScopeLines( nestedScopeLines, arrayToPath, @@ -4097,7 +4176,12 @@ function buildBridgeBody( // ── Nested scope: .field { ... } ── const nestedScopeLines = subs(scopeLine, "pathScopeLine"); - if (nestedScopeLines.length > 0 && !sc.scopeEquals && !sc.scopeArrow) { + const nestedSpreadLines = subs(scopeLine, "scopeSpreadLine"); + if ( + (nestedScopeLines.length > 0 || nestedSpreadLines.length > 0) && + !sc.scopeEquals && + !sc.scopeArrow + ) { // Process alias declarations inside the nested scope block first const scopeAliases = subs(scopeLine, "scopeAlias"); for (const aliasNode of scopeAliases) { @@ -4132,6 +4216,21 @@ function buildBridgeBody( ...(aliasSafe ? { safe: true as const } : {}), }); } + // Process spread lines inside this nested scope block: ...sourceExpr + const nestedToRef = resolveAddress(targetRoot, fullSegs, scopeLineNum); + for (const spreadLine of nestedSpreadLines) { + const spreadLineNum = line(findFirstToken(spreadLine)); + const sourceNode = sub(spreadLine, "spreadSource")!; + const { ref: fromRef, safe: spreadSafe } = buildSourceExprSafe( + sourceNode, + spreadLineNum, + ); + wires.push({ + from: fromRef, + to: nestedToRef, + ...(spreadSafe ? { safe: true as const } : {}), + }); + } processScopeLines(nestedScopeLines, targetRoot, fullSegs); continue; } @@ -4769,6 +4868,21 @@ function buildBridgeBody( }); } const scopeLines = subs(wireNode, "pathScopeLine"); + // Process spread lines inside the scope block: ...sourceExpr + const spreadLines = subs(wireNode, "scopeSpreadLine"); + for (const spreadLine of spreadLines) { + const spreadLineNum = line(findFirstToken(spreadLine)); + const sourceNode = sub(spreadLine, "spreadSource")!; + const { ref: fromRef, safe: spreadSafe } = buildSourceExprSafe( + sourceNode, + spreadLineNum, + ); + wires.push({ + from: fromRef, + to: toRef, + ...(spreadSafe ? { safe: true as const } : {}), + }); + } processScopeLines(scopeLines, targetRoot, targetSegs); continue; } diff --git a/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json b/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json index 9e7416ac..49bda115 100644 --- a/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json +++ b/packages/bridge-syntax-highlight/syntaxes/bridge.tmLanguage.json @@ -8,6 +8,7 @@ { "include": "#block-headers" }, { "include": "#block-braces" }, { "include": "#force-lines" }, + { "include": "#spread-lines" }, { "include": "#wire-lines" }, { "include": "#const-lines" }, { "include": "#reserved-handles" }, @@ -166,6 +167,15 @@ } }, + "spread-lines": { + "comment": "Spread line inside a path scope block: ...sourceExpr", + "match": "^\\s*(\\.\\.\\.)([A-Za-z_][A-Za-z0-9_.]*)", + "captures": { + "1": { "name": "keyword.operator.spread.bridge" }, + "2": { "name": "variable.other.source.bridge" } + } + }, + "wire-lines": { "comment": "Wire line: target <- source [|| …] [?? …] [catch …] — target may be dot-prefixed (.field) inside array-map blocks", "begin": "^(\\s*)(\\.?[A-Za-z_][A-Za-z0-9_.]*)\\s*(<-)", diff --git a/packages/bridge/test/path-scoping.test.ts b/packages/bridge/test/path-scoping.test.ts index 78aca899..80c3724c 100644 --- a/packages/bridge/test/path-scoping.test.ts +++ b/packages/bridge/test/path-scoping.test.ts @@ -715,6 +715,227 @@ bridge Query.test { }); }); +// ── Spread in scope blocks ─────────────────────────────────────────────────── + +describe("path scoping – spread syntax parser", () => { + test("spread in top-level scope block produces root pull wire", () => { + const result = parseBridge(`version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t { + ...i + } + + o.result <- t +}`); + const bridge = result.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const pullWires = bridge.wires.filter( + (w): w is Extract => "from" in w, + ); + const spreadWire = pullWires.find((w) => w.to.path.length === 0); + assert.ok(spreadWire, "spread wire targeting tool root should exist"); + assert.deepStrictEqual(spreadWire.from.path, []); + }); + + test("spread combined with constant wires in scope block", () => { + const result = parseBridge(`version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t { + ...i + .extra = "added" + } + + o.result <- t +}`); + const bridge = result.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const pullWires = bridge.wires.filter( + (w): w is Extract => "from" in w, + ); + const constWires = bridge.wires.filter( + (w): w is Extract => "value" in w, + ); + assert.ok( + pullWires.find((w) => w.to.path.length === 0), + "spread wire to tool root should exist", + ); + assert.ok( + constWires.find((w) => w.to.path.join(".") === "extra"), + "constant wire for .extra should exist", + ); + }); + + test("spread with sub-path source in scope block", () => { + const result = parseBridge(`version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t { + ...i.profile + } + + o.result <- t +}`); + const bridge = result.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const pullWires = bridge.wires.filter( + (w): w is Extract => "from" in w, + ); + const spreadWire = pullWires.find((w) => w.to.path.length === 0); + assert.ok(spreadWire, "spread wire should exist"); + assert.deepStrictEqual(spreadWire.from.path, ["profile"]); + }); + + test("spread in nested scope block produces wire to nested path", () => { + const result = parseBridge(`version 1.5 + +bridge Query.test { + with input as i + with output as o + + o.wrapper { + ...i + .flag = "true" + } +}`); + const bridge = result.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const pullWires = bridge.wires.filter( + (w): w is Extract => "from" in w, + ); + const spreadWire = pullWires.find( + (w) => w.to.path.join(".") === "wrapper" && w.from.path.length === 0, + ); + assert.ok(spreadWire, "spread wire to o.wrapper should exist"); + }); + + test("spread in deeply nested scope block", () => { + const result = parseBridge(`version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t.nested { + ...i + } + + o.result <- t +}`); + const bridge = result.instructions.find( + (i): i is Bridge => i.kind === "bridge", + )!; + const pullWires = bridge.wires.filter( + (w): w is Extract => "from" in w, + ); + const spreadWire = pullWires.find((w) => w.to.path.join(".") === "nested"); + assert.ok(spreadWire, "spread wire to tool.nested should exist"); + assert.deepStrictEqual(spreadWire.from.path, []); + }); +}); + +forEachEngine("path scoping – spread execution", (run, _ctx) => { + test("spread in scope block passes all input fields to tool", async () => { + const bridge = `version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t { + ...i + } + + o.result <- t +}`; + const result = await run( + bridge, + "Query.test", + { name: "Alice", age: 30 }, + { + myTool: async (input: any) => ({ received: input }), + }, + ); + assert.deepStrictEqual(result.data, { + result: { received: { name: "Alice", age: 30 } }, + }); + }); + + test("spread combined with constant field override", async () => { + const bridge = `version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t { + ...i + .extra = "added" + } + + o.result <- t +}`; + const result = await run( + bridge, + "Query.test", + { name: "Alice", age: 30 }, + { + myTool: async (input: any) => ({ received: input }), + }, + ); + assert.deepStrictEqual(result.data, { + result: { received: { name: "Alice", age: 30, extra: "added" } }, + }); + }); + + test("spread with sub-path source", async () => { + const bridge = `version 1.5 + +bridge Query.test { + with input as i + with myTool as t + with output as o + + t { + ...i.profile + } + + o.result <- t +}`; + const result = await run( + bridge, + "Query.test", + { profile: { name: "Bob", email: "bob@test.com" } }, + { + myTool: async (input: any) => ({ received: input }), + }, + ); + assert.deepStrictEqual(result.data, { + result: { received: { name: "Bob", email: "bob@test.com" } }, + }); + }); +}); + // ── Null intermediate path access ──────────────────────────────────────────── forEachEngine("path traversal: null intermediate segment", (run, _ctx) => { diff --git a/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx b/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx index a952fdbb..ac1c0dd2 100644 --- a/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx +++ b/packages/docs-site/src/content/docs/reference/30-wiring-routing.mdx @@ -62,7 +62,30 @@ api.body.user { ``` -## 3. The Universal `alias` +## 3. Spreading Objects (`...`) + +If you want to map an entire object into a specific target without writing out every single field, you can use the spread operator (`...`) inside a Path Scoping Block. + +This works exactly like the JavaScript spread operator. It unpacks all fields from the source and applies them to the current target path. + +```bridge +bridge Query.getUser { + with api.fetchProfile as profile + with output as o + + o.user { + # Spreads all fields from the profile (name, email, age, etc.) into o.user + ...profile + + # You can explicitly override specific fields after spreading + .id <- profile.internalId + .status = "ACTIVE" + } +} + +``` + +## 4. The Universal `alias` The `alias` keyword allows you to declare a temporary, reusable node in your graph. @@ -104,7 +127,7 @@ o.total <- total ``` -## 4. Forcing Execution (`force`) +## 5. Forcing Execution (`force`) Because The Bridge uses a lazy, pull-based execution engine, **a tool is never called unless a GraphQL output field actively demands its data.** @@ -143,4 +166,4 @@ If you want the side-effect to be **fire-and-forget** (where errors are safely c # Fire-and-forget: If the logger fails, gracefully swallow the network error force log catch null -``` \ No newline at end of file +``` diff --git a/packages/docs-site/src/content/docs/reference/summary.mdx b/packages/docs-site/src/content/docs/reference/summary.mdx index fa07650f..fc02246f 100644 --- a/packages/docs-site/src/content/docs/reference/summary.mdx +++ b/packages/docs-site/src/content/docs/reference/summary.mdx @@ -5,81 +5,83 @@ description: An overview of the key features and capabilities of the Bridge lang ### 1. Data Routing & Logic -*The core primitives for mapping data from tools to the output.* - -| Feature | Example | -| --- | --- | -| **Pull wires** (`<-`) | `out.name <- api.name` | -| **Constant wires** (`=`) | `api.method = "GET"` | -| **Nullish coalescing** (`??`) | `out.x <- api.x ?? "default"` | -| **Falsy fallback** (`\|\|`) | `out.x <- api.x \|\| backup.x` | -| **Conditional (Ternary)** | `api.mode <- i.premium ? "full" : "basic"` | -| **Lazy Evaluation** | Only the chosen branch in a ternary is executed | -| **Overdefinition** | Multiple wires to the same target (`o.x <- a` and `o.x <- b`) resolve as first non-null wins | -| **Root passthrough** | `o <- api` (maps the entire object) | -| **Context access** | `api.token <- ctx.apiKey` | +_The core primitives for mapping data from tools to the output._ + +| Feature | Example | +| ----------------------------- | -------------------------------------------------------------------------------------------- | +| **Pull wires** (`<-`) | `out.name <- api.name` | +| **Constant wires** (`=`) | `api.method = "GET"` | +| **Nullish coalescing** (`??`) | `out.x <- api.x ?? "default"` | +| **Falsy fallback** (`\|\|`) | `out.x <- api.x \|\| backup.x` | +| **Conditional (Ternary)** | `api.mode <- i.premium ? "full" : "basic"` | +| **Lazy Evaluation** | Only the chosen branch in a ternary is executed | +| **Overdefinition** | Multiple wires to the same target (`o.x <- a` and `o.x <- b`) resolve as first non-null wins | +| **Root passthrough** | `o <- api` (maps the entire object) | +| **Context access** | `api.token <- ctx.apiKey` | +| **Path scoping** (`{}`) | `out.user { .name <- api.name }` (Groups nested paths) | +| **Spreading objects** (`...`) | `out.user { ...api.profile }` (Merges object fields) | ### 2. Variables & Expressions -*Tools for data manipulation and code organization.* +_Tools for data manipulation and code organization._ -| Feature | Example | -| --- | --- | -| **`alias` declarations** | `alias api.result.data as d` | -| **Safe-exec aliases** (`?.`) | `alias api?.value as safeVal` | -| **Alias fallbacks** | `alias (expr) ? val : null ?? panic "msg" as x` | -| **Math / Comparison** | `o.total <- i.price * i.qty`, `o.isAdult <- i.age >= 18` | -| **String interpolation** | `o.msg <- "Hello, {i.name}!"` | -| **Pipe operators** | `o.loud <- tu:i.text` | -| **`const` blocks** | `const geo = { "lat": 0 }` (Inlined at compile time) | -| **`define` blocks** | `define profile { ... }` (Virtual containers) | +| Feature | Example | +| ---------------------------- | -------------------------------------------------------- | +| **`alias` declarations** | `alias api.result.data as d` | +| **Safe-exec aliases** (`?.`) | `alias api?.value as safeVal` | +| **Alias fallbacks** | `alias (expr) ? val : null ?? panic "msg" as x` | +| **Math / Comparison** | `o.total <- i.price * i.qty`, `o.isAdult <- i.age >= 18` | +| **String interpolation** | `o.msg <- "Hello, {i.name}!"` | +| **Pipe operators** | `o.loud <- tu:i.text` | +| **`const` blocks** | `const geo = { "lat": 0 }` (Inlined at compile time) | +| **`define` blocks** | `define profile { ... }` (Virtual containers) | ### 3. Array Processing -*Zero-allocation iteration using native JavaScript loops.* +_Zero-allocation iteration using native JavaScript loops._ -| Feature | Example | -| --- | --- | -| **Array mapping** | `out.items <- api.list[] as el { .id <- el.id }` | -| **Nested arrays** | `o <- items[] as i { .sub <- i.list[] as j { ... } }` | -| **Array Control Flow** | `item.name ?? break`, `item.name ?? continue` | -| **Nested Control Flow** | `break 1`/`continue 2` scopes correctly in sub-arrays | -| **Interpolation in arrays** | `o <- items[] as it { .url <- "/items/{it.id}" }` | -| **Null array preservation** | If source is `null`, output is `null` (not `[]`) | +| Feature | Example | +| --------------------------- | ----------------------------------------------------- | +| **Array mapping** | `out.items <- api.list[] as el { .id <- el.id }` | +| **Root array output** | `o <- api.list[] as el { ... }` | +| **Nested arrays** | `o <- items[] as i { .sub <- i.list[] as j { ... } }` | +| **Array Control Flow** | `item.name ?? break`, `item.name ?? continue` | +| **Nested Control Flow** | `break 1`/`continue 2` scopes correctly in sub-arrays | +| **Interpolation in arrays** | `o <- items[] as it { .url <- "/items/{it.id}" }` | +| **Null array preservation** | If source is `null`, output is `null` (not `[]`) | ### 4. Error Handling & Control Flow -*Granular control over tool failures and execution halting.* +_Granular control over tool failures and execution halting._ -| Feature | Example | -| --- | --- | -| **`catch` fallbacks** | `out.data <- api.result catch "fallback"` | -| **`catch` ref fallbacks** | `out.data <- primary.val catch backup.val` | -| **`throw`** | `o.name <- i.name \|\| throw "name is required"` | -| **`panic` (Fatal)** | `o.name <- i.name ?? panic "fatal error"` (Bypasses catch) | -| **`force` (Critical tool)** | `force audit` (Execution halts if tool fails) | -| **`force catch null`** | `force ping catch null` (Fire-and-forget execution) | +| Feature | Example | +| --------------------------- | ---------------------------------------------------------- | +| **`catch` fallbacks** | `out.data <- api.result catch "fallback"` | +| **`catch` ref fallbacks** | `out.data <- primary.val catch backup.val` | +| **`throw`** | `o.name <- i.name \|\| throw "name is required"` | +| **`panic` (Fatal)** | `o.name <- i.name ?? panic "fatal error"` (Bypasses catch) | +| **`force` (Critical tool)** | `force audit` (Execution halts if tool fails) | +| **`force catch null`** | `force ping catch null` (Fire-and-forget execution) | ### 5. Tool Composition (`ToolDef`) -*Reusable tool configurations defined in the schema.* +_Reusable tool configurations defined in the schema._ -| Feature | Example | -| --- | --- | -| **Pre-wired Inputs** | `tool api from httpCall { .method = "GET" }` | -| **Tool inheritance** | `tool childApi from parentApi { .path = "/v2" }` | -| **Centralized `on error`** | `tool api from httpCall { on error = {...} }` | -| **Override mechanics** | Bridge wires override ToolDef wires by key | +| Feature | Example | +| -------------------------- | ------------------------------------------------ | +| **Pre-wired Inputs** | `tool api from httpCall { .method = "GET" }` | +| **Tool inheritance** | `tool childApi from parentApi { .path = "/v2" }` | +| **Centralized `on error`** | `tool api from httpCall { on error = {...} }` | +| **Override mechanics** | Bridge wires override ToolDef wires by key | ### 6. Compiler & Runtime Guarantees -*Under-the-hood engine features that ensure stability and performance.* +_Under-the-hood engine features that ensure stability and performance._ -| Feature | Description | -| --- | --- | +| Feature | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------ | | **Dead-Code Elimination** | Unrequested GraphQL/Sparse fields are pruned; unneeded tools are mathematically excluded from the JIT compilation. | -| **Tool Timeouts** | Automatic `Promise.race` with memory-leak prevention (`clearTimeout`) based on `toolTimeoutMs`. | -| **Cycle Detection** | Compile-time detection of circular tool dependencies (Kahn's algorithm) throws an immediate initialization error. | -| **Abort Signals** | Pre-tool `signal.aborted` checks throw `BridgeAbortError`, which safely bypasses standard `catch` blocks. | -| **Telemetry Injection** | Automatically passes `{ logger, signal }` via `ctx` to custom tool functions. | - +| **Tool Timeouts** | Automatic `Promise.race` with memory-leak prevention (`clearTimeout`) based on `toolTimeoutMs`. | +| **Cycle Detection** | Compile-time detection of circular tool dependencies (Kahn's algorithm) throws an immediate initialization error. | +| **Abort Signals** | Pre-tool `signal.aborted` checks throw `BridgeAbortError`, which safely bypasses standard `catch` blocks. | +| **Telemetry Injection** | Automatically passes `{ logger, signal }` via `ctx` to custom tool functions. |