@stackables/bridge 1.1.1 → 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 +221 -59
- package/build/ExecutionTree.js +12 -2
- package/build/bridge-format.d.ts +0 -10
- package/build/bridge-format.js +721 -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,8 +111,52 @@ 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) {
|
|
88
|
-
|
|
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
|
+
}
|
|
138
|
+
// Validate mandatory braces: `bridge Foo.bar {` ... `}`
|
|
139
|
+
const rawLines = block.split("\n");
|
|
140
|
+
const keywordIdx = rawLines.findIndex((l) => /^bridge\s/i.test(l.trim()));
|
|
141
|
+
if (keywordIdx !== -1) {
|
|
142
|
+
const kw = rawLines[keywordIdx].trim();
|
|
143
|
+
if (!kw.endsWith("{")) {
|
|
144
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: bridge block must use braces: bridge Type.field {`);
|
|
145
|
+
}
|
|
146
|
+
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
147
|
+
if (!hasClose) {
|
|
148
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: bridge block missing closing }`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Strip braces for internal parsing
|
|
152
|
+
const lines = rawLines.map((l) => {
|
|
153
|
+
const trimmed = l.trimEnd();
|
|
154
|
+
if (trimmed === "}")
|
|
155
|
+
return "";
|
|
156
|
+
if (/^bridge\s/i.test(trimmed) && trimmed.endsWith("{"))
|
|
157
|
+
return trimmed.replace(/\s*\{\s*$/, "");
|
|
158
|
+
return trimmed;
|
|
159
|
+
});
|
|
89
160
|
const instructions = [];
|
|
90
161
|
/** 1-based global line number for error messages */
|
|
91
162
|
const ln = (i) => lineOffset + i + 1;
|
|
@@ -113,7 +184,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
113
184
|
if (!bridgeType) {
|
|
114
185
|
throw new Error(`Line ${ln(i)}: "with" declaration must come after "bridge" declaration`);
|
|
115
186
|
}
|
|
116
|
-
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters,
|
|
187
|
+
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters, previousInstructions ?? [], ln(i));
|
|
117
188
|
continue;
|
|
118
189
|
}
|
|
119
190
|
// First non-header line — body starts here
|
|
@@ -126,6 +197,8 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
126
197
|
// ── Parse wire lines ────────────────────────────────────────────────
|
|
127
198
|
const wires = [];
|
|
128
199
|
let currentArrayToPath = null;
|
|
200
|
+
let currentIterHandle = null;
|
|
201
|
+
const arrayIterators = {};
|
|
129
202
|
/** Monotonically-increasing index; combined with a high base to produce
|
|
130
203
|
* fork instances that can never collide with regular handle instances. */
|
|
131
204
|
let nextForkSeq = 0;
|
|
@@ -146,7 +219,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
146
219
|
function buildSourceExpr(sourceStr, lineNum, forceOnOutermost) {
|
|
147
220
|
const parts = sourceStr.split(":");
|
|
148
221
|
if (parts.length === 1) {
|
|
149
|
-
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField);
|
|
222
|
+
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField, lineNum);
|
|
150
223
|
}
|
|
151
224
|
// Pipe chain
|
|
152
225
|
const actualSource = parts[parts.length - 1];
|
|
@@ -163,7 +236,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
163
236
|
throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${handleName}". Add 'with <tool> as ${handleName}' to the bridge header.`);
|
|
164
237
|
}
|
|
165
238
|
}
|
|
166
|
-
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField);
|
|
239
|
+
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField, lineNum);
|
|
167
240
|
const reversedTokens = [...tokenChain].reverse();
|
|
168
241
|
for (let idx = 0; idx < reversedTokens.length; idx++) {
|
|
169
242
|
const tok = reversedTokens[idx];
|
|
@@ -184,72 +257,165 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
184
257
|
}
|
|
185
258
|
return prevOutRef; // fork-root ref
|
|
186
259
|
}
|
|
260
|
+
// ── Whether we are inside an element-mapping brace block
|
|
261
|
+
let inElementBlock = false;
|
|
187
262
|
for (let i = bodyStartIndex; i < lines.length; i++) {
|
|
188
263
|
const raw = lines[i];
|
|
189
264
|
const line = raw.trim();
|
|
190
265
|
if (!line || line.startsWith("#")) {
|
|
191
266
|
continue;
|
|
192
267
|
}
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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}`);
|
|
217
389
|
}
|
|
218
|
-
// End of array mapping block
|
|
219
|
-
currentArrayToPath = null;
|
|
220
390
|
// Constant wire: target = "value" or target = value (unquoted)
|
|
221
391
|
const constantMatch = line.match(/^(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
222
392
|
if (constantMatch) {
|
|
223
393
|
const [, targetStr, quotedValue, unquotedValue] = constantMatch;
|
|
224
394
|
const value = quotedValue ?? unquotedValue;
|
|
225
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField);
|
|
395
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
226
396
|
wires.push({ value, to: toRef });
|
|
227
397
|
continue;
|
|
228
398
|
}
|
|
229
399
|
// ── Wire: target <- A [|| B [|| C]] [|| "nullLiteral"] [?? errorSrc|"errorLiteral"]
|
|
230
|
-
//
|
|
231
|
-
// A, B, C are source expressions (handle.path or pipe|chain|src).
|
|
232
|
-
// `||` separates null-coalescing alternatives — evaluated left to right;
|
|
233
|
-
// the last alternative may be a JSON literal (→ nullFallback).
|
|
234
|
-
// `??` is the error fallback — fires when ALL sources throw.
|
|
235
|
-
// Can be a JSON literal (→ fallback) or a source expression (→ fallbackRef).
|
|
236
|
-
//
|
|
237
|
-
// Each `||` source becomes a separate wire targeting the same field.
|
|
238
|
-
// The last source wire carries nullFallback + error-fallback attributes.
|
|
239
|
-
// Desugars transparently: serializer emits back as multiple wire lines.
|
|
240
400
|
const arrowMatch = line.match(/^(\S+)\s*<-(!?)\s*(.+)$/);
|
|
241
401
|
if (arrowMatch) {
|
|
242
402
|
const [, targetStr, forceFlag, rhs] = arrowMatch;
|
|
243
403
|
const force = forceFlag === "!";
|
|
244
404
|
const rhsTrimmed = rhs.trim();
|
|
245
|
-
// ── Array mapping: target
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
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));
|
|
251
414
|
wires.push({ from: fromRef, to: toRef });
|
|
252
415
|
currentArrayToPath = toRef.path;
|
|
416
|
+
currentIterHandle = iterHandle;
|
|
417
|
+
arrayIterators[toRef.path[0]] = iterHandle;
|
|
418
|
+
inElementBlock = true;
|
|
253
419
|
continue;
|
|
254
420
|
}
|
|
255
421
|
// ── Strip the ?? tail (last " ?? " wins in case source contains " ?? ")
|
|
@@ -279,38 +445,27 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
279
445
|
if (sourceParts.length === 0) {
|
|
280
446
|
throw new Error(`Line ${ln(i)}: Wire has no source expression: ${line}`);
|
|
281
447
|
}
|
|
282
|
-
// ── Parse the ?? source/pipe into a fallbackRef (if needed)
|
|
283
|
-
// Wires added by buildSourceExpr for the fallback fork are deferred and
|
|
284
|
-
// pushed AFTER the source wires so that wire order is stable across
|
|
285
|
-
// parse → serialize → re-parse cycles.
|
|
286
448
|
let fallbackRef;
|
|
287
449
|
let fallbackInternalWires = [];
|
|
288
450
|
if (fallbackRefStr) {
|
|
289
451
|
const preLen = wires.length;
|
|
290
452
|
fallbackRef = buildSourceExpr(fallbackRefStr, ln(i), false);
|
|
291
|
-
// Splice out internal wires buildSourceExpr just added; push after sources.
|
|
292
453
|
fallbackInternalWires = wires.splice(preLen);
|
|
293
454
|
}
|
|
294
|
-
|
|
295
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField);
|
|
455
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
296
456
|
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
297
457
|
const isFirst = ci === 0;
|
|
298
458
|
const isLast = ci === sourceParts.length - 1;
|
|
299
459
|
const srcStr = sourceParts[ci];
|
|
300
|
-
// Parse source expression; for pipe chains buildSourceExpr pushes
|
|
301
|
-
// intermediate wires and returns the fork-root ref.
|
|
302
460
|
const termRef = buildSourceExpr(srcStr, ln(i), force && isFirst);
|
|
303
461
|
const isPipeFork = termRef.instance != null && termRef.path.length === 0
|
|
304
462
|
&& srcStr.includes(":");
|
|
305
|
-
// attrs carried only on the LAST wire of the coalesce chain
|
|
306
463
|
const lastAttrs = isLast ? {
|
|
307
464
|
...(nullFallback ? { nullFallback } : {}),
|
|
308
465
|
...(fallback ? { fallback } : {}),
|
|
309
466
|
...(fallbackRef ? { fallbackRef } : {}),
|
|
310
467
|
} : {};
|
|
311
468
|
if (isPipeFork) {
|
|
312
|
-
// Terminal pipe wire: fork-root → target (force only on outermost
|
|
313
|
-
// intermediate wire, already set inside buildSourceExpr)
|
|
314
469
|
wires.push({ from: termRef, to: toRef, pipe: true, ...lastAttrs });
|
|
315
470
|
}
|
|
316
471
|
else {
|
|
@@ -322,19 +477,29 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
322
477
|
});
|
|
323
478
|
}
|
|
324
479
|
}
|
|
325
|
-
// Push fallbackRef internal wires after all source wires (stable round-trip
|
|
326
|
-
// order: same position whether parsed inline or from serialized separate lines)
|
|
327
480
|
wires.push(...fallbackInternalWires);
|
|
328
481
|
continue;
|
|
329
482
|
}
|
|
330
483
|
throw new Error(`Line ${ln(i)}: Unrecognized line: ${line}`);
|
|
331
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
|
+
}
|
|
332
496
|
instructions.unshift({
|
|
333
497
|
kind: "bridge",
|
|
334
498
|
type: bridgeType,
|
|
335
499
|
field: bridgeField,
|
|
336
500
|
handles: handleBindings,
|
|
337
501
|
wires,
|
|
502
|
+
arrayIterators: Object.keys(arrayIterators).length > 0 ? arrayIterators : undefined,
|
|
338
503
|
pipeHandles: pipeHandleEntries.length > 0 ? pipeHandleEntries : undefined,
|
|
339
504
|
});
|
|
340
505
|
return instructions;
|
|
@@ -369,6 +534,45 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
369
534
|
});
|
|
370
535
|
return;
|
|
371
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
|
+
}
|
|
372
576
|
// with context as <handle>
|
|
373
577
|
match = line.match(/^with\s+context\s+as\s+(\w+)$/i);
|
|
374
578
|
if (match) {
|
|
@@ -421,13 +625,24 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
421
625
|
});
|
|
422
626
|
return;
|
|
423
627
|
}
|
|
424
|
-
// with <name> as <handle> —
|
|
628
|
+
// with <name> as <handle> — check for define invocation first
|
|
425
629
|
match = line.match(/^with\s+(\S+)\s+as\s+(\w+)$/i);
|
|
426
630
|
if (match) {
|
|
427
631
|
const name = match[1];
|
|
428
632
|
const handle = match[2];
|
|
429
633
|
checkDuplicate(handle);
|
|
430
|
-
|
|
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
|
+
}
|
|
431
646
|
const lastDot = name.lastIndexOf(".");
|
|
432
647
|
if (lastDot !== -1) {
|
|
433
648
|
const modulePart = name.substring(0, lastDot);
|
|
@@ -466,6 +681,17 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
466
681
|
const lastDot = name.lastIndexOf(".");
|
|
467
682
|
const handle = lastDot !== -1 ? name.substring(lastDot + 1) : name;
|
|
468
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
|
+
}
|
|
469
695
|
if (lastDot !== -1) {
|
|
470
696
|
const modulePart = name.substring(0, lastDot);
|
|
471
697
|
const fieldPart = name.substring(lastDot + 1);
|
|
@@ -486,6 +712,155 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
486
712
|
}
|
|
487
713
|
throw new Error(`Line ${lineNum}: Invalid with declaration: ${line}`);
|
|
488
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
|
+
}
|
|
489
864
|
/**
|
|
490
865
|
* Resolve an address string into a structured NodeRef.
|
|
491
866
|
*
|
|
@@ -495,7 +870,7 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
495
870
|
* 3. Prefix matches a declared handle → resolve via handle binding
|
|
496
871
|
* 4. Otherwise → nested output path (e.g., topPick.address)
|
|
497
872
|
*/
|
|
498
|
-
function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
873
|
+
function resolveAddress(address, handles, bridgeType, bridgeField, lineNum) {
|
|
499
874
|
const dotIndex = address.indexOf(".");
|
|
500
875
|
if (dotIndex === -1) {
|
|
501
876
|
// Whole address is a declared handle → resolve to its root (path: [])
|
|
@@ -511,13 +886,9 @@ function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
|
511
886
|
ref.instance = resolution.instance;
|
|
512
887
|
return ref;
|
|
513
888
|
}
|
|
514
|
-
//
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
type: bridgeType,
|
|
518
|
-
field: bridgeField,
|
|
519
|
-
path: parsePath(address),
|
|
520
|
-
};
|
|
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.`);
|
|
521
892
|
}
|
|
522
893
|
const prefix = address.substring(0, dotIndex);
|
|
523
894
|
const rest = address.substring(dotIndex + 1);
|
|
@@ -536,23 +907,9 @@ function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
|
536
907
|
}
|
|
537
908
|
return ref;
|
|
538
909
|
}
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (prefix === bridgeField) {
|
|
543
|
-
return {
|
|
544
|
-
module: SELF_MODULE,
|
|
545
|
-
type: bridgeType,
|
|
546
|
-
field: bridgeField,
|
|
547
|
-
path: pathParts,
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
return {
|
|
551
|
-
module: SELF_MODULE,
|
|
552
|
-
type: bridgeType,
|
|
553
|
-
field: bridgeField,
|
|
554
|
-
path: [prefix, ...pathParts],
|
|
555
|
-
};
|
|
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.`);
|
|
556
913
|
}
|
|
557
914
|
// ── Const block parser ──────────────────────────────────────────────────────
|
|
558
915
|
/**
|
|
@@ -583,6 +940,7 @@ function parseConstLines(block, lineOffset) {
|
|
|
583
940
|
throw new Error(`Line ${ln(i)}: Expected const declaration, got: ${line}`);
|
|
584
941
|
}
|
|
585
942
|
const name = constMatch[1];
|
|
943
|
+
assertNotReserved(name, ln(i), "const name");
|
|
586
944
|
let valuePart = constMatch[2].trim();
|
|
587
945
|
// Multi-line: if value starts with { or [ and isn't balanced, read more lines
|
|
588
946
|
if (/^[{[]/.test(valuePart)) {
|
|
@@ -621,36 +979,118 @@ function parseConstLines(block, lineOffset) {
|
|
|
621
979
|
}
|
|
622
980
|
return results;
|
|
623
981
|
}
|
|
624
|
-
// ──
|
|
982
|
+
// ── Define block parser ─────────────────────────────────────────────────────
|
|
625
983
|
/**
|
|
626
|
-
* Parse a `
|
|
984
|
+
* Parse a `define` block into a DefineDef instruction.
|
|
627
985
|
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
* with context
|
|
631
|
-
* baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
632
|
-
* 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.
|
|
633
988
|
*
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
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.
|
|
638
1047
|
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
1048
|
+
* Format:
|
|
1049
|
+
* tool hereapi from httpCall {
|
|
641
1050
|
* with context
|
|
642
|
-
* baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1051
|
+
* .baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1052
|
+
* .headers.apiKey <- context.hereapi.apiKey
|
|
1053
|
+
* }
|
|
643
1054
|
*
|
|
644
|
-
*
|
|
645
|
-
* method = GET
|
|
646
|
-
* path = /geocode
|
|
1055
|
+
* tool hereapi.geocode from hereapi {
|
|
1056
|
+
* .method = GET
|
|
1057
|
+
* .path = /geocode
|
|
1058
|
+
* }
|
|
647
1059
|
*
|
|
648
|
-
* When
|
|
649
|
-
* 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
|
|
650
1062
|
* is treated as a function name.
|
|
651
1063
|
*/
|
|
652
1064
|
function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
653
|
-
|
|
1065
|
+
// Validate mandatory braces for blocks that have a body (deps / wires)
|
|
1066
|
+
const rawLines = block.split("\n");
|
|
1067
|
+
const keywordIdx = rawLines.findIndex((l) => /^tool\s/i.test(l.trim()));
|
|
1068
|
+
if (keywordIdx !== -1) {
|
|
1069
|
+
// Check if there are non-blank, non-comment body lines after the keyword
|
|
1070
|
+
const bodyLines = rawLines.slice(keywordIdx + 1).filter((l) => {
|
|
1071
|
+
const t = l.trim();
|
|
1072
|
+
return t !== "" && !t.startsWith("#") && t !== "}";
|
|
1073
|
+
});
|
|
1074
|
+
const kw = rawLines[keywordIdx].trim();
|
|
1075
|
+
if (bodyLines.length > 0) {
|
|
1076
|
+
if (!kw.endsWith("{")) {
|
|
1077
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block with body must use braces: tool foo from bar {`);
|
|
1078
|
+
}
|
|
1079
|
+
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
1080
|
+
if (!hasClose) {
|
|
1081
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block missing closing }`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
// Strip braces for internal parsing
|
|
1086
|
+
const lines = rawLines.map((l) => {
|
|
1087
|
+
const trimmed = l.trimEnd();
|
|
1088
|
+
if (trimmed === "}")
|
|
1089
|
+
return "";
|
|
1090
|
+
if (/^tool\s/i.test(trimmed) && trimmed.endsWith("{"))
|
|
1091
|
+
return trimmed.replace(/\s*\{\s*$/, "");
|
|
1092
|
+
return trimmed;
|
|
1093
|
+
});
|
|
654
1094
|
/** 1-based global line number for error messages */
|
|
655
1095
|
const ln = (i) => lineOffset + i + 1;
|
|
656
1096
|
let toolName = "";
|
|
@@ -663,31 +1103,16 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
663
1103
|
const line = raw.trim();
|
|
664
1104
|
if (!line || line.startsWith("#"))
|
|
665
1105
|
continue;
|
|
666
|
-
// Tool declaration: tool <name>
|
|
1106
|
+
// Tool declaration: tool <name> from <source>
|
|
667
1107
|
if (/^tool\s/i.test(line)) {
|
|
668
|
-
const
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
toolExtends = extendsMatch[2];
|
|
672
|
-
continue;
|
|
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>`);
|
|
673
1111
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
throw new Error(`Line ${ln(i)}: Invalid tool declaration: ${line}`);
|
|
681
|
-
}
|
|
682
|
-
// Extend declaration: extend <source> as <name>
|
|
683
|
-
if (/^extend\s/i.test(line)) {
|
|
684
|
-
const extendMatch = line.match(/^extend\s+(\S+)\s+as\s+(\S+)$/i);
|
|
685
|
-
if (!extendMatch) {
|
|
686
|
-
throw new Error(`Line ${ln(i)}: Invalid extend declaration: ${line}. Expected: extend <source> as <name>`);
|
|
687
|
-
}
|
|
688
|
-
const source = extendMatch[1];
|
|
689
|
-
toolName = extendMatch[2];
|
|
690
|
-
// If source matches a previously-defined tool, it's an extends; otherwise it's a function name
|
|
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
|
|
691
1116
|
const isKnownTool = previousInstructions?.some((inst) => inst.kind === "tool" && inst.name === source);
|
|
692
1117
|
if (isKnownTool) {
|
|
693
1118
|
toolExtends = source;
|
|
@@ -751,8 +1176,8 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
751
1176
|
wires.push({ kind: "onError", source: onErrorPullMatch[1] });
|
|
752
1177
|
continue;
|
|
753
1178
|
}
|
|
754
|
-
// Constant wire: target = "value" or target = value (unquoted)
|
|
755
|
-
const constantMatch = line.match(
|
|
1179
|
+
// Constant wire: .target = "value" or .target = value (unquoted)
|
|
1180
|
+
const constantMatch = line.match(/^\.(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
756
1181
|
if (constantMatch) {
|
|
757
1182
|
const value = constantMatch[2] ?? constantMatch[3];
|
|
758
1183
|
wires.push({
|
|
@@ -762,12 +1187,16 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
762
1187
|
});
|
|
763
1188
|
continue;
|
|
764
1189
|
}
|
|
765
|
-
// Pull wire: target <- source
|
|
766
|
-
const pullMatch = line.match(
|
|
1190
|
+
// Pull wire: .target <- source
|
|
1191
|
+
const pullMatch = line.match(/^\.(\S+)\s*<-\s*(\S+)$/);
|
|
767
1192
|
if (pullMatch) {
|
|
768
1193
|
wires.push({ target: pullMatch[1], kind: "pull", source: pullMatch[2] });
|
|
769
1194
|
continue;
|
|
770
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
|
+
}
|
|
771
1200
|
throw new Error(`Line ${ln(i)}: Unrecognized tool line: ${line}`);
|
|
772
1201
|
}
|
|
773
1202
|
if (!toolName)
|
|
@@ -813,7 +1242,8 @@ export function serializeBridge(instructions) {
|
|
|
813
1242
|
const bridges = instructions.filter((i) => i.kind === "bridge");
|
|
814
1243
|
const tools = instructions.filter((i) => i.kind === "tool");
|
|
815
1244
|
const consts = instructions.filter((i) => i.kind === "const");
|
|
816
|
-
|
|
1245
|
+
const defines = instructions.filter((i) => i.kind === "define");
|
|
1246
|
+
if (bridges.length === 0 && tools.length === 0 && consts.length === 0 && defines.length === 0)
|
|
817
1247
|
return "";
|
|
818
1248
|
const blocks = [];
|
|
819
1249
|
// Group const declarations into a single block
|
|
@@ -823,20 +1253,20 @@ export function serializeBridge(instructions) {
|
|
|
823
1253
|
for (const tool of tools) {
|
|
824
1254
|
blocks.push(serializeToolBlock(tool));
|
|
825
1255
|
}
|
|
1256
|
+
for (const def of defines) {
|
|
1257
|
+
blocks.push(serializeDefineBlock(def));
|
|
1258
|
+
}
|
|
826
1259
|
for (const bridge of bridges) {
|
|
827
1260
|
blocks.push(serializeBridgeBlock(bridge));
|
|
828
1261
|
}
|
|
829
|
-
return blocks.join("\n\n
|
|
1262
|
+
return `version ${BRIDGE_VERSION}\n\n` + blocks.join("\n\n") + "\n";
|
|
830
1263
|
}
|
|
831
1264
|
function serializeToolBlock(tool) {
|
|
832
1265
|
const lines = [];
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
lines.push(`extend ${tool.fn} as ${tool.name}`);
|
|
839
|
-
}
|
|
1266
|
+
const hasBody = tool.deps.length > 0 || tool.wires.length > 0;
|
|
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}`);
|
|
840
1270
|
// Dependencies
|
|
841
1271
|
for (const dep of tool.deps) {
|
|
842
1272
|
if (dep.kind === "context") {
|
|
@@ -872,16 +1302,18 @@ function serializeToolBlock(tool) {
|
|
|
872
1302
|
else if (wire.kind === "constant") {
|
|
873
1303
|
// Use quoted form if value contains spaces or special chars, unquoted otherwise
|
|
874
1304
|
if (/\s/.test(wire.value) || wire.value === "") {
|
|
875
|
-
lines.push(`
|
|
1305
|
+
lines.push(` .${wire.target} = "${wire.value}"`);
|
|
876
1306
|
}
|
|
877
1307
|
else {
|
|
878
|
-
lines.push(`
|
|
1308
|
+
lines.push(` .${wire.target} = ${wire.value}`);
|
|
879
1309
|
}
|
|
880
1310
|
}
|
|
881
1311
|
else {
|
|
882
|
-
lines.push(`
|
|
1312
|
+
lines.push(` .${wire.target} <- ${wire.source}`);
|
|
883
1313
|
}
|
|
884
1314
|
}
|
|
1315
|
+
if (hasBody)
|
|
1316
|
+
lines.push(`}`);
|
|
885
1317
|
return lines.join("\n");
|
|
886
1318
|
}
|
|
887
1319
|
/**
|
|
@@ -894,7 +1326,7 @@ function serializeToolBlock(tool) {
|
|
|
894
1326
|
* This is used to emit `?? handle.path` or `?? pipe:source` for wire
|
|
895
1327
|
* `fallbackRef` values.
|
|
896
1328
|
*/
|
|
897
|
-
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle) {
|
|
1329
|
+
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle) {
|
|
898
1330
|
const refTk = ref.instance != null
|
|
899
1331
|
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
900
1332
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
@@ -925,16 +1357,40 @@ function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge
|
|
|
925
1357
|
}
|
|
926
1358
|
}
|
|
927
1359
|
if (actualSourceRef && handleChain.length > 0) {
|
|
928
|
-
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, true);
|
|
1360
|
+
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, outputHandle, true);
|
|
929
1361
|
return `${handleChain.join(":")}:${sourceStr}`;
|
|
930
1362
|
}
|
|
931
1363
|
}
|
|
932
|
-
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");
|
|
933
1385
|
}
|
|
934
1386
|
function serializeBridgeBlock(bridge) {
|
|
1387
|
+
// ── Passthrough shorthand ───────────────────────────────────────────
|
|
1388
|
+
if (bridge.passthrough) {
|
|
1389
|
+
return `bridge ${bridge.type}.${bridge.field} with ${bridge.passthrough}`;
|
|
1390
|
+
}
|
|
935
1391
|
const lines = [];
|
|
936
1392
|
// ── Header ──────────────────────────────────────────────────────────
|
|
937
|
-
lines.push(`bridge ${bridge.type}.${bridge.field}`);
|
|
1393
|
+
lines.push(`bridge ${bridge.type}.${bridge.field} {`);
|
|
938
1394
|
for (const h of bridge.handles) {
|
|
939
1395
|
switch (h.kind) {
|
|
940
1396
|
case "tool": {
|
|
@@ -950,7 +1406,20 @@ function serializeBridgeBlock(bridge) {
|
|
|
950
1406
|
break;
|
|
951
1407
|
}
|
|
952
1408
|
case "input":
|
|
953
|
-
|
|
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
|
+
}
|
|
954
1423
|
break;
|
|
955
1424
|
case "context":
|
|
956
1425
|
lines.push(` with context as ${h.handle}`);
|
|
@@ -963,29 +1432,28 @@ function serializeBridgeBlock(bridge) {
|
|
|
963
1432
|
lines.push(` with const as ${h.handle}`);
|
|
964
1433
|
}
|
|
965
1434
|
break;
|
|
1435
|
+
case "define":
|
|
1436
|
+
lines.push(` with ${h.name} as ${h.handle}`);
|
|
1437
|
+
break;
|
|
966
1438
|
}
|
|
967
1439
|
}
|
|
968
1440
|
lines.push("");
|
|
1441
|
+
// Mark where the wire body starts — everything after this gets 2-space indent
|
|
1442
|
+
const wireBodyStart = lines.length;
|
|
969
1443
|
// ── Build handle map for reverse resolution ─────────────────────────
|
|
970
|
-
const { handleMap, inputHandle } = buildHandleMap(bridge);
|
|
1444
|
+
const { handleMap, inputHandle, outputHandle } = buildHandleMap(bridge);
|
|
971
1445
|
// ── Pipe fork registry ──────────────────────────────────────────────
|
|
972
|
-
// Extend handleMap with fork → handle-name entries and build the set of
|
|
973
|
-
// known fork trunk keys so the wire classifiers below can use it.
|
|
974
1446
|
const pipeHandleTrunkKeys = new Set();
|
|
975
1447
|
for (const ph of bridge.pipeHandles ?? []) {
|
|
976
1448
|
handleMap.set(ph.key, ph.handle);
|
|
977
1449
|
pipeHandleTrunkKeys.add(ph.key);
|
|
978
1450
|
}
|
|
979
|
-
// ── Pipe wire detection
|
|
980
|
-
// Pipe wires are marked pipe:true. Classify them into two maps:
|
|
981
|
-
// toInMap: forkTrunkKey → wire feeding the fork's input field
|
|
982
|
-
// fromOutMap: forkTrunkKey → wire reading the fork's root result
|
|
983
|
-
// Terminal out-wires (destination is NOT another fork) are chain anchors.
|
|
1451
|
+
// ── Pipe wire detection ─────────────────────────────────────────────
|
|
984
1452
|
const refTrunkKey = (ref) => ref.instance != null
|
|
985
1453
|
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
986
1454
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
987
|
-
const toInMap = new Map();
|
|
988
|
-
const fromOutMap = new Map();
|
|
1455
|
+
const toInMap = new Map();
|
|
1456
|
+
const fromOutMap = new Map();
|
|
989
1457
|
const pipeWireSet = new Set();
|
|
990
1458
|
for (const w of bridge.wires) {
|
|
991
1459
|
if (!("from" in w) || !w.pipe)
|
|
@@ -993,70 +1461,94 @@ function serializeBridgeBlock(bridge) {
|
|
|
993
1461
|
const fw = w;
|
|
994
1462
|
pipeWireSet.add(w);
|
|
995
1463
|
const toTk = refTrunkKey(fw.to);
|
|
996
|
-
// In-wire: single-segment path targeting a known pipe fork
|
|
997
1464
|
if (fw.to.path.length === 1 && pipeHandleTrunkKeys.has(toTk)) {
|
|
998
1465
|
toInMap.set(toTk, fw);
|
|
999
1466
|
}
|
|
1000
|
-
// Out-wire: empty path from a known pipe fork
|
|
1001
1467
|
if (fw.from.path.length === 0 && pipeHandleTrunkKeys.has(refTrunkKey(fw.from))) {
|
|
1002
1468
|
fromOutMap.set(refTrunkKey(fw.from), fw);
|
|
1003
1469
|
}
|
|
1004
1470
|
}
|
|
1005
|
-
// ──
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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);
|
|
1015
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);
|
|
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));
|
|
1016
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);
|
|
1017
1501
|
for (const w of regularWires) {
|
|
1018
1502
|
// Constant wire
|
|
1019
1503
|
if ("value" in w) {
|
|
1020
|
-
const toStr =
|
|
1504
|
+
const toStr = sRef(w.to, false);
|
|
1021
1505
|
lines.push(`${toStr} = "${w.value}"`);
|
|
1022
1506
|
continue;
|
|
1023
1507
|
}
|
|
1024
|
-
// Array mapping
|
|
1508
|
+
// Array mapping — emit brace-delimited element block
|
|
1025
1509
|
const arrayKey = w.to.path.length === 1 ? w.to.path[0] : null;
|
|
1026
|
-
if (arrayKey &&
|
|
1027
|
-
elementGroups.has(arrayKey) &&
|
|
1028
|
-
!serializedArrays.has(arrayKey)) {
|
|
1510
|
+
if (arrayKey && allElementKeys.has(arrayKey) && !serializedArrays.has(arrayKey)) {
|
|
1029
1511
|
serializedArrays.add(arrayKey);
|
|
1030
|
-
const
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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);
|
|
1035
1527
|
const elemTo = "." + serPath(ew.to.path.slice(1));
|
|
1036
|
-
|
|
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}`);
|
|
1037
1534
|
}
|
|
1535
|
+
lines.push(`}`);
|
|
1038
1536
|
continue;
|
|
1039
1537
|
}
|
|
1040
1538
|
// Regular wire
|
|
1041
|
-
const fromStr =
|
|
1042
|
-
const toStr =
|
|
1539
|
+
const fromStr = sRef(w.from, true);
|
|
1540
|
+
const toStr = sRef(w.to, false);
|
|
1043
1541
|
const arrow = w.force ? "<-!" : "<-";
|
|
1044
1542
|
const nfb = w.nullFallback ? ` || ${w.nullFallback}` : "";
|
|
1045
1543
|
const errf = w.fallbackRef
|
|
1046
|
-
? ` ?? ${
|
|
1544
|
+
? ` ?? ${sPipeOrRef(w.fallbackRef)}`
|
|
1047
1545
|
: w.fallback ? ` ?? ${w.fallback}` : "";
|
|
1048
1546
|
lines.push(`${toStr} ${arrow} ${fromStr}${nfb}${errf}`);
|
|
1049
1547
|
}
|
|
1050
1548
|
// ── Pipe wires ───────────────────────────────────────────────────────
|
|
1051
|
-
// Find terminal fromOutMap entries — their destination is NOT another
|
|
1052
|
-
// pipe handle's .in. Follow the chain backward to reconstruct:
|
|
1053
|
-
// dest <- h1:h2:…:source
|
|
1054
|
-
const serializedPipeTrunks = new Set();
|
|
1055
1549
|
for (const [tk, outWire] of fromOutMap.entries()) {
|
|
1056
|
-
// Non-terminal: this fork's result feeds another fork's input field
|
|
1057
1550
|
if (pipeHandleTrunkKeys.has(refTrunkKey(outWire.to)))
|
|
1058
1551
|
continue;
|
|
1059
|
-
// Follow chain backward to collect handle names (outermost-first)
|
|
1060
1552
|
const handleChain = [];
|
|
1061
1553
|
let currentTk = tk;
|
|
1062
1554
|
let actualSourceRef = null;
|
|
@@ -1065,18 +1557,15 @@ function serializeBridgeBlock(bridge) {
|
|
|
1065
1557
|
const handleName = handleMap.get(currentTk);
|
|
1066
1558
|
if (!handleName)
|
|
1067
1559
|
break;
|
|
1068
|
-
// Token: "handle" when field is "in" (default), otherwise "handle.field"
|
|
1069
1560
|
const inWire = toInMap.get(currentTk);
|
|
1070
1561
|
const fieldName = inWire?.to.path[0] ?? "in";
|
|
1071
1562
|
const token = fieldName === "in" ? handleName : `${handleName}.${fieldName}`;
|
|
1072
1563
|
handleChain.push(token);
|
|
1073
|
-
serializedPipeTrunks.add(currentTk);
|
|
1074
1564
|
if (inWire?.force)
|
|
1075
1565
|
chainForced = true;
|
|
1076
1566
|
if (!inWire)
|
|
1077
1567
|
break;
|
|
1078
1568
|
const fromTk = refTrunkKey(inWire.from);
|
|
1079
|
-
// Inner source is another pipe fork root (empty path) → continue chain
|
|
1080
1569
|
if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) {
|
|
1081
1570
|
currentTk = fromTk;
|
|
1082
1571
|
}
|
|
@@ -1086,26 +1575,32 @@ function serializeBridgeBlock(bridge) {
|
|
|
1086
1575
|
}
|
|
1087
1576
|
}
|
|
1088
1577
|
if (actualSourceRef && handleChain.length > 0) {
|
|
1089
|
-
const sourceStr =
|
|
1090
|
-
const destStr =
|
|
1578
|
+
const sourceStr = sRef(actualSourceRef, true);
|
|
1579
|
+
const destStr = sRef(outWire.to, false);
|
|
1091
1580
|
const arrow = chainForced ? "<-!" : "<-";
|
|
1092
1581
|
const nfb = outWire.nullFallback ? ` || ${outWire.nullFallback}` : "";
|
|
1093
1582
|
const errf = outWire.fallbackRef
|
|
1094
|
-
? ` ?? ${
|
|
1583
|
+
? ` ?? ${sPipeOrRef(outWire.fallbackRef)}`
|
|
1095
1584
|
: outWire.fallback ? ` ?? ${outWire.fallback}` : "";
|
|
1096
1585
|
lines.push(`${destStr} ${arrow} ${handleChain.join(":")}:${sourceStr}${nfb}${errf}`);
|
|
1097
1586
|
}
|
|
1098
1587
|
}
|
|
1588
|
+
// Indent wire body lines and close the block
|
|
1589
|
+
for (let i = wireBodyStart; i < lines.length; i++) {
|
|
1590
|
+
if (lines[i] !== "")
|
|
1591
|
+
lines[i] = ` ${lines[i]}`;
|
|
1592
|
+
}
|
|
1593
|
+
lines.push(`}`);
|
|
1099
1594
|
return lines.join("\n");
|
|
1100
1595
|
}
|
|
1101
1596
|
/**
|
|
1102
|
-
* Build a reverse lookup: trunk key → handle name.
|
|
1103
1597
|
* Recomputes instance numbers from handle bindings in declaration order.
|
|
1104
1598
|
*/
|
|
1105
1599
|
function buildHandleMap(bridge) {
|
|
1106
1600
|
const handleMap = new Map();
|
|
1107
1601
|
const instanceCounters = new Map();
|
|
1108
1602
|
let inputHandle;
|
|
1603
|
+
let outputHandle;
|
|
1109
1604
|
for (const h of bridge.handles) {
|
|
1110
1605
|
switch (h.kind) {
|
|
1111
1606
|
case "tool": {
|
|
@@ -1131,19 +1626,27 @@ function buildHandleMap(bridge) {
|
|
|
1131
1626
|
case "input":
|
|
1132
1627
|
inputHandle = h.handle;
|
|
1133
1628
|
break;
|
|
1629
|
+
case "output":
|
|
1630
|
+
outputHandle = h.handle;
|
|
1631
|
+
break;
|
|
1134
1632
|
case "context":
|
|
1135
1633
|
handleMap.set(`${SELF_MODULE}:Context:context`, h.handle);
|
|
1136
1634
|
break;
|
|
1137
1635
|
case "const":
|
|
1138
1636
|
handleMap.set(`${SELF_MODULE}:Const:const`, h.handle);
|
|
1139
1637
|
break;
|
|
1638
|
+
case "define":
|
|
1639
|
+
handleMap.set(`__define_${h.handle}:${bridge.type}:${bridge.field}`, h.handle);
|
|
1640
|
+
break;
|
|
1140
1641
|
}
|
|
1141
1642
|
}
|
|
1142
|
-
return { handleMap, inputHandle };
|
|
1643
|
+
return { handleMap, inputHandle, outputHandle };
|
|
1143
1644
|
}
|
|
1144
|
-
function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
1645
|
+
function serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom) {
|
|
1145
1646
|
if (ref.element) {
|
|
1146
|
-
|
|
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);
|
|
1147
1650
|
}
|
|
1148
1651
|
// Bridge's own trunk (no instance, no element)
|
|
1149
1652
|
const isBridgeTrunk = ref.module === SELF_MODULE &&
|
|
@@ -1154,16 +1657,17 @@ function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
|
1154
1657
|
if (isBridgeTrunk) {
|
|
1155
1658
|
if (isFrom && inputHandle) {
|
|
1156
1659
|
// From side: use input handle (data comes from args)
|
|
1157
|
-
return
|
|
1660
|
+
return ref.path.length > 0
|
|
1661
|
+
? inputHandle + "." + serPath(ref.path)
|
|
1662
|
+
: inputHandle;
|
|
1158
1663
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
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;
|
|
1165
1669
|
}
|
|
1166
|
-
//
|
|
1670
|
+
// Fallback (no handle declared — legacy/serializer-only path)
|
|
1167
1671
|
return serPath(ref.path);
|
|
1168
1672
|
}
|
|
1169
1673
|
// Lookup by trunk key
|
|
@@ -1172,7 +1676,6 @@ function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
|
1172
1676
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
1173
1677
|
const handle = handleMap.get(trunkStr);
|
|
1174
1678
|
if (handle) {
|
|
1175
|
-
// Empty path — just the handle name (e.g. pipe result = tool root)
|
|
1176
1679
|
if (ref.path.length === 0)
|
|
1177
1680
|
return handle;
|
|
1178
1681
|
return handle + "." + serPath(ref.path);
|