@stackables/bridge 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -52
- package/build/ExecutionTree.js +12 -2
- package/build/bridge-format.d.ts +0 -10
- package/build/bridge-format.js +662 -218
- package/build/types.d.ts +48 -1
- package/package.json +2 -1
package/build/bridge-format.js
CHANGED
|
@@ -10,10 +10,34 @@ import { SELF_MODULE } from "./types.js";
|
|
|
10
10
|
* @param text - Bridge definition text
|
|
11
11
|
* @returns Array of instructions (Bridge, ToolDef)
|
|
12
12
|
*/
|
|
13
|
+
const BRIDGE_VERSION = "1.4";
|
|
14
|
+
// Keywords that cannot be used as tool names, aliases, or const names
|
|
15
|
+
const RESERVED_KEYWORDS = new Set(["bridge", "with", "as", "from", "const", "tool", "version", "define"]);
|
|
16
|
+
// Source identifiers reserved for their special meaning inside bridge/tool blocks
|
|
17
|
+
const SOURCE_IDENTIFIERS = new Set(["input", "output", "context"]);
|
|
18
|
+
function assertNotReserved(name, lineNum, label) {
|
|
19
|
+
if (RESERVED_KEYWORDS.has(name.toLowerCase())) {
|
|
20
|
+
throw new Error(`Line ${lineNum}: "${name}" is a reserved keyword and cannot be used as a ${label}`);
|
|
21
|
+
}
|
|
22
|
+
if (SOURCE_IDENTIFIERS.has(name.toLowerCase())) {
|
|
23
|
+
throw new Error(`Line ${lineNum}: "${name}" is a reserved source identifier and cannot be used as a ${label}`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
13
26
|
export function parseBridge(text) {
|
|
14
27
|
// Normalize: CRLF → LF, tabs → 2 spaces
|
|
15
28
|
const normalized = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
16
29
|
const allLines = normalized.split("\n");
|
|
30
|
+
// Version check — first non-blank, non-comment line must be `version 1.4`
|
|
31
|
+
const firstContentIdx = allLines.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("#"));
|
|
32
|
+
if (firstContentIdx === -1 || !/^version\s+/.test(allLines[firstContentIdx].trim())) {
|
|
33
|
+
throw new Error(`Missing version declaration. Bridge files must begin with: version ${BRIDGE_VERSION}`);
|
|
34
|
+
}
|
|
35
|
+
const versionToken = allLines[firstContentIdx].trim().replace(/^version\s+/, "");
|
|
36
|
+
if (versionToken !== BRIDGE_VERSION) {
|
|
37
|
+
throw new Error(`Unsupported bridge version "${versionToken}". This parser requires: version ${BRIDGE_VERSION}`);
|
|
38
|
+
}
|
|
39
|
+
// Blank out the version line so block-splitting ignores it
|
|
40
|
+
allLines[firstContentIdx] = "";
|
|
17
41
|
// Find separator lines (--- with optional surrounding whitespace)
|
|
18
42
|
const isSep = (line) => /^\s*---\s*$/.test(line);
|
|
19
43
|
// Collect block ranges as [start, end) line indices
|
|
@@ -35,7 +59,7 @@ export function parseBridge(text) {
|
|
|
35
59
|
let currentOffset = start;
|
|
36
60
|
for (let i = 0; i < blockLines.length; i++) {
|
|
37
61
|
const trimmed = blockLines[i].trim();
|
|
38
|
-
if (/^(tool|bridge|const|
|
|
62
|
+
if (/^(tool|bridge|const|define)\s/i.test(trimmed) && currentLines.length > 0) {
|
|
39
63
|
// Check if any non-blank content exists
|
|
40
64
|
if (currentLines.some((l) => l.trim())) {
|
|
41
65
|
subBlocks.push({ startOffset: currentOffset, lines: currentLines });
|
|
@@ -58,17 +82,20 @@ export function parseBridge(text) {
|
|
|
58
82
|
while (firstContentLine < sub.lines.length && !sub.lines[firstContentLine].trim())
|
|
59
83
|
firstContentLine++;
|
|
60
84
|
const firstLine = sub.lines[firstContentLine]?.trim();
|
|
61
|
-
if (firstLine && /^
|
|
85
|
+
if (firstLine && /^tool\s/i.test(firstLine)) {
|
|
62
86
|
instructions.push(parseToolBlock(subText, sub.startOffset + firstContentLine, instructions));
|
|
63
87
|
}
|
|
88
|
+
else if (firstLine && /^define\s/i.test(firstLine)) {
|
|
89
|
+
instructions.push(parseDefineBlock(subText, sub.startOffset + firstContentLine));
|
|
90
|
+
}
|
|
64
91
|
else if (firstLine && /^bridge\s/i.test(firstLine)) {
|
|
65
|
-
instructions.push(...parseBridgeBlock(subText, sub.startOffset + firstContentLine));
|
|
92
|
+
instructions.push(...parseBridgeBlock(subText, sub.startOffset + firstContentLine, instructions));
|
|
66
93
|
}
|
|
67
94
|
else if (firstLine && /^const\s/i.test(firstLine)) {
|
|
68
95
|
instructions.push(...parseConstLines(subText, sub.startOffset + firstContentLine));
|
|
69
96
|
}
|
|
70
97
|
else if (firstLine && !firstLine.startsWith("#")) {
|
|
71
|
-
throw new Error(`Line ${sub.startOffset + firstContentLine + 1}: Expected "tool", "
|
|
98
|
+
throw new Error(`Line ${sub.startOffset + firstContentLine + 1}: Expected "tool", "define", "bridge", or "const" declaration, got: ${firstLine}`);
|
|
72
99
|
}
|
|
73
100
|
}
|
|
74
101
|
}
|
|
@@ -84,7 +111,30 @@ function isJsonLiteral(s) {
|
|
|
84
111
|
return /^["\{\[\d]/.test(s) || /^-\d/.test(s) ||
|
|
85
112
|
s === "true" || s === "false" || s === "null";
|
|
86
113
|
}
|
|
87
|
-
function parseBridgeBlock(block, lineOffset) {
|
|
114
|
+
function parseBridgeBlock(block, lineOffset, previousInstructions) {
|
|
115
|
+
// ── Passthrough shorthand: `bridge Type.field with <name>` ──────────
|
|
116
|
+
// Expands into a full bridge that wires all input through the named
|
|
117
|
+
// handle (typically a define) and returns its output directly.
|
|
118
|
+
const shorthandMatch = block.match(/^bridge\s+(\w+)\.(\w+)\s+with\s+(\S+)\s*$/im);
|
|
119
|
+
if (shorthandMatch) {
|
|
120
|
+
const [, sType, sField, sName] = shorthandMatch;
|
|
121
|
+
const sHandle = sName.includes(".") ? sName.substring(sName.lastIndexOf(".") + 1) : sName;
|
|
122
|
+
const expanded = [
|
|
123
|
+
`bridge ${sType}.${sField} {`,
|
|
124
|
+
` with ${sName} as ${sHandle}`,
|
|
125
|
+
` with input`,
|
|
126
|
+
` with output as __out`,
|
|
127
|
+
` ${sHandle} <- input`,
|
|
128
|
+
` __out <- ${sHandle}`,
|
|
129
|
+
`}`,
|
|
130
|
+
].join("\n");
|
|
131
|
+
const result = parseBridgeBlock(expanded, lineOffset, previousInstructions);
|
|
132
|
+
// Tag the bridge instruction with the passthrough name for serialization
|
|
133
|
+
const bridgeInst = result.find((i) => i.kind === "bridge");
|
|
134
|
+
if (bridgeInst)
|
|
135
|
+
bridgeInst.passthrough = sName;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
88
138
|
// Validate mandatory braces: `bridge Foo.bar {` ... `}`
|
|
89
139
|
const rawLines = block.split("\n");
|
|
90
140
|
const keywordIdx = rawLines.findIndex((l) => /^bridge\s/i.test(l.trim()));
|
|
@@ -134,7 +184,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
134
184
|
if (!bridgeType) {
|
|
135
185
|
throw new Error(`Line ${ln(i)}: "with" declaration must come after "bridge" declaration`);
|
|
136
186
|
}
|
|
137
|
-
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters,
|
|
187
|
+
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters, previousInstructions ?? [], ln(i));
|
|
138
188
|
continue;
|
|
139
189
|
}
|
|
140
190
|
// First non-header line — body starts here
|
|
@@ -147,6 +197,8 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
147
197
|
// ── Parse wire lines ────────────────────────────────────────────────
|
|
148
198
|
const wires = [];
|
|
149
199
|
let currentArrayToPath = null;
|
|
200
|
+
let currentIterHandle = null;
|
|
201
|
+
const arrayIterators = {};
|
|
150
202
|
/** Monotonically-increasing index; combined with a high base to produce
|
|
151
203
|
* fork instances that can never collide with regular handle instances. */
|
|
152
204
|
let nextForkSeq = 0;
|
|
@@ -167,7 +219,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
167
219
|
function buildSourceExpr(sourceStr, lineNum, forceOnOutermost) {
|
|
168
220
|
const parts = sourceStr.split(":");
|
|
169
221
|
if (parts.length === 1) {
|
|
170
|
-
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField);
|
|
222
|
+
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField, lineNum);
|
|
171
223
|
}
|
|
172
224
|
// Pipe chain
|
|
173
225
|
const actualSource = parts[parts.length - 1];
|
|
@@ -184,7 +236,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
184
236
|
throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${handleName}". Add 'with <tool> as ${handleName}' to the bridge header.`);
|
|
185
237
|
}
|
|
186
238
|
}
|
|
187
|
-
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField);
|
|
239
|
+
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField, lineNum);
|
|
188
240
|
const reversedTokens = [...tokenChain].reverse();
|
|
189
241
|
for (let idx = 0; idx < reversedTokens.length; idx++) {
|
|
190
242
|
const tok = reversedTokens[idx];
|
|
@@ -205,72 +257,165 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
205
257
|
}
|
|
206
258
|
return prevOutRef; // fork-root ref
|
|
207
259
|
}
|
|
260
|
+
// ── Whether we are inside an element-mapping brace block
|
|
261
|
+
let inElementBlock = false;
|
|
208
262
|
for (let i = bodyStartIndex; i < lines.length; i++) {
|
|
209
263
|
const raw = lines[i];
|
|
210
264
|
const line = raw.trim();
|
|
211
265
|
if (!line || line.startsWith("#")) {
|
|
212
266
|
continue;
|
|
213
267
|
}
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
268
|
+
// Closing brace of an element mapping block `{ ... }`
|
|
269
|
+
if (line === "}") {
|
|
270
|
+
if (inElementBlock) {
|
|
271
|
+
currentArrayToPath = null;
|
|
272
|
+
currentIterHandle = null;
|
|
273
|
+
inElementBlock = false;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
throw new Error(`Line ${ln(i)}: Unexpected "}" — not inside an element block`);
|
|
277
|
+
}
|
|
278
|
+
// Element mapping lines (inside a brace block)
|
|
279
|
+
if (inElementBlock && currentArrayToPath) {
|
|
280
|
+
if (!line.startsWith(".")) {
|
|
281
|
+
throw new Error(`Line ${ln(i)}: Element mapping lines must start with ".": ${line}`);
|
|
282
|
+
}
|
|
283
|
+
// Constant: .target = "value" or .target = value
|
|
284
|
+
const elemConstMatch = line.match(/^\.(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
285
|
+
if (elemConstMatch) {
|
|
286
|
+
const [, fieldName, quotedValue, unquotedValue] = elemConstMatch;
|
|
287
|
+
const value = quotedValue ?? unquotedValue;
|
|
288
|
+
wires.push({
|
|
289
|
+
value,
|
|
290
|
+
to: {
|
|
291
|
+
module: SELF_MODULE,
|
|
292
|
+
type: bridgeType,
|
|
293
|
+
field: bridgeField,
|
|
294
|
+
element: true,
|
|
295
|
+
path: [...currentArrayToPath, ...parsePath(fieldName)],
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// Simple pull: .target <- <iter>.source (element-relative, no fallbacks)
|
|
301
|
+
const iterPfx = `${currentIterHandle}.`;
|
|
302
|
+
const elemRelMatch = line.match(/^\.(\S+)\s*<-\s*(\S+)$/);
|
|
303
|
+
if (elemRelMatch && elemRelMatch[2].startsWith(iterPfx)) {
|
|
304
|
+
const toPath = [...currentArrayToPath, ...parsePath(elemRelMatch[1])];
|
|
305
|
+
const fromPath = parsePath(elemRelMatch[2].slice(iterPfx.length));
|
|
306
|
+
wires.push({
|
|
307
|
+
from: {
|
|
308
|
+
module: SELF_MODULE,
|
|
309
|
+
type: bridgeType,
|
|
310
|
+
field: bridgeField,
|
|
311
|
+
element: true,
|
|
312
|
+
path: fromPath,
|
|
313
|
+
},
|
|
314
|
+
to: {
|
|
315
|
+
module: SELF_MODULE,
|
|
316
|
+
type: bridgeType,
|
|
317
|
+
field: bridgeField,
|
|
318
|
+
path: toPath,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
// Pull with fallbacks: .target <- source || "fallback" ?? errorSrc (relative or handle path)
|
|
324
|
+
const elemArrowMatch = line.match(/^\.(\S+)\s*<-\s*(.+)$/);
|
|
325
|
+
if (elemArrowMatch) {
|
|
326
|
+
const [, toField, rhs] = elemArrowMatch;
|
|
327
|
+
const toPath = [...currentArrayToPath, ...parsePath(toField)];
|
|
328
|
+
const toRef = { module: SELF_MODULE, type: bridgeType, field: bridgeField, path: toPath };
|
|
329
|
+
// Strip ?? tail
|
|
330
|
+
let exprCore = rhs.trim();
|
|
331
|
+
let fallback;
|
|
332
|
+
let fallbackRefStr;
|
|
333
|
+
const qqIdx = exprCore.lastIndexOf(" ?? ");
|
|
334
|
+
if (qqIdx !== -1) {
|
|
335
|
+
const tail = exprCore.slice(qqIdx + 4).trim();
|
|
336
|
+
exprCore = exprCore.slice(0, qqIdx).trim();
|
|
337
|
+
if (isJsonLiteral(tail))
|
|
338
|
+
fallback = tail;
|
|
339
|
+
else
|
|
340
|
+
fallbackRefStr = tail;
|
|
341
|
+
}
|
|
342
|
+
// Split on || — last may be JSON literal (nullFallback)
|
|
343
|
+
const orParts = exprCore.split(" || ").map((s) => s.trim());
|
|
344
|
+
let nullFallback;
|
|
345
|
+
let sourceParts = orParts;
|
|
346
|
+
if (orParts.length > 1 && isJsonLiteral(orParts[orParts.length - 1])) {
|
|
347
|
+
nullFallback = orParts[orParts.length - 1];
|
|
348
|
+
sourceParts = orParts.slice(0, -1);
|
|
349
|
+
}
|
|
350
|
+
let fallbackRef;
|
|
351
|
+
let fallbackInternalWires = [];
|
|
352
|
+
if (fallbackRefStr) {
|
|
353
|
+
const preLen = wires.length;
|
|
354
|
+
fallbackRef = buildSourceExpr(fallbackRefStr, ln(i), false);
|
|
355
|
+
fallbackInternalWires = wires.splice(preLen);
|
|
356
|
+
}
|
|
357
|
+
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
358
|
+
const srcStr = sourceParts[ci];
|
|
359
|
+
const isLast = ci === sourceParts.length - 1;
|
|
360
|
+
// Element-relative source: starts with "<iter>."
|
|
361
|
+
const iterPrefix = `${currentIterHandle}.`;
|
|
362
|
+
let fromRef;
|
|
363
|
+
if (srcStr.startsWith(iterPrefix)) {
|
|
364
|
+
fromRef = {
|
|
365
|
+
module: SELF_MODULE,
|
|
366
|
+
type: bridgeType,
|
|
367
|
+
field: bridgeField,
|
|
368
|
+
element: true,
|
|
369
|
+
path: parsePath(srcStr.slice(iterPrefix.length)),
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
else if (srcStr.startsWith(".")) {
|
|
373
|
+
throw new Error(`Line ${ln(i)}: Use "${currentIterHandle}.field" to reference element fields, not ".field"`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
fromRef = buildSourceExpr(srcStr, ln(i), false);
|
|
377
|
+
}
|
|
378
|
+
const lastAttrs = isLast ? {
|
|
379
|
+
...(nullFallback ? { nullFallback } : {}),
|
|
380
|
+
...(fallback ? { fallback } : {}),
|
|
381
|
+
...(fallbackRef ? { fallbackRef } : {}),
|
|
382
|
+
} : {};
|
|
383
|
+
wires.push({ from: fromRef, to: toRef, ...lastAttrs });
|
|
384
|
+
}
|
|
385
|
+
wires.push(...fallbackInternalWires);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
throw new Error(`Line ${ln(i)}: Invalid element mapping line: ${line}`);
|
|
238
389
|
}
|
|
239
|
-
// End of array mapping block
|
|
240
|
-
currentArrayToPath = null;
|
|
241
390
|
// Constant wire: target = "value" or target = value (unquoted)
|
|
242
391
|
const constantMatch = line.match(/^(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
243
392
|
if (constantMatch) {
|
|
244
393
|
const [, targetStr, quotedValue, unquotedValue] = constantMatch;
|
|
245
394
|
const value = quotedValue ?? unquotedValue;
|
|
246
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField);
|
|
395
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
247
396
|
wires.push({ value, to: toRef });
|
|
248
397
|
continue;
|
|
249
398
|
}
|
|
250
399
|
// ── Wire: target <- A [|| B [|| C]] [|| "nullLiteral"] [?? errorSrc|"errorLiteral"]
|
|
251
|
-
//
|
|
252
|
-
// A, B, C are source expressions (handle.path or pipe|chain|src).
|
|
253
|
-
// `||` separates null-coalescing alternatives — evaluated left to right;
|
|
254
|
-
// the last alternative may be a JSON literal (→ nullFallback).
|
|
255
|
-
// `??` is the error fallback — fires when ALL sources throw.
|
|
256
|
-
// Can be a JSON literal (→ fallback) or a source expression (→ fallbackRef).
|
|
257
|
-
//
|
|
258
|
-
// Each `||` source becomes a separate wire targeting the same field.
|
|
259
|
-
// The last source wire carries nullFallback + error-fallback attributes.
|
|
260
|
-
// Desugars transparently: serializer emits back as multiple wire lines.
|
|
261
400
|
const arrowMatch = line.match(/^(\S+)\s*<-(!?)\s*(.+)$/);
|
|
262
401
|
if (arrowMatch) {
|
|
263
402
|
const [, targetStr, forceFlag, rhs] = arrowMatch;
|
|
264
403
|
const force = forceFlag === "!";
|
|
265
404
|
const rhsTrimmed = rhs.trim();
|
|
266
|
-
// ── Array mapping: target
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
405
|
+
// ── Array mapping: target <- source[] as <iter> {
|
|
406
|
+
// Opens a brace-delimited element block.
|
|
407
|
+
const arrayBraceMatch = rhsTrimmed.match(/^(\S+)\[\]\s+as\s+(\w+)\s*\{\s*$/);
|
|
408
|
+
if (arrayBraceMatch) {
|
|
409
|
+
const fromClean = arrayBraceMatch[1];
|
|
410
|
+
const iterHandle = arrayBraceMatch[2];
|
|
411
|
+
assertNotReserved(iterHandle, ln(i), "iterator handle");
|
|
412
|
+
const fromRef = resolveAddress(fromClean, handleRes, bridgeType, bridgeField, ln(i));
|
|
413
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
272
414
|
wires.push({ from: fromRef, to: toRef });
|
|
273
415
|
currentArrayToPath = toRef.path;
|
|
416
|
+
currentIterHandle = iterHandle;
|
|
417
|
+
arrayIterators[toRef.path[0]] = iterHandle;
|
|
418
|
+
inElementBlock = true;
|
|
274
419
|
continue;
|
|
275
420
|
}
|
|
276
421
|
// ── Strip the ?? tail (last " ?? " wins in case source contains " ?? ")
|
|
@@ -300,38 +445,27 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
300
445
|
if (sourceParts.length === 0) {
|
|
301
446
|
throw new Error(`Line ${ln(i)}: Wire has no source expression: ${line}`);
|
|
302
447
|
}
|
|
303
|
-
// ── Parse the ?? source/pipe into a fallbackRef (if needed)
|
|
304
|
-
// Wires added by buildSourceExpr for the fallback fork are deferred and
|
|
305
|
-
// pushed AFTER the source wires so that wire order is stable across
|
|
306
|
-
// parse → serialize → re-parse cycles.
|
|
307
448
|
let fallbackRef;
|
|
308
449
|
let fallbackInternalWires = [];
|
|
309
450
|
if (fallbackRefStr) {
|
|
310
451
|
const preLen = wires.length;
|
|
311
452
|
fallbackRef = buildSourceExpr(fallbackRefStr, ln(i), false);
|
|
312
|
-
// Splice out internal wires buildSourceExpr just added; push after sources.
|
|
313
453
|
fallbackInternalWires = wires.splice(preLen);
|
|
314
454
|
}
|
|
315
|
-
|
|
316
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField);
|
|
455
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
317
456
|
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
318
457
|
const isFirst = ci === 0;
|
|
319
458
|
const isLast = ci === sourceParts.length - 1;
|
|
320
459
|
const srcStr = sourceParts[ci];
|
|
321
|
-
// Parse source expression; for pipe chains buildSourceExpr pushes
|
|
322
|
-
// intermediate wires and returns the fork-root ref.
|
|
323
460
|
const termRef = buildSourceExpr(srcStr, ln(i), force && isFirst);
|
|
324
461
|
const isPipeFork = termRef.instance != null && termRef.path.length === 0
|
|
325
462
|
&& srcStr.includes(":");
|
|
326
|
-
// attrs carried only on the LAST wire of the coalesce chain
|
|
327
463
|
const lastAttrs = isLast ? {
|
|
328
464
|
...(nullFallback ? { nullFallback } : {}),
|
|
329
465
|
...(fallback ? { fallback } : {}),
|
|
330
466
|
...(fallbackRef ? { fallbackRef } : {}),
|
|
331
467
|
} : {};
|
|
332
468
|
if (isPipeFork) {
|
|
333
|
-
// Terminal pipe wire: fork-root → target (force only on outermost
|
|
334
|
-
// intermediate wire, already set inside buildSourceExpr)
|
|
335
469
|
wires.push({ from: termRef, to: toRef, pipe: true, ...lastAttrs });
|
|
336
470
|
}
|
|
337
471
|
else {
|
|
@@ -343,19 +477,29 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
343
477
|
});
|
|
344
478
|
}
|
|
345
479
|
}
|
|
346
|
-
// Push fallbackRef internal wires after all source wires (stable round-trip
|
|
347
|
-
// order: same position whether parsed inline or from serialized separate lines)
|
|
348
480
|
wires.push(...fallbackInternalWires);
|
|
349
481
|
continue;
|
|
350
482
|
}
|
|
351
483
|
throw new Error(`Line ${ln(i)}: Unrecognized line: ${line}`);
|
|
352
484
|
}
|
|
485
|
+
// ── Inline define invocations ───────────────────────────────────────
|
|
486
|
+
const nextForkSeqRef = { value: nextForkSeq };
|
|
487
|
+
for (const hb of handleBindings) {
|
|
488
|
+
if (hb.kind !== "define")
|
|
489
|
+
continue;
|
|
490
|
+
const def = previousInstructions?.find((inst) => inst.kind === "define" && inst.name === hb.name);
|
|
491
|
+
if (!def) {
|
|
492
|
+
throw new Error(`Define "${hb.name}" referenced by handle "${hb.handle}" not found`);
|
|
493
|
+
}
|
|
494
|
+
inlineDefine(hb.handle, def, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef);
|
|
495
|
+
}
|
|
353
496
|
instructions.unshift({
|
|
354
497
|
kind: "bridge",
|
|
355
498
|
type: bridgeType,
|
|
356
499
|
field: bridgeField,
|
|
357
500
|
handles: handleBindings,
|
|
358
501
|
wires,
|
|
502
|
+
arrayIterators: Object.keys(arrayIterators).length > 0 ? arrayIterators : undefined,
|
|
359
503
|
pipeHandles: pipeHandleEntries.length > 0 ? pipeHandleEntries : undefined,
|
|
360
504
|
});
|
|
361
505
|
return instructions;
|
|
@@ -390,6 +534,45 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
390
534
|
});
|
|
391
535
|
return;
|
|
392
536
|
}
|
|
537
|
+
// with input (shorthand — handle defaults to "input")
|
|
538
|
+
match = line.match(/^with\s+input$/i);
|
|
539
|
+
if (match) {
|
|
540
|
+
const handle = "input";
|
|
541
|
+
checkDuplicate(handle);
|
|
542
|
+
handleBindings.push({ handle, kind: "input" });
|
|
543
|
+
handleRes.set(handle, {
|
|
544
|
+
module: SELF_MODULE,
|
|
545
|
+
type: bridgeType,
|
|
546
|
+
field: bridgeField,
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
// with output as <handle>
|
|
551
|
+
match = line.match(/^with\s+output\s+as\s+(\w+)$/i);
|
|
552
|
+
if (match) {
|
|
553
|
+
const handle = match[1];
|
|
554
|
+
checkDuplicate(handle);
|
|
555
|
+
handleBindings.push({ handle, kind: "output" });
|
|
556
|
+
handleRes.set(handle, {
|
|
557
|
+
module: SELF_MODULE,
|
|
558
|
+
type: bridgeType,
|
|
559
|
+
field: bridgeField,
|
|
560
|
+
});
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
// with output (shorthand — handle defaults to "output")
|
|
564
|
+
match = line.match(/^with\s+output$/i);
|
|
565
|
+
if (match) {
|
|
566
|
+
const handle = "output";
|
|
567
|
+
checkDuplicate(handle);
|
|
568
|
+
handleBindings.push({ handle, kind: "output" });
|
|
569
|
+
handleRes.set(handle, {
|
|
570
|
+
module: SELF_MODULE,
|
|
571
|
+
type: bridgeType,
|
|
572
|
+
field: bridgeField,
|
|
573
|
+
});
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
393
576
|
// with context as <handle>
|
|
394
577
|
match = line.match(/^with\s+context\s+as\s+(\w+)$/i);
|
|
395
578
|
if (match) {
|
|
@@ -442,13 +625,24 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
442
625
|
});
|
|
443
626
|
return;
|
|
444
627
|
}
|
|
445
|
-
// with <name> as <handle> —
|
|
628
|
+
// with <name> as <handle> — check for define invocation first
|
|
446
629
|
match = line.match(/^with\s+(\S+)\s+as\s+(\w+)$/i);
|
|
447
630
|
if (match) {
|
|
448
631
|
const name = match[1];
|
|
449
632
|
const handle = match[2];
|
|
450
633
|
checkDuplicate(handle);
|
|
451
|
-
|
|
634
|
+
assertNotReserved(handle, lineNum, "handle alias");
|
|
635
|
+
// Check if name matches a known define
|
|
636
|
+
const defineDef = instructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
637
|
+
if (defineDef) {
|
|
638
|
+
handleBindings.push({ handle, kind: "define", name });
|
|
639
|
+
handleRes.set(handle, {
|
|
640
|
+
module: `__define_${handle}`,
|
|
641
|
+
type: bridgeType,
|
|
642
|
+
field: bridgeField,
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
452
646
|
const lastDot = name.lastIndexOf(".");
|
|
453
647
|
if (lastDot !== -1) {
|
|
454
648
|
const modulePart = name.substring(0, lastDot);
|
|
@@ -487,6 +681,17 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
487
681
|
const lastDot = name.lastIndexOf(".");
|
|
488
682
|
const handle = lastDot !== -1 ? name.substring(lastDot + 1) : name;
|
|
489
683
|
checkDuplicate(handle);
|
|
684
|
+
// Check if name matches a known define
|
|
685
|
+
const defineDef = instructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
686
|
+
if (defineDef) {
|
|
687
|
+
handleBindings.push({ handle, kind: "define", name });
|
|
688
|
+
handleRes.set(handle, {
|
|
689
|
+
module: `__define_${handle}`,
|
|
690
|
+
type: bridgeType,
|
|
691
|
+
field: bridgeField,
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
490
695
|
if (lastDot !== -1) {
|
|
491
696
|
const modulePart = name.substring(0, lastDot);
|
|
492
697
|
const fieldPart = name.substring(lastDot + 1);
|
|
@@ -507,6 +712,155 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
507
712
|
}
|
|
508
713
|
throw new Error(`Line ${lineNum}: Invalid with declaration: ${line}`);
|
|
509
714
|
}
|
|
715
|
+
// ── Define inlining ─────────────────────────────────────────────────────────
|
|
716
|
+
/**
|
|
717
|
+
* Inline a define invocation into a bridge's wires.
|
|
718
|
+
*
|
|
719
|
+
* Splits the define handle into separate input/output synthetic trunks,
|
|
720
|
+
* clones the define's internal wires with remapped references, and adds
|
|
721
|
+
* them to the bridge. Tool instances are remapped to avoid collisions.
|
|
722
|
+
*
|
|
723
|
+
* The executor treats synthetic trunks (module starting with `__define_`)
|
|
724
|
+
* as pass-through data containers — no tool function is called.
|
|
725
|
+
*/
|
|
726
|
+
function inlineDefine(defineHandle, defineDef, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef) {
|
|
727
|
+
const genericModule = `__define_${defineHandle}`;
|
|
728
|
+
const inModule = `__define_in_${defineHandle}`;
|
|
729
|
+
const outModule = `__define_out_${defineHandle}`;
|
|
730
|
+
// The define was parsed as synthetic `bridge Define.<name>`, so its
|
|
731
|
+
// internal refs use type="Define", field=defineName for I/O, and
|
|
732
|
+
// standard tool resolutions for tools.
|
|
733
|
+
const defType = "Define";
|
|
734
|
+
const defField = defineDef.name;
|
|
735
|
+
// ── 1. Build trunk remapping for define's tool handles ──────────────
|
|
736
|
+
// Replay define's instance counter to determine original instances
|
|
737
|
+
const defCounters = new Map();
|
|
738
|
+
const trunkRemap = new Map();
|
|
739
|
+
for (const hb of defineDef.handles) {
|
|
740
|
+
if (hb.kind === "input" || hb.kind === "output" || hb.kind === "context" || hb.kind === "const")
|
|
741
|
+
continue;
|
|
742
|
+
if (hb.kind === "define")
|
|
743
|
+
continue; // nested defines — future
|
|
744
|
+
const name = hb.kind === "tool" ? hb.name : "";
|
|
745
|
+
if (!name)
|
|
746
|
+
continue;
|
|
747
|
+
const lastDot = name.lastIndexOf(".");
|
|
748
|
+
let oldModule, oldType, oldField, instanceKey, bridgeKey;
|
|
749
|
+
if (lastDot !== -1) {
|
|
750
|
+
oldModule = name.substring(0, lastDot);
|
|
751
|
+
oldType = defType;
|
|
752
|
+
oldField = name.substring(lastDot + 1);
|
|
753
|
+
instanceKey = `${oldModule}:${oldField}`;
|
|
754
|
+
bridgeKey = instanceKey;
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
oldModule = SELF_MODULE;
|
|
758
|
+
oldType = "Tools";
|
|
759
|
+
oldField = name;
|
|
760
|
+
instanceKey = `Tools:${name}`;
|
|
761
|
+
bridgeKey = instanceKey;
|
|
762
|
+
}
|
|
763
|
+
// Old instance (from define's isolated counter)
|
|
764
|
+
const oldInstance = (defCounters.get(instanceKey) ?? 0) + 1;
|
|
765
|
+
defCounters.set(instanceKey, oldInstance);
|
|
766
|
+
// New instance (from bridge's counter)
|
|
767
|
+
const newInstance = (instanceCounters.get(bridgeKey) ?? 0) + 1;
|
|
768
|
+
instanceCounters.set(bridgeKey, newInstance);
|
|
769
|
+
const oldKey = `${oldModule}:${oldType}:${oldField}:${oldInstance}`;
|
|
770
|
+
trunkRemap.set(oldKey, { module: oldModule, type: oldType, field: oldField, instance: newInstance });
|
|
771
|
+
// Add internal tool handle to bridge's handle bindings (namespaced)
|
|
772
|
+
handleBindings.push({ handle: `${defineHandle}$${hb.handle}`, kind: "tool", name });
|
|
773
|
+
}
|
|
774
|
+
// ── 2. Remap bridge wires involving the define handle ───────────────
|
|
775
|
+
for (const wire of wires) {
|
|
776
|
+
if ("from" in wire) {
|
|
777
|
+
if (wire.to.module === genericModule) {
|
|
778
|
+
wire.to = { ...wire.to, module: inModule };
|
|
779
|
+
}
|
|
780
|
+
if (wire.from.module === genericModule) {
|
|
781
|
+
wire.from = { ...wire.from, module: outModule };
|
|
782
|
+
}
|
|
783
|
+
if (wire.fallbackRef?.module === genericModule) {
|
|
784
|
+
wire.fallbackRef = { ...wire.fallbackRef, module: outModule };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if ("value" in wire && wire.to.module === genericModule) {
|
|
788
|
+
wire.to = { ...wire.to, module: inModule };
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// ── 3. Clone, remap, and add define's wires ────────────────────────
|
|
792
|
+
// Compute fork instance offset (define's fork instances start at 100000,
|
|
793
|
+
// bridge's may overlap — offset them to avoid collision)
|
|
794
|
+
const forkOffset = nextForkSeqRef.value;
|
|
795
|
+
let maxDefForkSeq = 0;
|
|
796
|
+
function remapRef(ref, side) {
|
|
797
|
+
// Define I/O trunk → split into input/output synthetic trunks
|
|
798
|
+
if (ref.module === SELF_MODULE && ref.type === defType && ref.field === defField) {
|
|
799
|
+
const targetModule = side === "from" ? inModule : outModule;
|
|
800
|
+
return { ...ref, module: targetModule, type: bridgeType, field: bridgeField };
|
|
801
|
+
}
|
|
802
|
+
// Tool trunk → remap instance
|
|
803
|
+
const key = `${ref.module}:${ref.type}:${ref.field}:${ref.instance ?? ""}`;
|
|
804
|
+
const newTrunk = trunkRemap.get(key);
|
|
805
|
+
if (newTrunk) {
|
|
806
|
+
return { ...ref, module: newTrunk.module, type: newTrunk.type, field: newTrunk.field, instance: newTrunk.instance };
|
|
807
|
+
}
|
|
808
|
+
// Fork instance → offset (fork instances are >= 100000)
|
|
809
|
+
if (ref.instance != null && ref.instance >= 100000) {
|
|
810
|
+
const defSeq = ref.instance - 100000;
|
|
811
|
+
if (defSeq + 1 > maxDefForkSeq)
|
|
812
|
+
maxDefForkSeq = defSeq + 1;
|
|
813
|
+
return { ...ref, instance: ref.instance + forkOffset };
|
|
814
|
+
}
|
|
815
|
+
return ref;
|
|
816
|
+
}
|
|
817
|
+
for (const wire of defineDef.wires) {
|
|
818
|
+
const cloned = JSON.parse(JSON.stringify(wire));
|
|
819
|
+
if ("from" in cloned) {
|
|
820
|
+
cloned.from = remapRef(cloned.from, "from");
|
|
821
|
+
cloned.to = remapRef(cloned.to, "to");
|
|
822
|
+
if (cloned.fallbackRef) {
|
|
823
|
+
cloned.fallbackRef = remapRef(cloned.fallbackRef, "from");
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
// Constant wire
|
|
828
|
+
cloned.to = remapRef(cloned.to, "to");
|
|
829
|
+
}
|
|
830
|
+
wires.push(cloned);
|
|
831
|
+
}
|
|
832
|
+
// Advance bridge's fork counter past define's forks
|
|
833
|
+
nextForkSeqRef.value += maxDefForkSeq;
|
|
834
|
+
// ── 4. Remap and merge pipe handles ─────────────────────────────────
|
|
835
|
+
if (defineDef.pipeHandles) {
|
|
836
|
+
for (const ph of defineDef.pipeHandles) {
|
|
837
|
+
const parts = ph.key.split(":");
|
|
838
|
+
// key format: "module:type:field:instance"
|
|
839
|
+
const phInstance = parseInt(parts[parts.length - 1]);
|
|
840
|
+
let newKey = ph.key;
|
|
841
|
+
if (phInstance >= 100000) {
|
|
842
|
+
const newInst = phInstance + forkOffset;
|
|
843
|
+
parts[parts.length - 1] = String(newInst);
|
|
844
|
+
newKey = parts.join(":");
|
|
845
|
+
}
|
|
846
|
+
// Remap baseTrunk
|
|
847
|
+
const bt = ph.baseTrunk;
|
|
848
|
+
const btKey = `${bt.module}:${defType}:${bt.field}:${bt.instance ?? ""}`;
|
|
849
|
+
const newBt = trunkRemap.get(btKey);
|
|
850
|
+
// Also try with Tools type for simple names
|
|
851
|
+
const btKey2 = `${bt.module}:Tools:${bt.field}:${bt.instance ?? ""}`;
|
|
852
|
+
const newBt2 = trunkRemap.get(btKey2);
|
|
853
|
+
const resolvedBt = newBt ?? newBt2;
|
|
854
|
+
pipeHandleEntries.push({
|
|
855
|
+
key: newKey,
|
|
856
|
+
handle: `${defineHandle}$${ph.handle}`,
|
|
857
|
+
baseTrunk: resolvedBt
|
|
858
|
+
? { module: resolvedBt.module, type: resolvedBt.type, field: resolvedBt.field, instance: resolvedBt.instance }
|
|
859
|
+
: ph.baseTrunk,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
510
864
|
/**
|
|
511
865
|
* Resolve an address string into a structured NodeRef.
|
|
512
866
|
*
|
|
@@ -516,7 +870,7 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
516
870
|
* 3. Prefix matches a declared handle → resolve via handle binding
|
|
517
871
|
* 4. Otherwise → nested output path (e.g., topPick.address)
|
|
518
872
|
*/
|
|
519
|
-
function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
873
|
+
function resolveAddress(address, handles, bridgeType, bridgeField, lineNum) {
|
|
520
874
|
const dotIndex = address.indexOf(".");
|
|
521
875
|
if (dotIndex === -1) {
|
|
522
876
|
// Whole address is a declared handle → resolve to its root (path: [])
|
|
@@ -532,13 +886,9 @@ function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
|
532
886
|
ref.instance = resolution.instance;
|
|
533
887
|
return ref;
|
|
534
888
|
}
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
type: bridgeType,
|
|
539
|
-
field: bridgeField,
|
|
540
|
-
path: parsePath(address),
|
|
541
|
-
};
|
|
889
|
+
// Strict scoping: every reference must go through a declared handle.
|
|
890
|
+
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared reference "${address}". ` +
|
|
891
|
+
`Add 'with output as o' for output fields, or 'with ${address}' for a tool.`);
|
|
542
892
|
}
|
|
543
893
|
const prefix = address.substring(0, dotIndex);
|
|
544
894
|
const rest = address.substring(dotIndex + 1);
|
|
@@ -557,23 +907,9 @@ function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
|
557
907
|
}
|
|
558
908
|
return ref;
|
|
559
909
|
}
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (prefix === bridgeField) {
|
|
564
|
-
return {
|
|
565
|
-
module: SELF_MODULE,
|
|
566
|
-
type: bridgeType,
|
|
567
|
-
field: bridgeField,
|
|
568
|
-
path: pathParts,
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
return {
|
|
572
|
-
module: SELF_MODULE,
|
|
573
|
-
type: bridgeType,
|
|
574
|
-
field: bridgeField,
|
|
575
|
-
path: [prefix, ...pathParts],
|
|
576
|
-
};
|
|
910
|
+
// Strict scoping: prefix must be a known handle.
|
|
911
|
+
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared handle "${prefix}". ` +
|
|
912
|
+
`Add 'with ${prefix}' or 'with ${prefix} as ${prefix}' to the bridge header.`);
|
|
577
913
|
}
|
|
578
914
|
// ── Const block parser ──────────────────────────────────────────────────────
|
|
579
915
|
/**
|
|
@@ -604,6 +940,7 @@ function parseConstLines(block, lineOffset) {
|
|
|
604
940
|
throw new Error(`Line ${ln(i)}: Expected const declaration, got: ${line}`);
|
|
605
941
|
}
|
|
606
942
|
const name = constMatch[1];
|
|
943
|
+
assertNotReserved(name, ln(i), "const name");
|
|
607
944
|
let valuePart = constMatch[2].trim();
|
|
608
945
|
// Multi-line: if value starts with { or [ and isn't balanced, read more lines
|
|
609
946
|
if (/^[{[]/.test(valuePart)) {
|
|
@@ -642,38 +979,92 @@ function parseConstLines(block, lineOffset) {
|
|
|
642
979
|
}
|
|
643
980
|
return results;
|
|
644
981
|
}
|
|
645
|
-
// ──
|
|
982
|
+
// ── Define block parser ─────────────────────────────────────────────────────
|
|
646
983
|
/**
|
|
647
|
-
* Parse a `
|
|
984
|
+
* Parse a `define` block into a DefineDef instruction.
|
|
648
985
|
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* with context
|
|
652
|
-
* baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
653
|
-
* headers.apiKey <- context.hereapi.apiKey
|
|
986
|
+
* Delegates to parseBridgeBlock with a synthetic `bridge Define.<name>` header,
|
|
987
|
+
* then converts the resulting Bridge to a DefineDef template.
|
|
654
988
|
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
989
|
+
* Example:
|
|
990
|
+
* define secureProfile {
|
|
991
|
+
* with userApi as api
|
|
992
|
+
* with input as i
|
|
993
|
+
* with output as o
|
|
994
|
+
* api.id <- i.userId
|
|
995
|
+
* o.name <- api.login
|
|
996
|
+
* }
|
|
997
|
+
*/
|
|
998
|
+
function parseDefineBlock(block, lineOffset) {
|
|
999
|
+
const rawLines = block.split("\n");
|
|
1000
|
+
// Find the define header line
|
|
1001
|
+
let headerIdx = -1;
|
|
1002
|
+
let defineName = "";
|
|
1003
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
1004
|
+
const line = rawLines[i].trim();
|
|
1005
|
+
if (!line || line.startsWith("#"))
|
|
1006
|
+
continue;
|
|
1007
|
+
const m = line.match(/^define\s+(\w+)\s*\{?\s*$/i);
|
|
1008
|
+
if (!m) {
|
|
1009
|
+
throw new Error(`Line ${lineOffset + i + 1}: Expected define declaration: define <name> {. Got: ${line}`);
|
|
1010
|
+
}
|
|
1011
|
+
defineName = m[1];
|
|
1012
|
+
assertNotReserved(defineName, lineOffset + i + 1, "define name");
|
|
1013
|
+
headerIdx = i;
|
|
1014
|
+
break;
|
|
1015
|
+
}
|
|
1016
|
+
if (!defineName) {
|
|
1017
|
+
throw new Error(`Line ${lineOffset + 1}: Missing define declaration`);
|
|
1018
|
+
}
|
|
1019
|
+
// Validate braces
|
|
1020
|
+
const kw = rawLines[headerIdx].trim();
|
|
1021
|
+
if (!kw.endsWith("{")) {
|
|
1022
|
+
throw new Error(`Line ${lineOffset + headerIdx + 1}: define block must use braces: define ${defineName} {`);
|
|
1023
|
+
}
|
|
1024
|
+
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
1025
|
+
if (!hasClose) {
|
|
1026
|
+
throw new Error(`Line ${lineOffset + headerIdx + 1}: define block missing closing }`);
|
|
1027
|
+
}
|
|
1028
|
+
// Rewrite header to a synthetic bridge: `bridge Define.<name> {`
|
|
1029
|
+
const syntheticLines = [...rawLines];
|
|
1030
|
+
syntheticLines[headerIdx] = rawLines[headerIdx]
|
|
1031
|
+
.replace(/^(\s*)define\s+\w+/i, `$1bridge Define.${defineName}`);
|
|
1032
|
+
const syntheticBlock = syntheticLines.join("\n");
|
|
1033
|
+
const results = parseBridgeBlock(syntheticBlock, lineOffset);
|
|
1034
|
+
const bridge = results[0];
|
|
1035
|
+
return {
|
|
1036
|
+
kind: "define",
|
|
1037
|
+
name: defineName,
|
|
1038
|
+
handles: bridge.handles,
|
|
1039
|
+
wires: bridge.wires,
|
|
1040
|
+
...(bridge.arrayIterators ? { arrayIterators: bridge.arrayIterators } : {}),
|
|
1041
|
+
...(bridge.pipeHandles ? { pipeHandles: bridge.pipeHandles } : {}),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
// ── Tool block parser ───────────────────────────────────────────────────────
|
|
1045
|
+
/**
|
|
1046
|
+
* Parse a `tool` block into a ToolDef instruction.
|
|
659
1047
|
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
1048
|
+
* Format:
|
|
1049
|
+
* tool hereapi from httpCall {
|
|
662
1050
|
* with context
|
|
663
|
-
* baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1051
|
+
* .baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1052
|
+
* .headers.apiKey <- context.hereapi.apiKey
|
|
1053
|
+
* }
|
|
664
1054
|
*
|
|
665
|
-
*
|
|
666
|
-
* method = GET
|
|
667
|
-
* path = /geocode
|
|
1055
|
+
* tool hereapi.geocode from hereapi {
|
|
1056
|
+
* .method = GET
|
|
1057
|
+
* .path = /geocode
|
|
1058
|
+
* }
|
|
668
1059
|
*
|
|
669
|
-
* When
|
|
670
|
-
* it's treated as
|
|
1060
|
+
* When the source matches a previously-defined tool name,
|
|
1061
|
+
* it's treated as inheritance (child inherits parent). Otherwise the source
|
|
671
1062
|
* is treated as a function name.
|
|
672
1063
|
*/
|
|
673
1064
|
function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
674
1065
|
// Validate mandatory braces for blocks that have a body (deps / wires)
|
|
675
1066
|
const rawLines = block.split("\n");
|
|
676
|
-
const keywordIdx = rawLines.findIndex((l) => /^
|
|
1067
|
+
const keywordIdx = rawLines.findIndex((l) => /^tool\s/i.test(l.trim()));
|
|
677
1068
|
if (keywordIdx !== -1) {
|
|
678
1069
|
// Check if there are non-blank, non-comment body lines after the keyword
|
|
679
1070
|
const bodyLines = rawLines.slice(keywordIdx + 1).filter((l) => {
|
|
@@ -683,11 +1074,11 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
683
1074
|
const kw = rawLines[keywordIdx].trim();
|
|
684
1075
|
if (bodyLines.length > 0) {
|
|
685
1076
|
if (!kw.endsWith("{")) {
|
|
686
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}:
|
|
1077
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block with body must use braces: tool foo from bar {`);
|
|
687
1078
|
}
|
|
688
1079
|
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
689
1080
|
if (!hasClose) {
|
|
690
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}:
|
|
1081
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block missing closing }`);
|
|
691
1082
|
}
|
|
692
1083
|
}
|
|
693
1084
|
}
|
|
@@ -696,7 +1087,7 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
696
1087
|
const trimmed = l.trimEnd();
|
|
697
1088
|
if (trimmed === "}")
|
|
698
1089
|
return "";
|
|
699
|
-
if (/^
|
|
1090
|
+
if (/^tool\s/i.test(trimmed) && trimmed.endsWith("{"))
|
|
700
1091
|
return trimmed.replace(/\s*\{\s*$/, "");
|
|
701
1092
|
return trimmed;
|
|
702
1093
|
});
|
|
@@ -712,31 +1103,16 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
712
1103
|
const line = raw.trim();
|
|
713
1104
|
if (!line || line.startsWith("#"))
|
|
714
1105
|
continue;
|
|
715
|
-
// Tool declaration: tool <name>
|
|
1106
|
+
// Tool declaration: tool <name> from <source>
|
|
716
1107
|
if (/^tool\s/i.test(line)) {
|
|
717
|
-
const
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
toolExtends = extendsMatch[2];
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
const fnMatch = line.match(/^tool\s+(\S+)\s+(\S+)$/i);
|
|
724
|
-
if (fnMatch) {
|
|
725
|
-
toolName = fnMatch[1];
|
|
726
|
-
toolFn = fnMatch[2];
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
throw new Error(`Line ${ln(i)}: Invalid tool declaration: ${line}`);
|
|
730
|
-
}
|
|
731
|
-
// Extend declaration: extend <source> as <name>
|
|
732
|
-
if (/^extend\s/i.test(line)) {
|
|
733
|
-
const extendMatch = line.match(/^extend\s+(\S+)\s+as\s+(\S+)$/i);
|
|
734
|
-
if (!extendMatch) {
|
|
735
|
-
throw new Error(`Line ${ln(i)}: Invalid extend declaration: ${line}. Expected: extend <source> as <name>`);
|
|
1108
|
+
const toolMatch = line.match(/^tool\s+(\S+)\s+from\s+(\S+)$/i);
|
|
1109
|
+
if (!toolMatch) {
|
|
1110
|
+
throw new Error(`Line ${ln(i)}: Invalid tool declaration: ${line}. Expected: tool <name> from <source>`);
|
|
736
1111
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1112
|
+
toolName = toolMatch[1];
|
|
1113
|
+
const source = toolMatch[2];
|
|
1114
|
+
assertNotReserved(toolName, ln(i), "tool name");
|
|
1115
|
+
// If source matches a previously-defined tool, it's inheritance; otherwise it's a function name
|
|
740
1116
|
const isKnownTool = previousInstructions?.some((inst) => inst.kind === "tool" && inst.name === source);
|
|
741
1117
|
if (isKnownTool) {
|
|
742
1118
|
toolExtends = source;
|
|
@@ -800,8 +1176,8 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
800
1176
|
wires.push({ kind: "onError", source: onErrorPullMatch[1] });
|
|
801
1177
|
continue;
|
|
802
1178
|
}
|
|
803
|
-
// Constant wire: target = "value" or target = value (unquoted)
|
|
804
|
-
const constantMatch = line.match(
|
|
1179
|
+
// Constant wire: .target = "value" or .target = value (unquoted)
|
|
1180
|
+
const constantMatch = line.match(/^\.(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
805
1181
|
if (constantMatch) {
|
|
806
1182
|
const value = constantMatch[2] ?? constantMatch[3];
|
|
807
1183
|
wires.push({
|
|
@@ -811,12 +1187,16 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
811
1187
|
});
|
|
812
1188
|
continue;
|
|
813
1189
|
}
|
|
814
|
-
// Pull wire: target <- source
|
|
815
|
-
const pullMatch = line.match(
|
|
1190
|
+
// Pull wire: .target <- source
|
|
1191
|
+
const pullMatch = line.match(/^\.(\S+)\s*<-\s*(\S+)$/);
|
|
816
1192
|
if (pullMatch) {
|
|
817
1193
|
wires.push({ target: pullMatch[1], kind: "pull", source: pullMatch[2] });
|
|
818
1194
|
continue;
|
|
819
1195
|
}
|
|
1196
|
+
// Catch bare param lines without leading dot — give a helpful error
|
|
1197
|
+
if (/^[a-zA-Z]/.test(line)) {
|
|
1198
|
+
throw new Error(`Line ${ln(i)}: Tool params require a dot prefix: ".${line.split(/[\s=<]/)[0]} ...". Only 'with' and 'on error' lines are unprefixed.`);
|
|
1199
|
+
}
|
|
820
1200
|
throw new Error(`Line ${ln(i)}: Unrecognized tool line: ${line}`);
|
|
821
1201
|
}
|
|
822
1202
|
if (!toolName)
|
|
@@ -862,7 +1242,8 @@ export function serializeBridge(instructions) {
|
|
|
862
1242
|
const bridges = instructions.filter((i) => i.kind === "bridge");
|
|
863
1243
|
const tools = instructions.filter((i) => i.kind === "tool");
|
|
864
1244
|
const consts = instructions.filter((i) => i.kind === "const");
|
|
865
|
-
|
|
1245
|
+
const defines = instructions.filter((i) => i.kind === "define");
|
|
1246
|
+
if (bridges.length === 0 && tools.length === 0 && consts.length === 0 && defines.length === 0)
|
|
866
1247
|
return "";
|
|
867
1248
|
const blocks = [];
|
|
868
1249
|
// Group const declarations into a single block
|
|
@@ -872,21 +1253,20 @@ export function serializeBridge(instructions) {
|
|
|
872
1253
|
for (const tool of tools) {
|
|
873
1254
|
blocks.push(serializeToolBlock(tool));
|
|
874
1255
|
}
|
|
1256
|
+
for (const def of defines) {
|
|
1257
|
+
blocks.push(serializeDefineBlock(def));
|
|
1258
|
+
}
|
|
875
1259
|
for (const bridge of bridges) {
|
|
876
1260
|
blocks.push(serializeBridgeBlock(bridge));
|
|
877
1261
|
}
|
|
878
|
-
return blocks.join("\n\n") + "\n";
|
|
1262
|
+
return `version ${BRIDGE_VERSION}\n\n` + blocks.join("\n\n") + "\n";
|
|
879
1263
|
}
|
|
880
1264
|
function serializeToolBlock(tool) {
|
|
881
1265
|
const lines = [];
|
|
882
1266
|
const hasBody = tool.deps.length > 0 || tool.wires.length > 0;
|
|
883
|
-
// Declaration line — use `
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
887
|
-
else {
|
|
888
|
-
lines.push(hasBody ? `extend ${tool.fn} as ${tool.name} {` : `extend ${tool.fn} as ${tool.name}`);
|
|
889
|
-
}
|
|
1267
|
+
// Declaration line — use `tool <name> from <source>` format
|
|
1268
|
+
const source = tool.extends ?? tool.fn;
|
|
1269
|
+
lines.push(hasBody ? `tool ${tool.name} from ${source} {` : `tool ${tool.name} from ${source}`);
|
|
890
1270
|
// Dependencies
|
|
891
1271
|
for (const dep of tool.deps) {
|
|
892
1272
|
if (dep.kind === "context") {
|
|
@@ -922,14 +1302,14 @@ function serializeToolBlock(tool) {
|
|
|
922
1302
|
else if (wire.kind === "constant") {
|
|
923
1303
|
// Use quoted form if value contains spaces or special chars, unquoted otherwise
|
|
924
1304
|
if (/\s/.test(wire.value) || wire.value === "") {
|
|
925
|
-
lines.push(`
|
|
1305
|
+
lines.push(` .${wire.target} = "${wire.value}"`);
|
|
926
1306
|
}
|
|
927
1307
|
else {
|
|
928
|
-
lines.push(`
|
|
1308
|
+
lines.push(` .${wire.target} = ${wire.value}`);
|
|
929
1309
|
}
|
|
930
1310
|
}
|
|
931
1311
|
else {
|
|
932
|
-
lines.push(`
|
|
1312
|
+
lines.push(` .${wire.target} <- ${wire.source}`);
|
|
933
1313
|
}
|
|
934
1314
|
}
|
|
935
1315
|
if (hasBody)
|
|
@@ -946,7 +1326,7 @@ function serializeToolBlock(tool) {
|
|
|
946
1326
|
* This is used to emit `?? handle.path` or `?? pipe:source` for wire
|
|
947
1327
|
* `fallbackRef` values.
|
|
948
1328
|
*/
|
|
949
|
-
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle) {
|
|
1329
|
+
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle) {
|
|
950
1330
|
const refTk = ref.instance != null
|
|
951
1331
|
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
952
1332
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
@@ -977,13 +1357,37 @@ function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge
|
|
|
977
1357
|
}
|
|
978
1358
|
}
|
|
979
1359
|
if (actualSourceRef && handleChain.length > 0) {
|
|
980
|
-
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, true);
|
|
1360
|
+
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, outputHandle, true);
|
|
981
1361
|
return `${handleChain.join(":")}:${sourceStr}`;
|
|
982
1362
|
}
|
|
983
1363
|
}
|
|
984
|
-
return serializeRef(ref, bridge, handleMap, inputHandle, true);
|
|
1364
|
+
return serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, true);
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Serialize a DefineDef into its textual form.
|
|
1368
|
+
*
|
|
1369
|
+
* Delegates to serializeBridgeBlock with a synthetic Bridge, then replaces
|
|
1370
|
+
* the `bridge Define.<name>` header with `define <name>`.
|
|
1371
|
+
*/
|
|
1372
|
+
function serializeDefineBlock(def) {
|
|
1373
|
+
const syntheticBridge = {
|
|
1374
|
+
kind: "bridge",
|
|
1375
|
+
type: "Define",
|
|
1376
|
+
field: def.name,
|
|
1377
|
+
handles: def.handles,
|
|
1378
|
+
wires: def.wires,
|
|
1379
|
+
arrayIterators: def.arrayIterators,
|
|
1380
|
+
pipeHandles: def.pipeHandles,
|
|
1381
|
+
};
|
|
1382
|
+
const bridgeText = serializeBridgeBlock(syntheticBridge);
|
|
1383
|
+
// Replace "bridge Define.<name>" → "define <name>"
|
|
1384
|
+
return bridgeText.replace(/^bridge Define\.(\w+)/, "define $1");
|
|
985
1385
|
}
|
|
986
1386
|
function serializeBridgeBlock(bridge) {
|
|
1387
|
+
// ── Passthrough shorthand ───────────────────────────────────────────
|
|
1388
|
+
if (bridge.passthrough) {
|
|
1389
|
+
return `bridge ${bridge.type}.${bridge.field} with ${bridge.passthrough}`;
|
|
1390
|
+
}
|
|
987
1391
|
const lines = [];
|
|
988
1392
|
// ── Header ──────────────────────────────────────────────────────────
|
|
989
1393
|
lines.push(`bridge ${bridge.type}.${bridge.field} {`);
|
|
@@ -1002,7 +1406,20 @@ function serializeBridgeBlock(bridge) {
|
|
|
1002
1406
|
break;
|
|
1003
1407
|
}
|
|
1004
1408
|
case "input":
|
|
1005
|
-
|
|
1409
|
+
if (h.handle === "input") {
|
|
1410
|
+
lines.push(` with input`);
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
lines.push(` with input as ${h.handle}`);
|
|
1414
|
+
}
|
|
1415
|
+
break;
|
|
1416
|
+
case "output":
|
|
1417
|
+
if (h.handle === "output") {
|
|
1418
|
+
lines.push(` with output`);
|
|
1419
|
+
}
|
|
1420
|
+
else {
|
|
1421
|
+
lines.push(` with output as ${h.handle}`);
|
|
1422
|
+
}
|
|
1006
1423
|
break;
|
|
1007
1424
|
case "context":
|
|
1008
1425
|
lines.push(` with context as ${h.handle}`);
|
|
@@ -1015,31 +1432,28 @@ function serializeBridgeBlock(bridge) {
|
|
|
1015
1432
|
lines.push(` with const as ${h.handle}`);
|
|
1016
1433
|
}
|
|
1017
1434
|
break;
|
|
1435
|
+
case "define":
|
|
1436
|
+
lines.push(` with ${h.name} as ${h.handle}`);
|
|
1437
|
+
break;
|
|
1018
1438
|
}
|
|
1019
1439
|
}
|
|
1020
1440
|
lines.push("");
|
|
1021
1441
|
// Mark where the wire body starts — everything after this gets 2-space indent
|
|
1022
1442
|
const wireBodyStart = lines.length;
|
|
1023
1443
|
// ── Build handle map for reverse resolution ─────────────────────────
|
|
1024
|
-
const { handleMap, inputHandle } = buildHandleMap(bridge);
|
|
1444
|
+
const { handleMap, inputHandle, outputHandle } = buildHandleMap(bridge);
|
|
1025
1445
|
// ── Pipe fork registry ──────────────────────────────────────────────
|
|
1026
|
-
// Extend handleMap with fork → handle-name entries and build the set of
|
|
1027
|
-
// known fork trunk keys so the wire classifiers below can use it.
|
|
1028
1446
|
const pipeHandleTrunkKeys = new Set();
|
|
1029
1447
|
for (const ph of bridge.pipeHandles ?? []) {
|
|
1030
1448
|
handleMap.set(ph.key, ph.handle);
|
|
1031
1449
|
pipeHandleTrunkKeys.add(ph.key);
|
|
1032
1450
|
}
|
|
1033
|
-
// ── Pipe wire detection
|
|
1034
|
-
// Pipe wires are marked pipe:true. Classify them into two maps:
|
|
1035
|
-
// toInMap: forkTrunkKey → wire feeding the fork's input field
|
|
1036
|
-
// fromOutMap: forkTrunkKey → wire reading the fork's root result
|
|
1037
|
-
// Terminal out-wires (destination is NOT another fork) are chain anchors.
|
|
1451
|
+
// ── Pipe wire detection ─────────────────────────────────────────────
|
|
1038
1452
|
const refTrunkKey = (ref) => ref.instance != null
|
|
1039
1453
|
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
1040
1454
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
1041
|
-
const toInMap = new Map();
|
|
1042
|
-
const fromOutMap = new Map();
|
|
1455
|
+
const toInMap = new Map();
|
|
1456
|
+
const fromOutMap = new Map();
|
|
1043
1457
|
const pipeWireSet = new Set();
|
|
1044
1458
|
for (const w of bridge.wires) {
|
|
1045
1459
|
if (!("from" in w) || !w.pipe)
|
|
@@ -1047,70 +1461,94 @@ function serializeBridgeBlock(bridge) {
|
|
|
1047
1461
|
const fw = w;
|
|
1048
1462
|
pipeWireSet.add(w);
|
|
1049
1463
|
const toTk = refTrunkKey(fw.to);
|
|
1050
|
-
// In-wire: single-segment path targeting a known pipe fork
|
|
1051
1464
|
if (fw.to.path.length === 1 && pipeHandleTrunkKeys.has(toTk)) {
|
|
1052
1465
|
toInMap.set(toTk, fw);
|
|
1053
1466
|
}
|
|
1054
|
-
// Out-wire: empty path from a known pipe fork
|
|
1055
1467
|
if (fw.from.path.length === 0 && pipeHandleTrunkKeys.has(refTrunkKey(fw.from))) {
|
|
1056
1468
|
fromOutMap.set(refTrunkKey(fw.from), fw);
|
|
1057
1469
|
}
|
|
1058
1470
|
}
|
|
1059
|
-
// ──
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1471
|
+
// ── Group element wires by array-destination field ──────────────────
|
|
1472
|
+
// Pull wires: from.element=true
|
|
1473
|
+
const elementPullWires = bridge.wires.filter((w) => "from" in w && !!w.from.element);
|
|
1474
|
+
// Constant wires: "value" in w && to.element=true
|
|
1475
|
+
const elementConstWires = bridge.wires.filter((w) => "value" in w && !!w.to.element);
|
|
1476
|
+
// Build grouped maps keyed by the array-destination field name (to.path[0])
|
|
1477
|
+
const elementPullGroups = new Map();
|
|
1478
|
+
const elementConstGroups = new Map();
|
|
1479
|
+
for (const w of elementPullWires) {
|
|
1480
|
+
const key = w.to.path[0];
|
|
1481
|
+
if (!elementPullGroups.has(key))
|
|
1482
|
+
elementPullGroups.set(key, []);
|
|
1483
|
+
elementPullGroups.get(key).push(w);
|
|
1484
|
+
}
|
|
1485
|
+
for (const w of elementConstWires) {
|
|
1486
|
+
const key = w.to.path[0];
|
|
1487
|
+
if (!elementConstGroups.has(key))
|
|
1488
|
+
elementConstGroups.set(key, []);
|
|
1489
|
+
elementConstGroups.get(key).push(w);
|
|
1069
1490
|
}
|
|
1491
|
+
// Union of keys that have any element wire (pull or constant)
|
|
1492
|
+
const allElementKeys = new Set([...elementPullGroups.keys(), ...elementConstGroups.keys()]);
|
|
1493
|
+
// ── Exclude pipe, element-pull, and element-const wires from main loop
|
|
1494
|
+
const regularWires = bridge.wires.filter((w) => !pipeWireSet.has(w) &&
|
|
1495
|
+
(!("from" in w) || !w.from.element) &&
|
|
1496
|
+
(!("value" in w) || !w.to.element));
|
|
1070
1497
|
const serializedArrays = new Set();
|
|
1498
|
+
// ── Helper: serialize a reference (forward outputHandle) ─────────────
|
|
1499
|
+
const sRef = (ref, isFrom) => serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom);
|
|
1500
|
+
const sPipeOrRef = (ref) => serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle);
|
|
1071
1501
|
for (const w of regularWires) {
|
|
1072
1502
|
// Constant wire
|
|
1073
1503
|
if ("value" in w) {
|
|
1074
|
-
const toStr =
|
|
1504
|
+
const toStr = sRef(w.to, false);
|
|
1075
1505
|
lines.push(`${toStr} = "${w.value}"`);
|
|
1076
1506
|
continue;
|
|
1077
1507
|
}
|
|
1078
|
-
// Array mapping
|
|
1508
|
+
// Array mapping — emit brace-delimited element block
|
|
1079
1509
|
const arrayKey = w.to.path.length === 1 ? w.to.path[0] : null;
|
|
1080
|
-
if (arrayKey &&
|
|
1081
|
-
elementGroups.has(arrayKey) &&
|
|
1082
|
-
!serializedArrays.has(arrayKey)) {
|
|
1510
|
+
if (arrayKey && allElementKeys.has(arrayKey) && !serializedArrays.has(arrayKey)) {
|
|
1083
1511
|
serializedArrays.add(arrayKey);
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1512
|
+
const iterName = bridge.arrayIterators?.[arrayKey] ?? "item";
|
|
1513
|
+
const fromStr = sRef(w.from, true) + "[]";
|
|
1514
|
+
const toStr = sRef(w.to, false);
|
|
1515
|
+
lines.push(`${toStr} <- ${fromStr} as ${iterName} {`);
|
|
1516
|
+
// Element constant wires (e.g. .provider = "RENFE")
|
|
1517
|
+
for (const ew of elementConstGroups.get(arrayKey) ?? []) {
|
|
1518
|
+
const fieldPath = ew.to.path.slice(1); // strip arrayKey prefix
|
|
1519
|
+
const elemTo = "." + serPath(fieldPath);
|
|
1520
|
+
lines.push(` ${elemTo} = "${ew.value}"`);
|
|
1521
|
+
}
|
|
1522
|
+
// Element pull wires (e.g. .name <- iter.title)
|
|
1523
|
+
for (const ew of elementPullGroups.get(arrayKey) ?? []) {
|
|
1524
|
+
const fromPart = ew.from.element
|
|
1525
|
+
? iterName + "." + serPath(ew.from.path)
|
|
1526
|
+
: sRef(ew.from, true);
|
|
1089
1527
|
const elemTo = "." + serPath(ew.to.path.slice(1));
|
|
1090
|
-
|
|
1528
|
+
// Handle fallbacks on element pull wires
|
|
1529
|
+
const nfb = "nullFallback" in ew && ew.nullFallback ? ` || ${ew.nullFallback}` : "";
|
|
1530
|
+
const errf = "fallbackRef" in ew && ew.fallbackRef
|
|
1531
|
+
? ` ?? ${sPipeOrRef(ew.fallbackRef)}`
|
|
1532
|
+
: "fallback" in ew && ew.fallback ? ` ?? ${ew.fallback}` : "";
|
|
1533
|
+
lines.push(` ${elemTo} <- ${fromPart}${nfb}${errf}`);
|
|
1091
1534
|
}
|
|
1535
|
+
lines.push(`}`);
|
|
1092
1536
|
continue;
|
|
1093
1537
|
}
|
|
1094
1538
|
// Regular wire
|
|
1095
|
-
const fromStr =
|
|
1096
|
-
const toStr =
|
|
1539
|
+
const fromStr = sRef(w.from, true);
|
|
1540
|
+
const toStr = sRef(w.to, false);
|
|
1097
1541
|
const arrow = w.force ? "<-!" : "<-";
|
|
1098
1542
|
const nfb = w.nullFallback ? ` || ${w.nullFallback}` : "";
|
|
1099
1543
|
const errf = w.fallbackRef
|
|
1100
|
-
? ` ?? ${
|
|
1544
|
+
? ` ?? ${sPipeOrRef(w.fallbackRef)}`
|
|
1101
1545
|
: w.fallback ? ` ?? ${w.fallback}` : "";
|
|
1102
1546
|
lines.push(`${toStr} ${arrow} ${fromStr}${nfb}${errf}`);
|
|
1103
1547
|
}
|
|
1104
1548
|
// ── Pipe wires ───────────────────────────────────────────────────────
|
|
1105
|
-
// Find terminal fromOutMap entries — their destination is NOT another
|
|
1106
|
-
// pipe handle's .in. Follow the chain backward to reconstruct:
|
|
1107
|
-
// dest <- h1:h2:…:source
|
|
1108
|
-
const serializedPipeTrunks = new Set();
|
|
1109
1549
|
for (const [tk, outWire] of fromOutMap.entries()) {
|
|
1110
|
-
// Non-terminal: this fork's result feeds another fork's input field
|
|
1111
1550
|
if (pipeHandleTrunkKeys.has(refTrunkKey(outWire.to)))
|
|
1112
1551
|
continue;
|
|
1113
|
-
// Follow chain backward to collect handle names (outermost-first)
|
|
1114
1552
|
const handleChain = [];
|
|
1115
1553
|
let currentTk = tk;
|
|
1116
1554
|
let actualSourceRef = null;
|
|
@@ -1119,18 +1557,15 @@ function serializeBridgeBlock(bridge) {
|
|
|
1119
1557
|
const handleName = handleMap.get(currentTk);
|
|
1120
1558
|
if (!handleName)
|
|
1121
1559
|
break;
|
|
1122
|
-
// Token: "handle" when field is "in" (default), otherwise "handle.field"
|
|
1123
1560
|
const inWire = toInMap.get(currentTk);
|
|
1124
1561
|
const fieldName = inWire?.to.path[0] ?? "in";
|
|
1125
1562
|
const token = fieldName === "in" ? handleName : `${handleName}.${fieldName}`;
|
|
1126
1563
|
handleChain.push(token);
|
|
1127
|
-
serializedPipeTrunks.add(currentTk);
|
|
1128
1564
|
if (inWire?.force)
|
|
1129
1565
|
chainForced = true;
|
|
1130
1566
|
if (!inWire)
|
|
1131
1567
|
break;
|
|
1132
1568
|
const fromTk = refTrunkKey(inWire.from);
|
|
1133
|
-
// Inner source is another pipe fork root (empty path) → continue chain
|
|
1134
1569
|
if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) {
|
|
1135
1570
|
currentTk = fromTk;
|
|
1136
1571
|
}
|
|
@@ -1140,12 +1575,12 @@ function serializeBridgeBlock(bridge) {
|
|
|
1140
1575
|
}
|
|
1141
1576
|
}
|
|
1142
1577
|
if (actualSourceRef && handleChain.length > 0) {
|
|
1143
|
-
const sourceStr =
|
|
1144
|
-
const destStr =
|
|
1578
|
+
const sourceStr = sRef(actualSourceRef, true);
|
|
1579
|
+
const destStr = sRef(outWire.to, false);
|
|
1145
1580
|
const arrow = chainForced ? "<-!" : "<-";
|
|
1146
1581
|
const nfb = outWire.nullFallback ? ` || ${outWire.nullFallback}` : "";
|
|
1147
1582
|
const errf = outWire.fallbackRef
|
|
1148
|
-
? ` ?? ${
|
|
1583
|
+
? ` ?? ${sPipeOrRef(outWire.fallbackRef)}`
|
|
1149
1584
|
: outWire.fallback ? ` ?? ${outWire.fallback}` : "";
|
|
1150
1585
|
lines.push(`${destStr} ${arrow} ${handleChain.join(":")}:${sourceStr}${nfb}${errf}`);
|
|
1151
1586
|
}
|
|
@@ -1165,6 +1600,7 @@ function buildHandleMap(bridge) {
|
|
|
1165
1600
|
const handleMap = new Map();
|
|
1166
1601
|
const instanceCounters = new Map();
|
|
1167
1602
|
let inputHandle;
|
|
1603
|
+
let outputHandle;
|
|
1168
1604
|
for (const h of bridge.handles) {
|
|
1169
1605
|
switch (h.kind) {
|
|
1170
1606
|
case "tool": {
|
|
@@ -1190,19 +1626,27 @@ function buildHandleMap(bridge) {
|
|
|
1190
1626
|
case "input":
|
|
1191
1627
|
inputHandle = h.handle;
|
|
1192
1628
|
break;
|
|
1629
|
+
case "output":
|
|
1630
|
+
outputHandle = h.handle;
|
|
1631
|
+
break;
|
|
1193
1632
|
case "context":
|
|
1194
1633
|
handleMap.set(`${SELF_MODULE}:Context:context`, h.handle);
|
|
1195
1634
|
break;
|
|
1196
1635
|
case "const":
|
|
1197
1636
|
handleMap.set(`${SELF_MODULE}:Const:const`, h.handle);
|
|
1198
1637
|
break;
|
|
1638
|
+
case "define":
|
|
1639
|
+
handleMap.set(`__define_${h.handle}:${bridge.type}:${bridge.field}`, h.handle);
|
|
1640
|
+
break;
|
|
1199
1641
|
}
|
|
1200
1642
|
}
|
|
1201
|
-
return { handleMap, inputHandle };
|
|
1643
|
+
return { handleMap, inputHandle, outputHandle };
|
|
1202
1644
|
}
|
|
1203
|
-
function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
1645
|
+
function serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom) {
|
|
1204
1646
|
if (ref.element) {
|
|
1205
|
-
|
|
1647
|
+
// Element refs are only serialized inside brace blocks (using the iterator name).
|
|
1648
|
+
// This path should not be reached in normal serialization.
|
|
1649
|
+
return "item." + serPath(ref.path);
|
|
1206
1650
|
}
|
|
1207
1651
|
// Bridge's own trunk (no instance, no element)
|
|
1208
1652
|
const isBridgeTrunk = ref.module === SELF_MODULE &&
|
|
@@ -1213,16 +1657,17 @@ function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
|
1213
1657
|
if (isBridgeTrunk) {
|
|
1214
1658
|
if (isFrom && inputHandle) {
|
|
1215
1659
|
// From side: use input handle (data comes from args)
|
|
1216
|
-
return
|
|
1660
|
+
return ref.path.length > 0
|
|
1661
|
+
? inputHandle + "." + serPath(ref.path)
|
|
1662
|
+
: inputHandle;
|
|
1217
1663
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
return bridge.field + "." + serPath(ref.path);
|
|
1664
|
+
if (!isFrom && outputHandle) {
|
|
1665
|
+
// To side: use output handle
|
|
1666
|
+
return ref.path.length > 0
|
|
1667
|
+
? outputHandle + "." + serPath(ref.path)
|
|
1668
|
+
: outputHandle;
|
|
1224
1669
|
}
|
|
1225
|
-
//
|
|
1670
|
+
// Fallback (no handle declared — legacy/serializer-only path)
|
|
1226
1671
|
return serPath(ref.path);
|
|
1227
1672
|
}
|
|
1228
1673
|
// Lookup by trunk key
|
|
@@ -1231,7 +1676,6 @@ function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
|
1231
1676
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
1232
1677
|
const handle = handleMap.get(trunkStr);
|
|
1233
1678
|
if (handle) {
|
|
1234
|
-
// Empty path — just the handle name (e.g. pipe result = tool root)
|
|
1235
1679
|
if (ref.path.length === 0)
|
|
1236
1680
|
return handle;
|
|
1237
1681
|
return handle + "." + serPath(ref.path);
|