@stackables/bridge 1.2.0 → 1.4.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/build/{ExecutionTree.d.ts → src/ExecutionTree.d.ts} +1 -0
- package/build/src/ExecutionTree.d.ts.map +1 -0
- package/build/{ExecutionTree.js → src/ExecutionTree.js} +12 -2
- package/build/{bridge-format.d.ts → src/bridge-format.d.ts} +1 -10
- package/build/src/bridge-format.d.ts.map +1 -0
- package/build/{bridge-format.js → src/bridge-format.js} +680 -220
- package/build/{bridge-transform.d.ts → src/bridge-transform.d.ts} +1 -0
- package/build/src/bridge-transform.d.ts.map +1 -0
- package/build/{index.d.ts → src/index.d.ts} +1 -0
- package/build/src/index.d.ts.map +1 -0
- package/build/{tools → src/tools}/find-object.d.ts +1 -0
- package/build/src/tools/find-object.d.ts.map +1 -0
- package/build/{tools → src/tools}/http-call.d.ts +1 -0
- package/build/src/tools/http-call.d.ts.map +1 -0
- package/build/{tools → src/tools}/index.d.ts +1 -0
- package/build/src/tools/index.d.ts.map +1 -0
- package/build/{tools → src/tools}/lower-case.d.ts +1 -0
- package/build/src/tools/lower-case.d.ts.map +1 -0
- package/build/{tools → src/tools}/pick-first.d.ts +1 -0
- package/build/src/tools/pick-first.d.ts.map +1 -0
- package/build/{tools → src/tools}/to-array.d.ts +1 -0
- package/build/src/tools/to-array.d.ts.map +1 -0
- package/build/{tools → src/tools}/upper-case.d.ts +1 -0
- package/build/src/tools/upper-case.d.ts.map +1 -0
- package/build/{types.d.ts → src/types.d.ts} +49 -1
- package/build/src/types.d.ts.map +1 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +9 -6
- package/LICENSE +0 -21
- package/README.md +0 -361
- /package/build/{bridge-transform.js → src/bridge-transform.js} +0 -0
- /package/build/{index.js → src/index.js} +0 -0
- /package/build/{tools → src/tools}/find-object.js +0 -0
- /package/build/{tools → src/tools}/http-call.js +0 -0
- /package/build/{tools → src/tools}/index.js +0 -0
- /package/build/{tools → src/tools}/lower-case.js +0 -0
- /package/build/{tools → src/tools}/pick-first.js +0 -0
- /package/build/{tools → src/tools}/to-array.js +0 -0
- /package/build/{tools → src/tools}/upper-case.js +0 -0
- /package/build/{types.js → src/types.js} +0 -0
|
@@ -10,10 +10,50 @@ 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
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Strip a trailing `# comment` from a single source line, respecting string
|
|
28
|
+
* literals so that a `#` inside `"..."` is never treated as a comment marker.
|
|
29
|
+
* Leading/trailing whitespace of the returned value is preserved (callers trim
|
|
30
|
+
* the result themselves as needed).
|
|
31
|
+
*/
|
|
32
|
+
function stripInlineComment(line) {
|
|
33
|
+
let inString = false;
|
|
34
|
+
for (let i = 0; i < line.length; i++) {
|
|
35
|
+
if (line[i] === '"')
|
|
36
|
+
inString = !inString;
|
|
37
|
+
if (!inString && line[i] === "#")
|
|
38
|
+
return line.slice(0, i).trimEnd();
|
|
39
|
+
}
|
|
40
|
+
return line;
|
|
41
|
+
}
|
|
13
42
|
export function parseBridge(text) {
|
|
14
|
-
// Normalize: CRLF → LF, tabs → 2 spaces
|
|
43
|
+
// Normalize: CRLF → LF, tabs → 2 spaces, inline comments stripped
|
|
15
44
|
const normalized = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
16
|
-
const allLines = normalized.split("\n");
|
|
45
|
+
const allLines = normalized.split("\n").map(stripInlineComment);
|
|
46
|
+
// Version check — first non-blank, non-comment line must be `version 1.4`
|
|
47
|
+
const firstContentIdx = allLines.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("#"));
|
|
48
|
+
if (firstContentIdx === -1 || !/^version\s+/.test(allLines[firstContentIdx].trim())) {
|
|
49
|
+
throw new Error(`Missing version declaration. Bridge files must begin with: version ${BRIDGE_VERSION}`);
|
|
50
|
+
}
|
|
51
|
+
const versionToken = allLines[firstContentIdx].trim().replace(/^version\s+/, "");
|
|
52
|
+
if (versionToken !== BRIDGE_VERSION) {
|
|
53
|
+
throw new Error(`Unsupported bridge version "${versionToken}". This parser requires: version ${BRIDGE_VERSION}`);
|
|
54
|
+
}
|
|
55
|
+
// Blank out the version line so block-splitting ignores it
|
|
56
|
+
allLines[firstContentIdx] = "";
|
|
17
57
|
// Find separator lines (--- with optional surrounding whitespace)
|
|
18
58
|
const isSep = (line) => /^\s*---\s*$/.test(line);
|
|
19
59
|
// Collect block ranges as [start, end) line indices
|
|
@@ -35,7 +75,7 @@ export function parseBridge(text) {
|
|
|
35
75
|
let currentOffset = start;
|
|
36
76
|
for (let i = 0; i < blockLines.length; i++) {
|
|
37
77
|
const trimmed = blockLines[i].trim();
|
|
38
|
-
if (/^(tool|bridge|const|
|
|
78
|
+
if (/^(tool|bridge|const|define)\s/i.test(trimmed) && currentLines.length > 0) {
|
|
39
79
|
// Check if any non-blank content exists
|
|
40
80
|
if (currentLines.some((l) => l.trim())) {
|
|
41
81
|
subBlocks.push({ startOffset: currentOffset, lines: currentLines });
|
|
@@ -58,17 +98,20 @@ export function parseBridge(text) {
|
|
|
58
98
|
while (firstContentLine < sub.lines.length && !sub.lines[firstContentLine].trim())
|
|
59
99
|
firstContentLine++;
|
|
60
100
|
const firstLine = sub.lines[firstContentLine]?.trim();
|
|
61
|
-
if (firstLine && /^
|
|
101
|
+
if (firstLine && /^tool\s/i.test(firstLine)) {
|
|
62
102
|
instructions.push(parseToolBlock(subText, sub.startOffset + firstContentLine, instructions));
|
|
63
103
|
}
|
|
104
|
+
else if (firstLine && /^define\s/i.test(firstLine)) {
|
|
105
|
+
instructions.push(parseDefineBlock(subText, sub.startOffset + firstContentLine));
|
|
106
|
+
}
|
|
64
107
|
else if (firstLine && /^bridge\s/i.test(firstLine)) {
|
|
65
|
-
instructions.push(...parseBridgeBlock(subText, sub.startOffset + firstContentLine));
|
|
108
|
+
instructions.push(...parseBridgeBlock(subText, sub.startOffset + firstContentLine, instructions));
|
|
66
109
|
}
|
|
67
110
|
else if (firstLine && /^const\s/i.test(firstLine)) {
|
|
68
111
|
instructions.push(...parseConstLines(subText, sub.startOffset + firstContentLine));
|
|
69
112
|
}
|
|
70
113
|
else if (firstLine && !firstLine.startsWith("#")) {
|
|
71
|
-
throw new Error(`Line ${sub.startOffset + firstContentLine + 1}: Expected "tool", "
|
|
114
|
+
throw new Error(`Line ${sub.startOffset + firstContentLine + 1}: Expected "tool", "define", "bridge", or "const" declaration, got: ${firstLine}`);
|
|
72
115
|
}
|
|
73
116
|
}
|
|
74
117
|
}
|
|
@@ -84,7 +127,30 @@ function isJsonLiteral(s) {
|
|
|
84
127
|
return /^["\{\[\d]/.test(s) || /^-\d/.test(s) ||
|
|
85
128
|
s === "true" || s === "false" || s === "null";
|
|
86
129
|
}
|
|
87
|
-
function parseBridgeBlock(block, lineOffset) {
|
|
130
|
+
function parseBridgeBlock(block, lineOffset, previousInstructions) {
|
|
131
|
+
// ── Passthrough shorthand: `bridge Type.field with <name>` ──────────
|
|
132
|
+
// Expands into a full bridge that wires all input through the named
|
|
133
|
+
// handle (typically a define) and returns its output directly.
|
|
134
|
+
const shorthandMatch = block.match(/^bridge\s+(\w+)\.(\w+)\s+with\s+(\S+)\s*$/im);
|
|
135
|
+
if (shorthandMatch) {
|
|
136
|
+
const [, sType, sField, sName] = shorthandMatch;
|
|
137
|
+
const sHandle = sName.includes(".") ? sName.substring(sName.lastIndexOf(".") + 1) : sName;
|
|
138
|
+
const expanded = [
|
|
139
|
+
`bridge ${sType}.${sField} {`,
|
|
140
|
+
` with ${sName} as ${sHandle}`,
|
|
141
|
+
` with input`,
|
|
142
|
+
` with output as __out`,
|
|
143
|
+
` ${sHandle} <- input`,
|
|
144
|
+
` __out <- ${sHandle}`,
|
|
145
|
+
`}`,
|
|
146
|
+
].join("\n");
|
|
147
|
+
const result = parseBridgeBlock(expanded, lineOffset, previousInstructions);
|
|
148
|
+
// Tag the bridge instruction with the passthrough name for serialization
|
|
149
|
+
const bridgeInst = result.find((i) => i.kind === "bridge");
|
|
150
|
+
if (bridgeInst)
|
|
151
|
+
bridgeInst.passthrough = sName;
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
88
154
|
// Validate mandatory braces: `bridge Foo.bar {` ... `}`
|
|
89
155
|
const rawLines = block.split("\n");
|
|
90
156
|
const keywordIdx = rawLines.findIndex((l) => /^bridge\s/i.test(l.trim()));
|
|
@@ -134,7 +200,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
134
200
|
if (!bridgeType) {
|
|
135
201
|
throw new Error(`Line ${ln(i)}: "with" declaration must come after "bridge" declaration`);
|
|
136
202
|
}
|
|
137
|
-
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters,
|
|
203
|
+
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters, previousInstructions ?? [], ln(i));
|
|
138
204
|
continue;
|
|
139
205
|
}
|
|
140
206
|
// First non-header line — body starts here
|
|
@@ -147,6 +213,8 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
147
213
|
// ── Parse wire lines ────────────────────────────────────────────────
|
|
148
214
|
const wires = [];
|
|
149
215
|
let currentArrayToPath = null;
|
|
216
|
+
let currentIterHandle = null;
|
|
217
|
+
const arrayIterators = {};
|
|
150
218
|
/** Monotonically-increasing index; combined with a high base to produce
|
|
151
219
|
* fork instances that can never collide with regular handle instances. */
|
|
152
220
|
let nextForkSeq = 0;
|
|
@@ -167,7 +235,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
167
235
|
function buildSourceExpr(sourceStr, lineNum, forceOnOutermost) {
|
|
168
236
|
const parts = sourceStr.split(":");
|
|
169
237
|
if (parts.length === 1) {
|
|
170
|
-
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField);
|
|
238
|
+
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField, lineNum);
|
|
171
239
|
}
|
|
172
240
|
// Pipe chain
|
|
173
241
|
const actualSource = parts[parts.length - 1];
|
|
@@ -184,7 +252,7 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
184
252
|
throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${handleName}". Add 'with <tool> as ${handleName}' to the bridge header.`);
|
|
185
253
|
}
|
|
186
254
|
}
|
|
187
|
-
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField);
|
|
255
|
+
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField, lineNum);
|
|
188
256
|
const reversedTokens = [...tokenChain].reverse();
|
|
189
257
|
for (let idx = 0; idx < reversedTokens.length; idx++) {
|
|
190
258
|
const tok = reversedTokens[idx];
|
|
@@ -205,72 +273,165 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
205
273
|
}
|
|
206
274
|
return prevOutRef; // fork-root ref
|
|
207
275
|
}
|
|
276
|
+
// ── Whether we are inside an element-mapping brace block
|
|
277
|
+
let inElementBlock = false;
|
|
208
278
|
for (let i = bodyStartIndex; i < lines.length; i++) {
|
|
209
279
|
const raw = lines[i];
|
|
210
280
|
const line = raw.trim();
|
|
211
281
|
if (!line || line.startsWith("#")) {
|
|
212
282
|
continue;
|
|
213
283
|
}
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
284
|
+
// Closing brace of an element mapping block `{ ... }`
|
|
285
|
+
if (line === "}") {
|
|
286
|
+
if (inElementBlock) {
|
|
287
|
+
currentArrayToPath = null;
|
|
288
|
+
currentIterHandle = null;
|
|
289
|
+
inElementBlock = false;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Line ${ln(i)}: Unexpected "}" — not inside an element block`);
|
|
293
|
+
}
|
|
294
|
+
// Element mapping lines (inside a brace block)
|
|
295
|
+
if (inElementBlock && currentArrayToPath) {
|
|
296
|
+
if (!line.startsWith(".")) {
|
|
297
|
+
throw new Error(`Line ${ln(i)}: Element mapping lines must start with ".": ${line}`);
|
|
298
|
+
}
|
|
299
|
+
// Constant: .target = "value" or .target = value
|
|
300
|
+
const elemConstMatch = line.match(/^\.(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
301
|
+
if (elemConstMatch) {
|
|
302
|
+
const [, fieldName, quotedValue, unquotedValue] = elemConstMatch;
|
|
303
|
+
const value = quotedValue ?? unquotedValue;
|
|
304
|
+
wires.push({
|
|
305
|
+
value,
|
|
306
|
+
to: {
|
|
307
|
+
module: SELF_MODULE,
|
|
308
|
+
type: bridgeType,
|
|
309
|
+
field: bridgeField,
|
|
310
|
+
element: true,
|
|
311
|
+
path: [...currentArrayToPath, ...parsePath(fieldName)],
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// Simple pull: .target <- <iter>.source (element-relative, no fallbacks)
|
|
317
|
+
const iterPfx = `${currentIterHandle}.`;
|
|
318
|
+
const elemRelMatch = line.match(/^\.(\S+)\s*<-\s*(\S+)$/);
|
|
319
|
+
if (elemRelMatch && elemRelMatch[2].startsWith(iterPfx)) {
|
|
320
|
+
const toPath = [...currentArrayToPath, ...parsePath(elemRelMatch[1])];
|
|
321
|
+
const fromPath = parsePath(elemRelMatch[2].slice(iterPfx.length));
|
|
322
|
+
wires.push({
|
|
323
|
+
from: {
|
|
324
|
+
module: SELF_MODULE,
|
|
325
|
+
type: bridgeType,
|
|
326
|
+
field: bridgeField,
|
|
327
|
+
element: true,
|
|
328
|
+
path: fromPath,
|
|
329
|
+
},
|
|
330
|
+
to: {
|
|
331
|
+
module: SELF_MODULE,
|
|
332
|
+
type: bridgeType,
|
|
333
|
+
field: bridgeField,
|
|
334
|
+
path: toPath,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// Pull with fallbacks: .target <- source || "fallback" ?? errorSrc (relative or handle path)
|
|
340
|
+
const elemArrowMatch = line.match(/^\.(\S+)\s*<-\s*(.+)$/);
|
|
341
|
+
if (elemArrowMatch) {
|
|
342
|
+
const [, toField, rhs] = elemArrowMatch;
|
|
343
|
+
const toPath = [...currentArrayToPath, ...parsePath(toField)];
|
|
344
|
+
const toRef = { module: SELF_MODULE, type: bridgeType, field: bridgeField, path: toPath };
|
|
345
|
+
// Strip ?? tail
|
|
346
|
+
let exprCore = rhs.trim();
|
|
347
|
+
let fallback;
|
|
348
|
+
let fallbackRefStr;
|
|
349
|
+
const qqIdx = exprCore.lastIndexOf(" ?? ");
|
|
350
|
+
if (qqIdx !== -1) {
|
|
351
|
+
const tail = exprCore.slice(qqIdx + 4).trim();
|
|
352
|
+
exprCore = exprCore.slice(0, qqIdx).trim();
|
|
353
|
+
if (isJsonLiteral(tail))
|
|
354
|
+
fallback = tail;
|
|
355
|
+
else
|
|
356
|
+
fallbackRefStr = tail;
|
|
357
|
+
}
|
|
358
|
+
// Split on || — last may be JSON literal (nullFallback)
|
|
359
|
+
const orParts = exprCore.split(" || ").map((s) => s.trim());
|
|
360
|
+
let nullFallback;
|
|
361
|
+
let sourceParts = orParts;
|
|
362
|
+
if (orParts.length > 1 && isJsonLiteral(orParts[orParts.length - 1])) {
|
|
363
|
+
nullFallback = orParts[orParts.length - 1];
|
|
364
|
+
sourceParts = orParts.slice(0, -1);
|
|
365
|
+
}
|
|
366
|
+
let fallbackRef;
|
|
367
|
+
let fallbackInternalWires = [];
|
|
368
|
+
if (fallbackRefStr) {
|
|
369
|
+
const preLen = wires.length;
|
|
370
|
+
fallbackRef = buildSourceExpr(fallbackRefStr, ln(i), false);
|
|
371
|
+
fallbackInternalWires = wires.splice(preLen);
|
|
372
|
+
}
|
|
373
|
+
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
374
|
+
const srcStr = sourceParts[ci];
|
|
375
|
+
const isLast = ci === sourceParts.length - 1;
|
|
376
|
+
// Element-relative source: starts with "<iter>."
|
|
377
|
+
const iterPrefix = `${currentIterHandle}.`;
|
|
378
|
+
let fromRef;
|
|
379
|
+
if (srcStr.startsWith(iterPrefix)) {
|
|
380
|
+
fromRef = {
|
|
381
|
+
module: SELF_MODULE,
|
|
382
|
+
type: bridgeType,
|
|
383
|
+
field: bridgeField,
|
|
384
|
+
element: true,
|
|
385
|
+
path: parsePath(srcStr.slice(iterPrefix.length)),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
else if (srcStr.startsWith(".")) {
|
|
389
|
+
throw new Error(`Line ${ln(i)}: Use "${currentIterHandle}.field" to reference element fields, not ".field"`);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
fromRef = buildSourceExpr(srcStr, ln(i), false);
|
|
393
|
+
}
|
|
394
|
+
const lastAttrs = isLast ? {
|
|
395
|
+
...(nullFallback ? { nullFallback } : {}),
|
|
396
|
+
...(fallback ? { fallback } : {}),
|
|
397
|
+
...(fallbackRef ? { fallbackRef } : {}),
|
|
398
|
+
} : {};
|
|
399
|
+
wires.push({ from: fromRef, to: toRef, ...lastAttrs });
|
|
400
|
+
}
|
|
401
|
+
wires.push(...fallbackInternalWires);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
throw new Error(`Line ${ln(i)}: Invalid element mapping line: ${line}`);
|
|
238
405
|
}
|
|
239
|
-
// End of array mapping block
|
|
240
|
-
currentArrayToPath = null;
|
|
241
406
|
// Constant wire: target = "value" or target = value (unquoted)
|
|
242
407
|
const constantMatch = line.match(/^(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
243
408
|
if (constantMatch) {
|
|
244
409
|
const [, targetStr, quotedValue, unquotedValue] = constantMatch;
|
|
245
410
|
const value = quotedValue ?? unquotedValue;
|
|
246
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField);
|
|
411
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
247
412
|
wires.push({ value, to: toRef });
|
|
248
413
|
continue;
|
|
249
414
|
}
|
|
250
415
|
// ── 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
416
|
const arrowMatch = line.match(/^(\S+)\s*<-(!?)\s*(.+)$/);
|
|
262
417
|
if (arrowMatch) {
|
|
263
418
|
const [, targetStr, forceFlag, rhs] = arrowMatch;
|
|
264
419
|
const force = forceFlag === "!";
|
|
265
420
|
const rhsTrimmed = rhs.trim();
|
|
266
|
-
// ── Array mapping: target
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
421
|
+
// ── Array mapping: target <- source[] as <iter> {
|
|
422
|
+
// Opens a brace-delimited element block.
|
|
423
|
+
const arrayBraceMatch = rhsTrimmed.match(/^(\S+)\[\]\s+as\s+(\w+)\s*\{\s*$/);
|
|
424
|
+
if (arrayBraceMatch) {
|
|
425
|
+
const fromClean = arrayBraceMatch[1];
|
|
426
|
+
const iterHandle = arrayBraceMatch[2];
|
|
427
|
+
assertNotReserved(iterHandle, ln(i), "iterator handle");
|
|
428
|
+
const fromRef = resolveAddress(fromClean, handleRes, bridgeType, bridgeField, ln(i));
|
|
429
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
272
430
|
wires.push({ from: fromRef, to: toRef });
|
|
273
431
|
currentArrayToPath = toRef.path;
|
|
432
|
+
currentIterHandle = iterHandle;
|
|
433
|
+
arrayIterators[toRef.path[0]] = iterHandle;
|
|
434
|
+
inElementBlock = true;
|
|
274
435
|
continue;
|
|
275
436
|
}
|
|
276
437
|
// ── Strip the ?? tail (last " ?? " wins in case source contains " ?? ")
|
|
@@ -300,38 +461,27 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
300
461
|
if (sourceParts.length === 0) {
|
|
301
462
|
throw new Error(`Line ${ln(i)}: Wire has no source expression: ${line}`);
|
|
302
463
|
}
|
|
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
464
|
let fallbackRef;
|
|
308
465
|
let fallbackInternalWires = [];
|
|
309
466
|
if (fallbackRefStr) {
|
|
310
467
|
const preLen = wires.length;
|
|
311
468
|
fallbackRef = buildSourceExpr(fallbackRefStr, ln(i), false);
|
|
312
|
-
// Splice out internal wires buildSourceExpr just added; push after sources.
|
|
313
469
|
fallbackInternalWires = wires.splice(preLen);
|
|
314
470
|
}
|
|
315
|
-
|
|
316
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField);
|
|
471
|
+
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
317
472
|
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
318
473
|
const isFirst = ci === 0;
|
|
319
474
|
const isLast = ci === sourceParts.length - 1;
|
|
320
475
|
const srcStr = sourceParts[ci];
|
|
321
|
-
// Parse source expression; for pipe chains buildSourceExpr pushes
|
|
322
|
-
// intermediate wires and returns the fork-root ref.
|
|
323
476
|
const termRef = buildSourceExpr(srcStr, ln(i), force && isFirst);
|
|
324
477
|
const isPipeFork = termRef.instance != null && termRef.path.length === 0
|
|
325
478
|
&& srcStr.includes(":");
|
|
326
|
-
// attrs carried only on the LAST wire of the coalesce chain
|
|
327
479
|
const lastAttrs = isLast ? {
|
|
328
480
|
...(nullFallback ? { nullFallback } : {}),
|
|
329
481
|
...(fallback ? { fallback } : {}),
|
|
330
482
|
...(fallbackRef ? { fallbackRef } : {}),
|
|
331
483
|
} : {};
|
|
332
484
|
if (isPipeFork) {
|
|
333
|
-
// Terminal pipe wire: fork-root → target (force only on outermost
|
|
334
|
-
// intermediate wire, already set inside buildSourceExpr)
|
|
335
485
|
wires.push({ from: termRef, to: toRef, pipe: true, ...lastAttrs });
|
|
336
486
|
}
|
|
337
487
|
else {
|
|
@@ -343,19 +493,29 @@ function parseBridgeBlock(block, lineOffset) {
|
|
|
343
493
|
});
|
|
344
494
|
}
|
|
345
495
|
}
|
|
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
496
|
wires.push(...fallbackInternalWires);
|
|
349
497
|
continue;
|
|
350
498
|
}
|
|
351
499
|
throw new Error(`Line ${ln(i)}: Unrecognized line: ${line}`);
|
|
352
500
|
}
|
|
501
|
+
// ── Inline define invocations ───────────────────────────────────────
|
|
502
|
+
const nextForkSeqRef = { value: nextForkSeq };
|
|
503
|
+
for (const hb of handleBindings) {
|
|
504
|
+
if (hb.kind !== "define")
|
|
505
|
+
continue;
|
|
506
|
+
const def = previousInstructions?.find((inst) => inst.kind === "define" && inst.name === hb.name);
|
|
507
|
+
if (!def) {
|
|
508
|
+
throw new Error(`Define "${hb.name}" referenced by handle "${hb.handle}" not found`);
|
|
509
|
+
}
|
|
510
|
+
inlineDefine(hb.handle, def, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef);
|
|
511
|
+
}
|
|
353
512
|
instructions.unshift({
|
|
354
513
|
kind: "bridge",
|
|
355
514
|
type: bridgeType,
|
|
356
515
|
field: bridgeField,
|
|
357
516
|
handles: handleBindings,
|
|
358
517
|
wires,
|
|
518
|
+
arrayIterators: Object.keys(arrayIterators).length > 0 ? arrayIterators : undefined,
|
|
359
519
|
pipeHandles: pipeHandleEntries.length > 0 ? pipeHandleEntries : undefined,
|
|
360
520
|
});
|
|
361
521
|
return instructions;
|
|
@@ -390,6 +550,45 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
390
550
|
});
|
|
391
551
|
return;
|
|
392
552
|
}
|
|
553
|
+
// with input (shorthand — handle defaults to "input")
|
|
554
|
+
match = line.match(/^with\s+input$/i);
|
|
555
|
+
if (match) {
|
|
556
|
+
const handle = "input";
|
|
557
|
+
checkDuplicate(handle);
|
|
558
|
+
handleBindings.push({ handle, kind: "input" });
|
|
559
|
+
handleRes.set(handle, {
|
|
560
|
+
module: SELF_MODULE,
|
|
561
|
+
type: bridgeType,
|
|
562
|
+
field: bridgeField,
|
|
563
|
+
});
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
// with output as <handle>
|
|
567
|
+
match = line.match(/^with\s+output\s+as\s+(\w+)$/i);
|
|
568
|
+
if (match) {
|
|
569
|
+
const handle = match[1];
|
|
570
|
+
checkDuplicate(handle);
|
|
571
|
+
handleBindings.push({ handle, kind: "output" });
|
|
572
|
+
handleRes.set(handle, {
|
|
573
|
+
module: SELF_MODULE,
|
|
574
|
+
type: bridgeType,
|
|
575
|
+
field: bridgeField,
|
|
576
|
+
});
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
// with output (shorthand — handle defaults to "output")
|
|
580
|
+
match = line.match(/^with\s+output$/i);
|
|
581
|
+
if (match) {
|
|
582
|
+
const handle = "output";
|
|
583
|
+
checkDuplicate(handle);
|
|
584
|
+
handleBindings.push({ handle, kind: "output" });
|
|
585
|
+
handleRes.set(handle, {
|
|
586
|
+
module: SELF_MODULE,
|
|
587
|
+
type: bridgeType,
|
|
588
|
+
field: bridgeField,
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
393
592
|
// with context as <handle>
|
|
394
593
|
match = line.match(/^with\s+context\s+as\s+(\w+)$/i);
|
|
395
594
|
if (match) {
|
|
@@ -442,13 +641,24 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
442
641
|
});
|
|
443
642
|
return;
|
|
444
643
|
}
|
|
445
|
-
// with <name> as <handle> —
|
|
644
|
+
// with <name> as <handle> — check for define invocation first
|
|
446
645
|
match = line.match(/^with\s+(\S+)\s+as\s+(\w+)$/i);
|
|
447
646
|
if (match) {
|
|
448
647
|
const name = match[1];
|
|
449
648
|
const handle = match[2];
|
|
450
649
|
checkDuplicate(handle);
|
|
451
|
-
|
|
650
|
+
assertNotReserved(handle, lineNum, "handle alias");
|
|
651
|
+
// Check if name matches a known define
|
|
652
|
+
const defineDef = instructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
653
|
+
if (defineDef) {
|
|
654
|
+
handleBindings.push({ handle, kind: "define", name });
|
|
655
|
+
handleRes.set(handle, {
|
|
656
|
+
module: `__define_${handle}`,
|
|
657
|
+
type: bridgeType,
|
|
658
|
+
field: bridgeField,
|
|
659
|
+
});
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
452
662
|
const lastDot = name.lastIndexOf(".");
|
|
453
663
|
if (lastDot !== -1) {
|
|
454
664
|
const modulePart = name.substring(0, lastDot);
|
|
@@ -487,6 +697,17 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
487
697
|
const lastDot = name.lastIndexOf(".");
|
|
488
698
|
const handle = lastDot !== -1 ? name.substring(lastDot + 1) : name;
|
|
489
699
|
checkDuplicate(handle);
|
|
700
|
+
// Check if name matches a known define
|
|
701
|
+
const defineDef = instructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
702
|
+
if (defineDef) {
|
|
703
|
+
handleBindings.push({ handle, kind: "define", name });
|
|
704
|
+
handleRes.set(handle, {
|
|
705
|
+
module: `__define_${handle}`,
|
|
706
|
+
type: bridgeType,
|
|
707
|
+
field: bridgeField,
|
|
708
|
+
});
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
490
711
|
if (lastDot !== -1) {
|
|
491
712
|
const modulePart = name.substring(0, lastDot);
|
|
492
713
|
const fieldPart = name.substring(lastDot + 1);
|
|
@@ -507,6 +728,155 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
507
728
|
}
|
|
508
729
|
throw new Error(`Line ${lineNum}: Invalid with declaration: ${line}`);
|
|
509
730
|
}
|
|
731
|
+
// ── Define inlining ─────────────────────────────────────────────────────────
|
|
732
|
+
/**
|
|
733
|
+
* Inline a define invocation into a bridge's wires.
|
|
734
|
+
*
|
|
735
|
+
* Splits the define handle into separate input/output synthetic trunks,
|
|
736
|
+
* clones the define's internal wires with remapped references, and adds
|
|
737
|
+
* them to the bridge. Tool instances are remapped to avoid collisions.
|
|
738
|
+
*
|
|
739
|
+
* The executor treats synthetic trunks (module starting with `__define_`)
|
|
740
|
+
* as pass-through data containers — no tool function is called.
|
|
741
|
+
*/
|
|
742
|
+
function inlineDefine(defineHandle, defineDef, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef) {
|
|
743
|
+
const genericModule = `__define_${defineHandle}`;
|
|
744
|
+
const inModule = `__define_in_${defineHandle}`;
|
|
745
|
+
const outModule = `__define_out_${defineHandle}`;
|
|
746
|
+
// The define was parsed as synthetic `bridge Define.<name>`, so its
|
|
747
|
+
// internal refs use type="Define", field=defineName for I/O, and
|
|
748
|
+
// standard tool resolutions for tools.
|
|
749
|
+
const defType = "Define";
|
|
750
|
+
const defField = defineDef.name;
|
|
751
|
+
// ── 1. Build trunk remapping for define's tool handles ──────────────
|
|
752
|
+
// Replay define's instance counter to determine original instances
|
|
753
|
+
const defCounters = new Map();
|
|
754
|
+
const trunkRemap = new Map();
|
|
755
|
+
for (const hb of defineDef.handles) {
|
|
756
|
+
if (hb.kind === "input" || hb.kind === "output" || hb.kind === "context" || hb.kind === "const")
|
|
757
|
+
continue;
|
|
758
|
+
if (hb.kind === "define")
|
|
759
|
+
continue; // nested defines — future
|
|
760
|
+
const name = hb.kind === "tool" ? hb.name : "";
|
|
761
|
+
if (!name)
|
|
762
|
+
continue;
|
|
763
|
+
const lastDot = name.lastIndexOf(".");
|
|
764
|
+
let oldModule, oldType, oldField, instanceKey, bridgeKey;
|
|
765
|
+
if (lastDot !== -1) {
|
|
766
|
+
oldModule = name.substring(0, lastDot);
|
|
767
|
+
oldType = defType;
|
|
768
|
+
oldField = name.substring(lastDot + 1);
|
|
769
|
+
instanceKey = `${oldModule}:${oldField}`;
|
|
770
|
+
bridgeKey = instanceKey;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
oldModule = SELF_MODULE;
|
|
774
|
+
oldType = "Tools";
|
|
775
|
+
oldField = name;
|
|
776
|
+
instanceKey = `Tools:${name}`;
|
|
777
|
+
bridgeKey = instanceKey;
|
|
778
|
+
}
|
|
779
|
+
// Old instance (from define's isolated counter)
|
|
780
|
+
const oldInstance = (defCounters.get(instanceKey) ?? 0) + 1;
|
|
781
|
+
defCounters.set(instanceKey, oldInstance);
|
|
782
|
+
// New instance (from bridge's counter)
|
|
783
|
+
const newInstance = (instanceCounters.get(bridgeKey) ?? 0) + 1;
|
|
784
|
+
instanceCounters.set(bridgeKey, newInstance);
|
|
785
|
+
const oldKey = `${oldModule}:${oldType}:${oldField}:${oldInstance}`;
|
|
786
|
+
trunkRemap.set(oldKey, { module: oldModule, type: oldType, field: oldField, instance: newInstance });
|
|
787
|
+
// Add internal tool handle to bridge's handle bindings (namespaced)
|
|
788
|
+
handleBindings.push({ handle: `${defineHandle}$${hb.handle}`, kind: "tool", name });
|
|
789
|
+
}
|
|
790
|
+
// ── 2. Remap bridge wires involving the define handle ───────────────
|
|
791
|
+
for (const wire of wires) {
|
|
792
|
+
if ("from" in wire) {
|
|
793
|
+
if (wire.to.module === genericModule) {
|
|
794
|
+
wire.to = { ...wire.to, module: inModule };
|
|
795
|
+
}
|
|
796
|
+
if (wire.from.module === genericModule) {
|
|
797
|
+
wire.from = { ...wire.from, module: outModule };
|
|
798
|
+
}
|
|
799
|
+
if (wire.fallbackRef?.module === genericModule) {
|
|
800
|
+
wire.fallbackRef = { ...wire.fallbackRef, module: outModule };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if ("value" in wire && wire.to.module === genericModule) {
|
|
804
|
+
wire.to = { ...wire.to, module: inModule };
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
// ── 3. Clone, remap, and add define's wires ────────────────────────
|
|
808
|
+
// Compute fork instance offset (define's fork instances start at 100000,
|
|
809
|
+
// bridge's may overlap — offset them to avoid collision)
|
|
810
|
+
const forkOffset = nextForkSeqRef.value;
|
|
811
|
+
let maxDefForkSeq = 0;
|
|
812
|
+
function remapRef(ref, side) {
|
|
813
|
+
// Define I/O trunk → split into input/output synthetic trunks
|
|
814
|
+
if (ref.module === SELF_MODULE && ref.type === defType && ref.field === defField) {
|
|
815
|
+
const targetModule = side === "from" ? inModule : outModule;
|
|
816
|
+
return { ...ref, module: targetModule, type: bridgeType, field: bridgeField };
|
|
817
|
+
}
|
|
818
|
+
// Tool trunk → remap instance
|
|
819
|
+
const key = `${ref.module}:${ref.type}:${ref.field}:${ref.instance ?? ""}`;
|
|
820
|
+
const newTrunk = trunkRemap.get(key);
|
|
821
|
+
if (newTrunk) {
|
|
822
|
+
return { ...ref, module: newTrunk.module, type: newTrunk.type, field: newTrunk.field, instance: newTrunk.instance };
|
|
823
|
+
}
|
|
824
|
+
// Fork instance → offset (fork instances are >= 100000)
|
|
825
|
+
if (ref.instance != null && ref.instance >= 100000) {
|
|
826
|
+
const defSeq = ref.instance - 100000;
|
|
827
|
+
if (defSeq + 1 > maxDefForkSeq)
|
|
828
|
+
maxDefForkSeq = defSeq + 1;
|
|
829
|
+
return { ...ref, instance: ref.instance + forkOffset };
|
|
830
|
+
}
|
|
831
|
+
return ref;
|
|
832
|
+
}
|
|
833
|
+
for (const wire of defineDef.wires) {
|
|
834
|
+
const cloned = JSON.parse(JSON.stringify(wire));
|
|
835
|
+
if ("from" in cloned) {
|
|
836
|
+
cloned.from = remapRef(cloned.from, "from");
|
|
837
|
+
cloned.to = remapRef(cloned.to, "to");
|
|
838
|
+
if (cloned.fallbackRef) {
|
|
839
|
+
cloned.fallbackRef = remapRef(cloned.fallbackRef, "from");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
// Constant wire
|
|
844
|
+
cloned.to = remapRef(cloned.to, "to");
|
|
845
|
+
}
|
|
846
|
+
wires.push(cloned);
|
|
847
|
+
}
|
|
848
|
+
// Advance bridge's fork counter past define's forks
|
|
849
|
+
nextForkSeqRef.value += maxDefForkSeq;
|
|
850
|
+
// ── 4. Remap and merge pipe handles ─────────────────────────────────
|
|
851
|
+
if (defineDef.pipeHandles) {
|
|
852
|
+
for (const ph of defineDef.pipeHandles) {
|
|
853
|
+
const parts = ph.key.split(":");
|
|
854
|
+
// key format: "module:type:field:instance"
|
|
855
|
+
const phInstance = parseInt(parts[parts.length - 1]);
|
|
856
|
+
let newKey = ph.key;
|
|
857
|
+
if (phInstance >= 100000) {
|
|
858
|
+
const newInst = phInstance + forkOffset;
|
|
859
|
+
parts[parts.length - 1] = String(newInst);
|
|
860
|
+
newKey = parts.join(":");
|
|
861
|
+
}
|
|
862
|
+
// Remap baseTrunk
|
|
863
|
+
const bt = ph.baseTrunk;
|
|
864
|
+
const btKey = `${bt.module}:${defType}:${bt.field}:${bt.instance ?? ""}`;
|
|
865
|
+
const newBt = trunkRemap.get(btKey);
|
|
866
|
+
// Also try with Tools type for simple names
|
|
867
|
+
const btKey2 = `${bt.module}:Tools:${bt.field}:${bt.instance ?? ""}`;
|
|
868
|
+
const newBt2 = trunkRemap.get(btKey2);
|
|
869
|
+
const resolvedBt = newBt ?? newBt2;
|
|
870
|
+
pipeHandleEntries.push({
|
|
871
|
+
key: newKey,
|
|
872
|
+
handle: `${defineHandle}$${ph.handle}`,
|
|
873
|
+
baseTrunk: resolvedBt
|
|
874
|
+
? { module: resolvedBt.module, type: resolvedBt.type, field: resolvedBt.field, instance: resolvedBt.instance }
|
|
875
|
+
: ph.baseTrunk,
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
510
880
|
/**
|
|
511
881
|
* Resolve an address string into a structured NodeRef.
|
|
512
882
|
*
|
|
@@ -516,7 +886,7 @@ function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBi
|
|
|
516
886
|
* 3. Prefix matches a declared handle → resolve via handle binding
|
|
517
887
|
* 4. Otherwise → nested output path (e.g., topPick.address)
|
|
518
888
|
*/
|
|
519
|
-
function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
889
|
+
function resolveAddress(address, handles, bridgeType, bridgeField, lineNum) {
|
|
520
890
|
const dotIndex = address.indexOf(".");
|
|
521
891
|
if (dotIndex === -1) {
|
|
522
892
|
// Whole address is a declared handle → resolve to its root (path: [])
|
|
@@ -532,13 +902,9 @@ function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
|
532
902
|
ref.instance = resolution.instance;
|
|
533
903
|
return ref;
|
|
534
904
|
}
|
|
535
|
-
//
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
type: bridgeType,
|
|
539
|
-
field: bridgeField,
|
|
540
|
-
path: parsePath(address),
|
|
541
|
-
};
|
|
905
|
+
// Strict scoping: every reference must go through a declared handle.
|
|
906
|
+
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared reference "${address}". ` +
|
|
907
|
+
`Add 'with output as o' for output fields, or 'with ${address}' for a tool.`);
|
|
542
908
|
}
|
|
543
909
|
const prefix = address.substring(0, dotIndex);
|
|
544
910
|
const rest = address.substring(dotIndex + 1);
|
|
@@ -557,23 +923,9 @@ function resolveAddress(address, handles, bridgeType, bridgeField) {
|
|
|
557
923
|
}
|
|
558
924
|
return ref;
|
|
559
925
|
}
|
|
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
|
-
};
|
|
926
|
+
// Strict scoping: prefix must be a known handle.
|
|
927
|
+
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared handle "${prefix}". ` +
|
|
928
|
+
`Add 'with ${prefix}' or 'with ${prefix} as ${prefix}' to the bridge header.`);
|
|
577
929
|
}
|
|
578
930
|
// ── Const block parser ──────────────────────────────────────────────────────
|
|
579
931
|
/**
|
|
@@ -604,6 +956,7 @@ function parseConstLines(block, lineOffset) {
|
|
|
604
956
|
throw new Error(`Line ${ln(i)}: Expected const declaration, got: ${line}`);
|
|
605
957
|
}
|
|
606
958
|
const name = constMatch[1];
|
|
959
|
+
assertNotReserved(name, ln(i), "const name");
|
|
607
960
|
let valuePart = constMatch[2].trim();
|
|
608
961
|
// Multi-line: if value starts with { or [ and isn't balanced, read more lines
|
|
609
962
|
if (/^[{[]/.test(valuePart)) {
|
|
@@ -642,38 +995,92 @@ function parseConstLines(block, lineOffset) {
|
|
|
642
995
|
}
|
|
643
996
|
return results;
|
|
644
997
|
}
|
|
645
|
-
// ──
|
|
998
|
+
// ── Define block parser ─────────────────────────────────────────────────────
|
|
646
999
|
/**
|
|
647
|
-
* Parse a `
|
|
1000
|
+
* Parse a `define` block into a DefineDef instruction.
|
|
648
1001
|
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* with context
|
|
652
|
-
* baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
653
|
-
* headers.apiKey <- context.hereapi.apiKey
|
|
1002
|
+
* Delegates to parseBridgeBlock with a synthetic `bridge Define.<name>` header,
|
|
1003
|
+
* then converts the resulting Bridge to a DefineDef template.
|
|
654
1004
|
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
1005
|
+
* Example:
|
|
1006
|
+
* define secureProfile {
|
|
1007
|
+
* with userApi as api
|
|
1008
|
+
* with input as i
|
|
1009
|
+
* with output as o
|
|
1010
|
+
* api.id <- i.userId
|
|
1011
|
+
* o.name <- api.login
|
|
1012
|
+
* }
|
|
1013
|
+
*/
|
|
1014
|
+
function parseDefineBlock(block, lineOffset) {
|
|
1015
|
+
const rawLines = block.split("\n");
|
|
1016
|
+
// Find the define header line
|
|
1017
|
+
let headerIdx = -1;
|
|
1018
|
+
let defineName = "";
|
|
1019
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
1020
|
+
const line = rawLines[i].trim();
|
|
1021
|
+
if (!line || line.startsWith("#"))
|
|
1022
|
+
continue;
|
|
1023
|
+
const m = line.match(/^define\s+(\w+)\s*\{?\s*$/i);
|
|
1024
|
+
if (!m) {
|
|
1025
|
+
throw new Error(`Line ${lineOffset + i + 1}: Expected define declaration: define <name> {. Got: ${line}`);
|
|
1026
|
+
}
|
|
1027
|
+
defineName = m[1];
|
|
1028
|
+
assertNotReserved(defineName, lineOffset + i + 1, "define name");
|
|
1029
|
+
headerIdx = i;
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
if (!defineName) {
|
|
1033
|
+
throw new Error(`Line ${lineOffset + 1}: Missing define declaration`);
|
|
1034
|
+
}
|
|
1035
|
+
// Validate braces
|
|
1036
|
+
const kw = rawLines[headerIdx].trim();
|
|
1037
|
+
if (!kw.endsWith("{")) {
|
|
1038
|
+
throw new Error(`Line ${lineOffset + headerIdx + 1}: define block must use braces: define ${defineName} {`);
|
|
1039
|
+
}
|
|
1040
|
+
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
1041
|
+
if (!hasClose) {
|
|
1042
|
+
throw new Error(`Line ${lineOffset + headerIdx + 1}: define block missing closing }`);
|
|
1043
|
+
}
|
|
1044
|
+
// Rewrite header to a synthetic bridge: `bridge Define.<name> {`
|
|
1045
|
+
const syntheticLines = [...rawLines];
|
|
1046
|
+
syntheticLines[headerIdx] = rawLines[headerIdx]
|
|
1047
|
+
.replace(/^(\s*)define\s+\w+/i, `$1bridge Define.${defineName}`);
|
|
1048
|
+
const syntheticBlock = syntheticLines.join("\n");
|
|
1049
|
+
const results = parseBridgeBlock(syntheticBlock, lineOffset);
|
|
1050
|
+
const bridge = results[0];
|
|
1051
|
+
return {
|
|
1052
|
+
kind: "define",
|
|
1053
|
+
name: defineName,
|
|
1054
|
+
handles: bridge.handles,
|
|
1055
|
+
wires: bridge.wires,
|
|
1056
|
+
...(bridge.arrayIterators ? { arrayIterators: bridge.arrayIterators } : {}),
|
|
1057
|
+
...(bridge.pipeHandles ? { pipeHandles: bridge.pipeHandles } : {}),
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
// ── Tool block parser ───────────────────────────────────────────────────────
|
|
1061
|
+
/**
|
|
1062
|
+
* Parse a `tool` block into a ToolDef instruction.
|
|
659
1063
|
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
1064
|
+
* Format:
|
|
1065
|
+
* tool hereapi from httpCall {
|
|
662
1066
|
* with context
|
|
663
|
-
* baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1067
|
+
* .baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1068
|
+
* .headers.apiKey <- context.hereapi.apiKey
|
|
1069
|
+
* }
|
|
664
1070
|
*
|
|
665
|
-
*
|
|
666
|
-
* method = GET
|
|
667
|
-
* path = /geocode
|
|
1071
|
+
* tool hereapi.geocode from hereapi {
|
|
1072
|
+
* .method = GET
|
|
1073
|
+
* .path = /geocode
|
|
1074
|
+
* }
|
|
668
1075
|
*
|
|
669
|
-
* When
|
|
670
|
-
* it's treated as
|
|
1076
|
+
* When the source matches a previously-defined tool name,
|
|
1077
|
+
* it's treated as inheritance (child inherits parent). Otherwise the source
|
|
671
1078
|
* is treated as a function name.
|
|
672
1079
|
*/
|
|
673
1080
|
function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
674
1081
|
// Validate mandatory braces for blocks that have a body (deps / wires)
|
|
675
1082
|
const rawLines = block.split("\n");
|
|
676
|
-
const keywordIdx = rawLines.findIndex((l) => /^
|
|
1083
|
+
const keywordIdx = rawLines.findIndex((l) => /^tool\s/i.test(l.trim()));
|
|
677
1084
|
if (keywordIdx !== -1) {
|
|
678
1085
|
// Check if there are non-blank, non-comment body lines after the keyword
|
|
679
1086
|
const bodyLines = rawLines.slice(keywordIdx + 1).filter((l) => {
|
|
@@ -683,11 +1090,11 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
683
1090
|
const kw = rawLines[keywordIdx].trim();
|
|
684
1091
|
if (bodyLines.length > 0) {
|
|
685
1092
|
if (!kw.endsWith("{")) {
|
|
686
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}:
|
|
1093
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block with body must use braces: tool foo from bar {`);
|
|
687
1094
|
}
|
|
688
1095
|
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
689
1096
|
if (!hasClose) {
|
|
690
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}:
|
|
1097
|
+
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block missing closing }`);
|
|
691
1098
|
}
|
|
692
1099
|
}
|
|
693
1100
|
}
|
|
@@ -696,7 +1103,7 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
696
1103
|
const trimmed = l.trimEnd();
|
|
697
1104
|
if (trimmed === "}")
|
|
698
1105
|
return "";
|
|
699
|
-
if (/^
|
|
1106
|
+
if (/^tool\s/i.test(trimmed) && trimmed.endsWith("{"))
|
|
700
1107
|
return trimmed.replace(/\s*\{\s*$/, "");
|
|
701
1108
|
return trimmed;
|
|
702
1109
|
});
|
|
@@ -712,31 +1119,16 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
712
1119
|
const line = raw.trim();
|
|
713
1120
|
if (!line || line.startsWith("#"))
|
|
714
1121
|
continue;
|
|
715
|
-
// Tool declaration: tool <name>
|
|
1122
|
+
// Tool declaration: tool <name> from <source>
|
|
716
1123
|
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>`);
|
|
1124
|
+
const toolMatch = line.match(/^tool\s+(\S+)\s+from\s+(\S+)$/i);
|
|
1125
|
+
if (!toolMatch) {
|
|
1126
|
+
throw new Error(`Line ${ln(i)}: Invalid tool declaration: ${line}. Expected: tool <name> from <source>`);
|
|
736
1127
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1128
|
+
toolName = toolMatch[1];
|
|
1129
|
+
const source = toolMatch[2];
|
|
1130
|
+
assertNotReserved(toolName, ln(i), "tool name");
|
|
1131
|
+
// If source matches a previously-defined tool, it's inheritance; otherwise it's a function name
|
|
740
1132
|
const isKnownTool = previousInstructions?.some((inst) => inst.kind === "tool" && inst.name === source);
|
|
741
1133
|
if (isKnownTool) {
|
|
742
1134
|
toolExtends = source;
|
|
@@ -800,8 +1192,8 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
800
1192
|
wires.push({ kind: "onError", source: onErrorPullMatch[1] });
|
|
801
1193
|
continue;
|
|
802
1194
|
}
|
|
803
|
-
// Constant wire: target = "value" or target = value (unquoted)
|
|
804
|
-
const constantMatch = line.match(
|
|
1195
|
+
// Constant wire: .target = "value" or .target = value (unquoted)
|
|
1196
|
+
const constantMatch = line.match(/^\.(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
805
1197
|
if (constantMatch) {
|
|
806
1198
|
const value = constantMatch[2] ?? constantMatch[3];
|
|
807
1199
|
wires.push({
|
|
@@ -811,12 +1203,16 @@ function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
|
811
1203
|
});
|
|
812
1204
|
continue;
|
|
813
1205
|
}
|
|
814
|
-
// Pull wire: target <- source
|
|
815
|
-
const pullMatch = line.match(
|
|
1206
|
+
// Pull wire: .target <- source
|
|
1207
|
+
const pullMatch = line.match(/^\.(\S+)\s*<-\s*(\S+)$/);
|
|
816
1208
|
if (pullMatch) {
|
|
817
1209
|
wires.push({ target: pullMatch[1], kind: "pull", source: pullMatch[2] });
|
|
818
1210
|
continue;
|
|
819
1211
|
}
|
|
1212
|
+
// Catch bare param lines without leading dot — give a helpful error
|
|
1213
|
+
if (/^[a-zA-Z]/.test(line)) {
|
|
1214
|
+
throw new Error(`Line ${ln(i)}: Tool params require a dot prefix: ".${line.split(/[\s=<]/)[0]} ...". Only 'with' and 'on error' lines are unprefixed.`);
|
|
1215
|
+
}
|
|
820
1216
|
throw new Error(`Line ${ln(i)}: Unrecognized tool line: ${line}`);
|
|
821
1217
|
}
|
|
822
1218
|
if (!toolName)
|
|
@@ -862,7 +1258,8 @@ export function serializeBridge(instructions) {
|
|
|
862
1258
|
const bridges = instructions.filter((i) => i.kind === "bridge");
|
|
863
1259
|
const tools = instructions.filter((i) => i.kind === "tool");
|
|
864
1260
|
const consts = instructions.filter((i) => i.kind === "const");
|
|
865
|
-
|
|
1261
|
+
const defines = instructions.filter((i) => i.kind === "define");
|
|
1262
|
+
if (bridges.length === 0 && tools.length === 0 && consts.length === 0 && defines.length === 0)
|
|
866
1263
|
return "";
|
|
867
1264
|
const blocks = [];
|
|
868
1265
|
// Group const declarations into a single block
|
|
@@ -872,21 +1269,20 @@ export function serializeBridge(instructions) {
|
|
|
872
1269
|
for (const tool of tools) {
|
|
873
1270
|
blocks.push(serializeToolBlock(tool));
|
|
874
1271
|
}
|
|
1272
|
+
for (const def of defines) {
|
|
1273
|
+
blocks.push(serializeDefineBlock(def));
|
|
1274
|
+
}
|
|
875
1275
|
for (const bridge of bridges) {
|
|
876
1276
|
blocks.push(serializeBridgeBlock(bridge));
|
|
877
1277
|
}
|
|
878
|
-
return blocks.join("\n\n") + "\n";
|
|
1278
|
+
return `version ${BRIDGE_VERSION}\n\n` + blocks.join("\n\n") + "\n";
|
|
879
1279
|
}
|
|
880
1280
|
function serializeToolBlock(tool) {
|
|
881
1281
|
const lines = [];
|
|
882
1282
|
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
|
-
}
|
|
1283
|
+
// Declaration line — use `tool <name> from <source>` format
|
|
1284
|
+
const source = tool.extends ?? tool.fn;
|
|
1285
|
+
lines.push(hasBody ? `tool ${tool.name} from ${source} {` : `tool ${tool.name} from ${source}`);
|
|
890
1286
|
// Dependencies
|
|
891
1287
|
for (const dep of tool.deps) {
|
|
892
1288
|
if (dep.kind === "context") {
|
|
@@ -922,14 +1318,14 @@ function serializeToolBlock(tool) {
|
|
|
922
1318
|
else if (wire.kind === "constant") {
|
|
923
1319
|
// Use quoted form if value contains spaces or special chars, unquoted otherwise
|
|
924
1320
|
if (/\s/.test(wire.value) || wire.value === "") {
|
|
925
|
-
lines.push(`
|
|
1321
|
+
lines.push(` .${wire.target} = "${wire.value}"`);
|
|
926
1322
|
}
|
|
927
1323
|
else {
|
|
928
|
-
lines.push(`
|
|
1324
|
+
lines.push(` .${wire.target} = ${wire.value}`);
|
|
929
1325
|
}
|
|
930
1326
|
}
|
|
931
1327
|
else {
|
|
932
|
-
lines.push(`
|
|
1328
|
+
lines.push(` .${wire.target} <- ${wire.source}`);
|
|
933
1329
|
}
|
|
934
1330
|
}
|
|
935
1331
|
if (hasBody)
|
|
@@ -946,7 +1342,7 @@ function serializeToolBlock(tool) {
|
|
|
946
1342
|
* This is used to emit `?? handle.path` or `?? pipe:source` for wire
|
|
947
1343
|
* `fallbackRef` values.
|
|
948
1344
|
*/
|
|
949
|
-
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle) {
|
|
1345
|
+
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle) {
|
|
950
1346
|
const refTk = ref.instance != null
|
|
951
1347
|
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
952
1348
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
@@ -977,13 +1373,37 @@ function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge
|
|
|
977
1373
|
}
|
|
978
1374
|
}
|
|
979
1375
|
if (actualSourceRef && handleChain.length > 0) {
|
|
980
|
-
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, true);
|
|
1376
|
+
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, outputHandle, true);
|
|
981
1377
|
return `${handleChain.join(":")}:${sourceStr}`;
|
|
982
1378
|
}
|
|
983
1379
|
}
|
|
984
|
-
return serializeRef(ref, bridge, handleMap, inputHandle, true);
|
|
1380
|
+
return serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, true);
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Serialize a DefineDef into its textual form.
|
|
1384
|
+
*
|
|
1385
|
+
* Delegates to serializeBridgeBlock with a synthetic Bridge, then replaces
|
|
1386
|
+
* the `bridge Define.<name>` header with `define <name>`.
|
|
1387
|
+
*/
|
|
1388
|
+
function serializeDefineBlock(def) {
|
|
1389
|
+
const syntheticBridge = {
|
|
1390
|
+
kind: "bridge",
|
|
1391
|
+
type: "Define",
|
|
1392
|
+
field: def.name,
|
|
1393
|
+
handles: def.handles,
|
|
1394
|
+
wires: def.wires,
|
|
1395
|
+
arrayIterators: def.arrayIterators,
|
|
1396
|
+
pipeHandles: def.pipeHandles,
|
|
1397
|
+
};
|
|
1398
|
+
const bridgeText = serializeBridgeBlock(syntheticBridge);
|
|
1399
|
+
// Replace "bridge Define.<name>" → "define <name>"
|
|
1400
|
+
return bridgeText.replace(/^bridge Define\.(\w+)/, "define $1");
|
|
985
1401
|
}
|
|
986
1402
|
function serializeBridgeBlock(bridge) {
|
|
1403
|
+
// ── Passthrough shorthand ───────────────────────────────────────────
|
|
1404
|
+
if (bridge.passthrough) {
|
|
1405
|
+
return `bridge ${bridge.type}.${bridge.field} with ${bridge.passthrough}`;
|
|
1406
|
+
}
|
|
987
1407
|
const lines = [];
|
|
988
1408
|
// ── Header ──────────────────────────────────────────────────────────
|
|
989
1409
|
lines.push(`bridge ${bridge.type}.${bridge.field} {`);
|
|
@@ -1002,7 +1422,20 @@ function serializeBridgeBlock(bridge) {
|
|
|
1002
1422
|
break;
|
|
1003
1423
|
}
|
|
1004
1424
|
case "input":
|
|
1005
|
-
|
|
1425
|
+
if (h.handle === "input") {
|
|
1426
|
+
lines.push(` with input`);
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
lines.push(` with input as ${h.handle}`);
|
|
1430
|
+
}
|
|
1431
|
+
break;
|
|
1432
|
+
case "output":
|
|
1433
|
+
if (h.handle === "output") {
|
|
1434
|
+
lines.push(` with output`);
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
lines.push(` with output as ${h.handle}`);
|
|
1438
|
+
}
|
|
1006
1439
|
break;
|
|
1007
1440
|
case "context":
|
|
1008
1441
|
lines.push(` with context as ${h.handle}`);
|
|
@@ -1015,31 +1448,28 @@ function serializeBridgeBlock(bridge) {
|
|
|
1015
1448
|
lines.push(` with const as ${h.handle}`);
|
|
1016
1449
|
}
|
|
1017
1450
|
break;
|
|
1451
|
+
case "define":
|
|
1452
|
+
lines.push(` with ${h.name} as ${h.handle}`);
|
|
1453
|
+
break;
|
|
1018
1454
|
}
|
|
1019
1455
|
}
|
|
1020
1456
|
lines.push("");
|
|
1021
1457
|
// Mark where the wire body starts — everything after this gets 2-space indent
|
|
1022
1458
|
const wireBodyStart = lines.length;
|
|
1023
1459
|
// ── Build handle map for reverse resolution ─────────────────────────
|
|
1024
|
-
const { handleMap, inputHandle } = buildHandleMap(bridge);
|
|
1460
|
+
const { handleMap, inputHandle, outputHandle } = buildHandleMap(bridge);
|
|
1025
1461
|
// ── 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
1462
|
const pipeHandleTrunkKeys = new Set();
|
|
1029
1463
|
for (const ph of bridge.pipeHandles ?? []) {
|
|
1030
1464
|
handleMap.set(ph.key, ph.handle);
|
|
1031
1465
|
pipeHandleTrunkKeys.add(ph.key);
|
|
1032
1466
|
}
|
|
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.
|
|
1467
|
+
// ── Pipe wire detection ─────────────────────────────────────────────
|
|
1038
1468
|
const refTrunkKey = (ref) => ref.instance != null
|
|
1039
1469
|
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
1040
1470
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
1041
|
-
const toInMap = new Map();
|
|
1042
|
-
const fromOutMap = new Map();
|
|
1471
|
+
const toInMap = new Map();
|
|
1472
|
+
const fromOutMap = new Map();
|
|
1043
1473
|
const pipeWireSet = new Set();
|
|
1044
1474
|
for (const w of bridge.wires) {
|
|
1045
1475
|
if (!("from" in w) || !w.pipe)
|
|
@@ -1047,70 +1477,94 @@ function serializeBridgeBlock(bridge) {
|
|
|
1047
1477
|
const fw = w;
|
|
1048
1478
|
pipeWireSet.add(w);
|
|
1049
1479
|
const toTk = refTrunkKey(fw.to);
|
|
1050
|
-
// In-wire: single-segment path targeting a known pipe fork
|
|
1051
1480
|
if (fw.to.path.length === 1 && pipeHandleTrunkKeys.has(toTk)) {
|
|
1052
1481
|
toInMap.set(toTk, fw);
|
|
1053
1482
|
}
|
|
1054
|
-
// Out-wire: empty path from a known pipe fork
|
|
1055
1483
|
if (fw.from.path.length === 0 && pipeHandleTrunkKeys.has(refTrunkKey(fw.from))) {
|
|
1056
1484
|
fromOutMap.set(refTrunkKey(fw.from), fw);
|
|
1057
1485
|
}
|
|
1058
1486
|
}
|
|
1059
|
-
// ──
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1487
|
+
// ── Group element wires by array-destination field ──────────────────
|
|
1488
|
+
// Pull wires: from.element=true
|
|
1489
|
+
const elementPullWires = bridge.wires.filter((w) => "from" in w && !!w.from.element);
|
|
1490
|
+
// Constant wires: "value" in w && to.element=true
|
|
1491
|
+
const elementConstWires = bridge.wires.filter((w) => "value" in w && !!w.to.element);
|
|
1492
|
+
// Build grouped maps keyed by the array-destination field name (to.path[0])
|
|
1493
|
+
const elementPullGroups = new Map();
|
|
1494
|
+
const elementConstGroups = new Map();
|
|
1495
|
+
for (const w of elementPullWires) {
|
|
1496
|
+
const key = w.to.path[0];
|
|
1497
|
+
if (!elementPullGroups.has(key))
|
|
1498
|
+
elementPullGroups.set(key, []);
|
|
1499
|
+
elementPullGroups.get(key).push(w);
|
|
1500
|
+
}
|
|
1501
|
+
for (const w of elementConstWires) {
|
|
1502
|
+
const key = w.to.path[0];
|
|
1503
|
+
if (!elementConstGroups.has(key))
|
|
1504
|
+
elementConstGroups.set(key, []);
|
|
1505
|
+
elementConstGroups.get(key).push(w);
|
|
1069
1506
|
}
|
|
1507
|
+
// Union of keys that have any element wire (pull or constant)
|
|
1508
|
+
const allElementKeys = new Set([...elementPullGroups.keys(), ...elementConstGroups.keys()]);
|
|
1509
|
+
// ── Exclude pipe, element-pull, and element-const wires from main loop
|
|
1510
|
+
const regularWires = bridge.wires.filter((w) => !pipeWireSet.has(w) &&
|
|
1511
|
+
(!("from" in w) || !w.from.element) &&
|
|
1512
|
+
(!("value" in w) || !w.to.element));
|
|
1070
1513
|
const serializedArrays = new Set();
|
|
1514
|
+
// ── Helper: serialize a reference (forward outputHandle) ─────────────
|
|
1515
|
+
const sRef = (ref, isFrom) => serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom);
|
|
1516
|
+
const sPipeOrRef = (ref) => serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle);
|
|
1071
1517
|
for (const w of regularWires) {
|
|
1072
1518
|
// Constant wire
|
|
1073
1519
|
if ("value" in w) {
|
|
1074
|
-
const toStr =
|
|
1520
|
+
const toStr = sRef(w.to, false);
|
|
1075
1521
|
lines.push(`${toStr} = "${w.value}"`);
|
|
1076
1522
|
continue;
|
|
1077
1523
|
}
|
|
1078
|
-
// Array mapping
|
|
1524
|
+
// Array mapping — emit brace-delimited element block
|
|
1079
1525
|
const arrayKey = w.to.path.length === 1 ? w.to.path[0] : null;
|
|
1080
|
-
if (arrayKey &&
|
|
1081
|
-
elementGroups.has(arrayKey) &&
|
|
1082
|
-
!serializedArrays.has(arrayKey)) {
|
|
1526
|
+
if (arrayKey && allElementKeys.has(arrayKey) && !serializedArrays.has(arrayKey)) {
|
|
1083
1527
|
serializedArrays.add(arrayKey);
|
|
1084
|
-
const
|
|
1085
|
-
const
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1528
|
+
const iterName = bridge.arrayIterators?.[arrayKey] ?? "item";
|
|
1529
|
+
const fromStr = sRef(w.from, true) + "[]";
|
|
1530
|
+
const toStr = sRef(w.to, false);
|
|
1531
|
+
lines.push(`${toStr} <- ${fromStr} as ${iterName} {`);
|
|
1532
|
+
// Element constant wires (e.g. .provider = "RENFE")
|
|
1533
|
+
for (const ew of elementConstGroups.get(arrayKey) ?? []) {
|
|
1534
|
+
const fieldPath = ew.to.path.slice(1); // strip arrayKey prefix
|
|
1535
|
+
const elemTo = "." + serPath(fieldPath);
|
|
1536
|
+
lines.push(` ${elemTo} = "${ew.value}"`);
|
|
1537
|
+
}
|
|
1538
|
+
// Element pull wires (e.g. .name <- iter.title)
|
|
1539
|
+
for (const ew of elementPullGroups.get(arrayKey) ?? []) {
|
|
1540
|
+
const fromPart = ew.from.element
|
|
1541
|
+
? iterName + "." + serPath(ew.from.path)
|
|
1542
|
+
: sRef(ew.from, true);
|
|
1089
1543
|
const elemTo = "." + serPath(ew.to.path.slice(1));
|
|
1090
|
-
|
|
1544
|
+
// Handle fallbacks on element pull wires
|
|
1545
|
+
const nfb = "nullFallback" in ew && ew.nullFallback ? ` || ${ew.nullFallback}` : "";
|
|
1546
|
+
const errf = "fallbackRef" in ew && ew.fallbackRef
|
|
1547
|
+
? ` ?? ${sPipeOrRef(ew.fallbackRef)}`
|
|
1548
|
+
: "fallback" in ew && ew.fallback ? ` ?? ${ew.fallback}` : "";
|
|
1549
|
+
lines.push(` ${elemTo} <- ${fromPart}${nfb}${errf}`);
|
|
1091
1550
|
}
|
|
1551
|
+
lines.push(`}`);
|
|
1092
1552
|
continue;
|
|
1093
1553
|
}
|
|
1094
1554
|
// Regular wire
|
|
1095
|
-
const fromStr =
|
|
1096
|
-
const toStr =
|
|
1555
|
+
const fromStr = sRef(w.from, true);
|
|
1556
|
+
const toStr = sRef(w.to, false);
|
|
1097
1557
|
const arrow = w.force ? "<-!" : "<-";
|
|
1098
1558
|
const nfb = w.nullFallback ? ` || ${w.nullFallback}` : "";
|
|
1099
1559
|
const errf = w.fallbackRef
|
|
1100
|
-
? ` ?? ${
|
|
1560
|
+
? ` ?? ${sPipeOrRef(w.fallbackRef)}`
|
|
1101
1561
|
: w.fallback ? ` ?? ${w.fallback}` : "";
|
|
1102
1562
|
lines.push(`${toStr} ${arrow} ${fromStr}${nfb}${errf}`);
|
|
1103
1563
|
}
|
|
1104
1564
|
// ── 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
1565
|
for (const [tk, outWire] of fromOutMap.entries()) {
|
|
1110
|
-
// Non-terminal: this fork's result feeds another fork's input field
|
|
1111
1566
|
if (pipeHandleTrunkKeys.has(refTrunkKey(outWire.to)))
|
|
1112
1567
|
continue;
|
|
1113
|
-
// Follow chain backward to collect handle names (outermost-first)
|
|
1114
1568
|
const handleChain = [];
|
|
1115
1569
|
let currentTk = tk;
|
|
1116
1570
|
let actualSourceRef = null;
|
|
@@ -1119,18 +1573,15 @@ function serializeBridgeBlock(bridge) {
|
|
|
1119
1573
|
const handleName = handleMap.get(currentTk);
|
|
1120
1574
|
if (!handleName)
|
|
1121
1575
|
break;
|
|
1122
|
-
// Token: "handle" when field is "in" (default), otherwise "handle.field"
|
|
1123
1576
|
const inWire = toInMap.get(currentTk);
|
|
1124
1577
|
const fieldName = inWire?.to.path[0] ?? "in";
|
|
1125
1578
|
const token = fieldName === "in" ? handleName : `${handleName}.${fieldName}`;
|
|
1126
1579
|
handleChain.push(token);
|
|
1127
|
-
serializedPipeTrunks.add(currentTk);
|
|
1128
1580
|
if (inWire?.force)
|
|
1129
1581
|
chainForced = true;
|
|
1130
1582
|
if (!inWire)
|
|
1131
1583
|
break;
|
|
1132
1584
|
const fromTk = refTrunkKey(inWire.from);
|
|
1133
|
-
// Inner source is another pipe fork root (empty path) → continue chain
|
|
1134
1585
|
if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) {
|
|
1135
1586
|
currentTk = fromTk;
|
|
1136
1587
|
}
|
|
@@ -1140,12 +1591,12 @@ function serializeBridgeBlock(bridge) {
|
|
|
1140
1591
|
}
|
|
1141
1592
|
}
|
|
1142
1593
|
if (actualSourceRef && handleChain.length > 0) {
|
|
1143
|
-
const sourceStr =
|
|
1144
|
-
const destStr =
|
|
1594
|
+
const sourceStr = sRef(actualSourceRef, true);
|
|
1595
|
+
const destStr = sRef(outWire.to, false);
|
|
1145
1596
|
const arrow = chainForced ? "<-!" : "<-";
|
|
1146
1597
|
const nfb = outWire.nullFallback ? ` || ${outWire.nullFallback}` : "";
|
|
1147
1598
|
const errf = outWire.fallbackRef
|
|
1148
|
-
? ` ?? ${
|
|
1599
|
+
? ` ?? ${sPipeOrRef(outWire.fallbackRef)}`
|
|
1149
1600
|
: outWire.fallback ? ` ?? ${outWire.fallback}` : "";
|
|
1150
1601
|
lines.push(`${destStr} ${arrow} ${handleChain.join(":")}:${sourceStr}${nfb}${errf}`);
|
|
1151
1602
|
}
|
|
@@ -1165,6 +1616,7 @@ function buildHandleMap(bridge) {
|
|
|
1165
1616
|
const handleMap = new Map();
|
|
1166
1617
|
const instanceCounters = new Map();
|
|
1167
1618
|
let inputHandle;
|
|
1619
|
+
let outputHandle;
|
|
1168
1620
|
for (const h of bridge.handles) {
|
|
1169
1621
|
switch (h.kind) {
|
|
1170
1622
|
case "tool": {
|
|
@@ -1190,19 +1642,27 @@ function buildHandleMap(bridge) {
|
|
|
1190
1642
|
case "input":
|
|
1191
1643
|
inputHandle = h.handle;
|
|
1192
1644
|
break;
|
|
1645
|
+
case "output":
|
|
1646
|
+
outputHandle = h.handle;
|
|
1647
|
+
break;
|
|
1193
1648
|
case "context":
|
|
1194
1649
|
handleMap.set(`${SELF_MODULE}:Context:context`, h.handle);
|
|
1195
1650
|
break;
|
|
1196
1651
|
case "const":
|
|
1197
1652
|
handleMap.set(`${SELF_MODULE}:Const:const`, h.handle);
|
|
1198
1653
|
break;
|
|
1654
|
+
case "define":
|
|
1655
|
+
handleMap.set(`__define_${h.handle}:${bridge.type}:${bridge.field}`, h.handle);
|
|
1656
|
+
break;
|
|
1199
1657
|
}
|
|
1200
1658
|
}
|
|
1201
|
-
return { handleMap, inputHandle };
|
|
1659
|
+
return { handleMap, inputHandle, outputHandle };
|
|
1202
1660
|
}
|
|
1203
|
-
function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
1661
|
+
function serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom) {
|
|
1204
1662
|
if (ref.element) {
|
|
1205
|
-
|
|
1663
|
+
// Element refs are only serialized inside brace blocks (using the iterator name).
|
|
1664
|
+
// This path should not be reached in normal serialization.
|
|
1665
|
+
return "item." + serPath(ref.path);
|
|
1206
1666
|
}
|
|
1207
1667
|
// Bridge's own trunk (no instance, no element)
|
|
1208
1668
|
const isBridgeTrunk = ref.module === SELF_MODULE &&
|
|
@@ -1213,16 +1673,17 @@ function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
|
1213
1673
|
if (isBridgeTrunk) {
|
|
1214
1674
|
if (isFrom && inputHandle) {
|
|
1215
1675
|
// From side: use input handle (data comes from args)
|
|
1216
|
-
return
|
|
1676
|
+
return ref.path.length > 0
|
|
1677
|
+
? inputHandle + "." + serPath(ref.path)
|
|
1678
|
+
: inputHandle;
|
|
1217
1679
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
return bridge.field + "." + serPath(ref.path);
|
|
1680
|
+
if (!isFrom && outputHandle) {
|
|
1681
|
+
// To side: use output handle
|
|
1682
|
+
return ref.path.length > 0
|
|
1683
|
+
? outputHandle + "." + serPath(ref.path)
|
|
1684
|
+
: outputHandle;
|
|
1224
1685
|
}
|
|
1225
|
-
//
|
|
1686
|
+
// Fallback (no handle declared — legacy/serializer-only path)
|
|
1226
1687
|
return serPath(ref.path);
|
|
1227
1688
|
}
|
|
1228
1689
|
// Lookup by trunk key
|
|
@@ -1231,7 +1692,6 @@ function serializeRef(ref, bridge, handleMap, inputHandle, isFrom) {
|
|
|
1231
1692
|
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
1232
1693
|
const handle = handleMap.get(trunkStr);
|
|
1233
1694
|
if (handle) {
|
|
1234
|
-
// Empty path — just the handle name (e.g. pipe result = tool root)
|
|
1235
1695
|
if (ref.path.length === 0)
|
|
1236
1696
|
return handle;
|
|
1237
1697
|
return handle + "." + serPath(ref.path);
|