@stackables/bridge 1.6.0 → 1.7.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 +8 -0
- package/build/ExecutionTree.d.ts.map +1 -1
- package/build/ExecutionTree.js +69 -1
- package/build/bridge-format.d.ts +3 -10
- package/build/bridge-format.d.ts.map +1 -1
- package/build/bridge-format.js +22 -1264
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/parser/index.d.ts +8 -0
- package/build/parser/index.d.ts.map +1 -0
- package/build/parser/index.js +7 -0
- package/build/parser/lexer.d.ts +45 -0
- package/build/parser/lexer.d.ts.map +1 -0
- package/build/parser/lexer.js +114 -0
- package/build/parser/parser.d.ts +3 -0
- package/build/parser/parser.d.ts.map +1 -0
- package/build/parser/parser.js +1536 -0
- package/build/utils.d.ts +9 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +23 -0
- package/package.json +2 -1
package/build/bridge-format.js
CHANGED
|
@@ -1,1270 +1,14 @@
|
|
|
1
1
|
import { SELF_MODULE } from "./types.js";
|
|
2
|
-
|
|
2
|
+
import { parseBridgeChevrotain } from "./parser/index.js";
|
|
3
|
+
export { parsePath } from "./utils.js";
|
|
3
4
|
/**
|
|
4
|
-
* Parse .bridge text
|
|
5
|
-
*
|
|
6
|
-
* The .bridge format is a human-readable representation of connection wires.
|
|
7
|
-
* Multiple blocks are separated by `---`.
|
|
8
|
-
* Tool blocks define API tools, bridge blocks define wire mappings.
|
|
9
|
-
*
|
|
10
|
-
* @param text - Bridge definition text
|
|
11
|
-
* @returns Array of instructions (Bridge, ToolDef)
|
|
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).
|
|
5
|
+
* Parse .bridge text — delegates to the Chevrotain parser.
|
|
31
6
|
*/
|
|
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
|
-
}
|
|
42
7
|
export function parseBridge(text) {
|
|
43
|
-
|
|
44
|
-
const normalized = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ");
|
|
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] = "";
|
|
57
|
-
// Find separator lines (--- with optional surrounding whitespace)
|
|
58
|
-
const isSep = (line) => /^\s*---\s*$/.test(line);
|
|
59
|
-
// Collect block ranges as [start, end) line indices
|
|
60
|
-
const blockRanges = [];
|
|
61
|
-
let blockStart = 0;
|
|
62
|
-
for (let i = 0; i < allLines.length; i++) {
|
|
63
|
-
if (isSep(allLines[i])) {
|
|
64
|
-
blockRanges.push({ start: blockStart, end: i });
|
|
65
|
-
blockStart = i + 1;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
blockRanges.push({ start: blockStart, end: allLines.length });
|
|
69
|
-
const instructions = [];
|
|
70
|
-
for (const { start, end } of blockRanges) {
|
|
71
|
-
const blockLines = allLines.slice(start, end);
|
|
72
|
-
// Split into sub-blocks by top-level `tool` or `bridge` keywords
|
|
73
|
-
const subBlocks = [];
|
|
74
|
-
let currentLines = [];
|
|
75
|
-
let currentOffset = start;
|
|
76
|
-
for (let i = 0; i < blockLines.length; i++) {
|
|
77
|
-
const trimmed = blockLines[i].trim();
|
|
78
|
-
if (/^(tool|bridge|const|define)\s/i.test(trimmed) && currentLines.length > 0) {
|
|
79
|
-
// Check if any non-blank content exists
|
|
80
|
-
if (currentLines.some((l) => l.trim())) {
|
|
81
|
-
subBlocks.push({ startOffset: currentOffset, lines: currentLines });
|
|
82
|
-
}
|
|
83
|
-
currentLines = [blockLines[i]];
|
|
84
|
-
currentOffset = start + i;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
currentLines.push(blockLines[i]);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
if (currentLines.some((l) => l.trim())) {
|
|
91
|
-
subBlocks.push({ startOffset: currentOffset, lines: currentLines });
|
|
92
|
-
}
|
|
93
|
-
for (const sub of subBlocks) {
|
|
94
|
-
const subText = sub.lines.join("\n").trim();
|
|
95
|
-
if (!subText)
|
|
96
|
-
continue;
|
|
97
|
-
let firstContentLine = 0;
|
|
98
|
-
while (firstContentLine < sub.lines.length && !sub.lines[firstContentLine].trim())
|
|
99
|
-
firstContentLine++;
|
|
100
|
-
const firstLine = sub.lines[firstContentLine]?.trim();
|
|
101
|
-
if (firstLine && /^tool\s/i.test(firstLine)) {
|
|
102
|
-
instructions.push(parseToolBlock(subText, sub.startOffset + firstContentLine, instructions));
|
|
103
|
-
}
|
|
104
|
-
else if (firstLine && /^define\s/i.test(firstLine)) {
|
|
105
|
-
instructions.push(parseDefineBlock(subText, sub.startOffset + firstContentLine));
|
|
106
|
-
}
|
|
107
|
-
else if (firstLine && /^bridge\s/i.test(firstLine)) {
|
|
108
|
-
instructions.push(...parseBridgeBlock(subText, sub.startOffset + firstContentLine, instructions));
|
|
109
|
-
}
|
|
110
|
-
else if (firstLine && /^const\s/i.test(firstLine)) {
|
|
111
|
-
instructions.push(...parseConstLines(subText, sub.startOffset + firstContentLine));
|
|
112
|
-
}
|
|
113
|
-
else if (firstLine && !firstLine.startsWith("#")) {
|
|
114
|
-
throw new Error(`Line ${sub.startOffset + firstContentLine + 1}: Expected "tool", "define", "bridge", or "const" declaration, got: ${firstLine}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return instructions;
|
|
119
|
-
}
|
|
120
|
-
// ── Bridge block parser ─────────────────────────────────────────────────────
|
|
121
|
-
/**
|
|
122
|
-
* Returns true when the token looks like a JSON literal rather than a
|
|
123
|
-
* source reference (dotted path or pipe chain).
|
|
124
|
-
* JSON start chars: `"`, `{`, `[`, digit, `-` + digit, `true`, `false`, `null`.
|
|
125
|
-
*/
|
|
126
|
-
function isJsonLiteral(s) {
|
|
127
|
-
return /^["\{\[\d]/.test(s) || /^-\d/.test(s) ||
|
|
128
|
-
s === "true" || s === "false" || s === "null";
|
|
129
|
-
}
|
|
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
|
-
}
|
|
154
|
-
// Validate mandatory braces: `bridge Foo.bar {` ... `}`
|
|
155
|
-
const rawLines = block.split("\n");
|
|
156
|
-
const keywordIdx = rawLines.findIndex((l) => /^bridge\s/i.test(l.trim()));
|
|
157
|
-
if (keywordIdx !== -1) {
|
|
158
|
-
const kw = rawLines[keywordIdx].trim();
|
|
159
|
-
if (!kw.endsWith("{")) {
|
|
160
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}: bridge block must use braces: bridge Type.field {`);
|
|
161
|
-
}
|
|
162
|
-
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
163
|
-
if (!hasClose) {
|
|
164
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}: bridge block missing closing }`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// Strip braces for internal parsing
|
|
168
|
-
const lines = rawLines.map((l) => {
|
|
169
|
-
const trimmed = l.trimEnd();
|
|
170
|
-
if (trimmed === "}")
|
|
171
|
-
return "";
|
|
172
|
-
if (/^bridge\s/i.test(trimmed) && trimmed.endsWith("{"))
|
|
173
|
-
return trimmed.replace(/\s*\{\s*$/, "");
|
|
174
|
-
return trimmed;
|
|
175
|
-
});
|
|
176
|
-
const instructions = [];
|
|
177
|
-
/** 1-based global line number for error messages */
|
|
178
|
-
const ln = (i) => lineOffset + i + 1;
|
|
179
|
-
// ── Parse header ────────────────────────────────────────────────────
|
|
180
|
-
let bridgeType = "";
|
|
181
|
-
let bridgeField = "";
|
|
182
|
-
const handleRes = new Map();
|
|
183
|
-
const handleBindings = [];
|
|
184
|
-
const instanceCounters = new Map();
|
|
185
|
-
let bodyStartIndex = 0;
|
|
186
|
-
for (let i = 0; i < lines.length; i++) {
|
|
187
|
-
const line = lines[i].trim();
|
|
188
|
-
if (!line || line.startsWith("#")) {
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
if (/^bridge\s/i.test(line)) {
|
|
192
|
-
const match = line.match(/^bridge\s+(\w+)\.(\w+)$/i);
|
|
193
|
-
if (!match)
|
|
194
|
-
throw new Error(`Line ${ln(i)}: Invalid bridge declaration: ${line}`);
|
|
195
|
-
bridgeType = match[1];
|
|
196
|
-
bridgeField = match[2];
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
if (/^with\s/i.test(line)) {
|
|
200
|
-
if (!bridgeType) {
|
|
201
|
-
throw new Error(`Line ${ln(i)}: "with" declaration must come after "bridge" declaration`);
|
|
202
|
-
}
|
|
203
|
-
parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters, previousInstructions ?? [], ln(i));
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
// First non-header line — body starts here
|
|
207
|
-
bodyStartIndex = i;
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
if (!bridgeType || !bridgeField) {
|
|
211
|
-
throw new Error(`Line ${ln(0)}: Missing bridge declaration`);
|
|
212
|
-
}
|
|
213
|
-
// ── Parse wire lines ────────────────────────────────────────────────
|
|
214
|
-
const wires = [];
|
|
215
|
-
let currentArrayToPath = null;
|
|
216
|
-
let currentIterHandle = null;
|
|
217
|
-
const arrayIterators = {};
|
|
218
|
-
/** Monotonically-increasing index; combined with a high base to produce
|
|
219
|
-
* fork instances that can never collide with regular handle instances. */
|
|
220
|
-
let nextForkSeq = 0;
|
|
221
|
-
const pipeHandleEntries = [];
|
|
222
|
-
/**
|
|
223
|
-
* Parse a source expression (`handle.path` or `h1:h2:source`) into bridge
|
|
224
|
-
* wires, returning the terminal NodeRef.
|
|
225
|
-
*
|
|
226
|
-
* For pipe chains: pushes the intermediate `.in <- prev` wires and registers
|
|
227
|
-
* the fork instances, then returns the fork-root ref. The caller is
|
|
228
|
-
* responsible for pushing the TERMINAL wire (forkRoot → target).
|
|
229
|
-
*
|
|
230
|
-
* For simple refs: returns the resolved NodeRef directly (no wires pushed).
|
|
231
|
-
*
|
|
232
|
-
* @param forceOnOutermost When true, marks the outermost intermediate pipe
|
|
233
|
-
* wire with `force: true` (used for `<-!`).
|
|
234
|
-
*/
|
|
235
|
-
function buildSourceExpr(sourceStr, lineNum, forceOnOutermost) {
|
|
236
|
-
const parts = sourceStr.split(":");
|
|
237
|
-
if (parts.length === 1) {
|
|
238
|
-
return resolveAddress(sourceStr, handleRes, bridgeType, bridgeField, lineNum);
|
|
239
|
-
}
|
|
240
|
-
// Pipe chain
|
|
241
|
-
const actualSource = parts[parts.length - 1];
|
|
242
|
-
const tokenChain = parts.slice(0, -1);
|
|
243
|
-
const parseToken = (t) => {
|
|
244
|
-
const dot = t.indexOf(".");
|
|
245
|
-
return dot === -1
|
|
246
|
-
? { handleName: t, fieldName: "in" }
|
|
247
|
-
: { handleName: t.substring(0, dot), fieldName: t.substring(dot + 1) };
|
|
248
|
-
};
|
|
249
|
-
for (const tok of tokenChain) {
|
|
250
|
-
const { handleName } = parseToken(tok);
|
|
251
|
-
if (!handleRes.has(handleName)) {
|
|
252
|
-
throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${handleName}". Add 'with <tool> as ${handleName}' to the bridge header.`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
let prevOutRef = resolveAddress(actualSource, handleRes, bridgeType, bridgeField, lineNum);
|
|
256
|
-
const reversedTokens = [...tokenChain].reverse();
|
|
257
|
-
for (let idx = 0; idx < reversedTokens.length; idx++) {
|
|
258
|
-
const tok = reversedTokens[idx];
|
|
259
|
-
const { handleName, fieldName } = parseToken(tok);
|
|
260
|
-
const res = handleRes.get(handleName);
|
|
261
|
-
const forkInstance = 100000 + nextForkSeq++;
|
|
262
|
-
const forkKey = `${res.module}:${res.type}:${res.field}:${forkInstance}`;
|
|
263
|
-
pipeHandleEntries.push({
|
|
264
|
-
key: forkKey,
|
|
265
|
-
handle: handleName,
|
|
266
|
-
baseTrunk: { module: res.module, type: res.type, field: res.field, instance: res.instance },
|
|
267
|
-
});
|
|
268
|
-
const forkInRef = { module: res.module, type: res.type, field: res.field, instance: forkInstance, path: parsePath(fieldName) };
|
|
269
|
-
const forkRootRef = { module: res.module, type: res.type, field: res.field, instance: forkInstance, path: [] };
|
|
270
|
-
const isOutermost = idx === reversedTokens.length - 1;
|
|
271
|
-
wires.push({ from: prevOutRef, to: forkInRef, pipe: true, ...(forceOnOutermost && isOutermost ? { force: true } : {}) });
|
|
272
|
-
prevOutRef = forkRootRef;
|
|
273
|
-
}
|
|
274
|
-
return prevOutRef; // fork-root ref
|
|
275
|
-
}
|
|
276
|
-
// ── Whether we are inside an element-mapping brace block
|
|
277
|
-
let inElementBlock = false;
|
|
278
|
-
for (let i = bodyStartIndex; i < lines.length; i++) {
|
|
279
|
-
const raw = lines[i];
|
|
280
|
-
const line = raw.trim();
|
|
281
|
-
if (!line || line.startsWith("#")) {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
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}`);
|
|
405
|
-
}
|
|
406
|
-
// Constant wire: target = "value" or target = value (unquoted)
|
|
407
|
-
const constantMatch = line.match(/^(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
408
|
-
if (constantMatch) {
|
|
409
|
-
const [, targetStr, quotedValue, unquotedValue] = constantMatch;
|
|
410
|
-
const value = quotedValue ?? unquotedValue;
|
|
411
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
412
|
-
assertNoTargetIndices(toRef, ln(i));
|
|
413
|
-
wires.push({ value, to: toRef });
|
|
414
|
-
continue;
|
|
415
|
-
}
|
|
416
|
-
// ── Wire: target <- A [|| B [|| C]] [|| "nullLiteral"] [?? errorSrc|"errorLiteral"]
|
|
417
|
-
const arrowMatch = line.match(/^(\S+)\s*<-(!?)\s*(.+)$/);
|
|
418
|
-
if (arrowMatch) {
|
|
419
|
-
const [, targetStr, forceFlag, rhs] = arrowMatch;
|
|
420
|
-
const force = forceFlag === "!";
|
|
421
|
-
const rhsTrimmed = rhs.trim();
|
|
422
|
-
// ── Array mapping: target <- source[] as <iter> {
|
|
423
|
-
// Opens a brace-delimited element block.
|
|
424
|
-
const arrayBraceMatch = rhsTrimmed.match(/^(\S+)\[\]\s+as\s+(\w+)\s*\{\s*$/);
|
|
425
|
-
if (arrayBraceMatch) {
|
|
426
|
-
const fromClean = arrayBraceMatch[1];
|
|
427
|
-
const iterHandle = arrayBraceMatch[2];
|
|
428
|
-
assertNotReserved(iterHandle, ln(i), "iterator handle");
|
|
429
|
-
const fromRef = resolveAddress(fromClean, handleRes, bridgeType, bridgeField, ln(i));
|
|
430
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
431
|
-
assertNoTargetIndices(toRef, ln(i));
|
|
432
|
-
wires.push({ from: fromRef, to: toRef });
|
|
433
|
-
currentArrayToPath = toRef.path;
|
|
434
|
-
currentIterHandle = iterHandle;
|
|
435
|
-
arrayIterators[toRef.path[0]] = iterHandle;
|
|
436
|
-
inElementBlock = true;
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
// ── Strip the ?? tail (last " ?? " wins in case source contains " ?? ")
|
|
440
|
-
let exprCore = rhsTrimmed;
|
|
441
|
-
let fallback;
|
|
442
|
-
let fallbackRefStr;
|
|
443
|
-
const qqIdx = rhsTrimmed.lastIndexOf(" ?? ");
|
|
444
|
-
if (qqIdx !== -1) {
|
|
445
|
-
exprCore = rhsTrimmed.slice(0, qqIdx).trim();
|
|
446
|
-
const tail = rhsTrimmed.slice(qqIdx + 4).trim();
|
|
447
|
-
if (isJsonLiteral(tail)) {
|
|
448
|
-
fallback = tail;
|
|
449
|
-
}
|
|
450
|
-
else {
|
|
451
|
-
fallbackRefStr = tail;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
// ── Split on " || " to get coalesce chain parts
|
|
455
|
-
const orParts = exprCore.split(" || ").map((s) => s.trim());
|
|
456
|
-
// Last part may be a JSON literal → becomes nullFallback on the last source wire
|
|
457
|
-
let nullFallback;
|
|
458
|
-
let sourceParts = orParts;
|
|
459
|
-
if (orParts.length > 1 && isJsonLiteral(orParts[orParts.length - 1])) {
|
|
460
|
-
nullFallback = orParts[orParts.length - 1];
|
|
461
|
-
sourceParts = orParts.slice(0, -1);
|
|
462
|
-
}
|
|
463
|
-
if (sourceParts.length === 0) {
|
|
464
|
-
throw new Error(`Line ${ln(i)}: Wire has no source expression: ${line}`);
|
|
465
|
-
}
|
|
466
|
-
let fallbackRef;
|
|
467
|
-
let fallbackInternalWires = [];
|
|
468
|
-
if (fallbackRefStr) {
|
|
469
|
-
const preLen = wires.length;
|
|
470
|
-
fallbackRef = buildSourceExpr(fallbackRefStr, ln(i), false);
|
|
471
|
-
fallbackInternalWires = wires.splice(preLen);
|
|
472
|
-
}
|
|
473
|
-
const toRef = resolveAddress(targetStr, handleRes, bridgeType, bridgeField, ln(i));
|
|
474
|
-
assertNoTargetIndices(toRef, ln(i));
|
|
475
|
-
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
476
|
-
const isFirst = ci === 0;
|
|
477
|
-
const isLast = ci === sourceParts.length - 1;
|
|
478
|
-
const srcStr = sourceParts[ci];
|
|
479
|
-
const termRef = buildSourceExpr(srcStr, ln(i), force && isFirst);
|
|
480
|
-
const isPipeFork = termRef.instance != null && termRef.path.length === 0
|
|
481
|
-
&& srcStr.includes(":");
|
|
482
|
-
const lastAttrs = isLast ? {
|
|
483
|
-
...(nullFallback ? { nullFallback } : {}),
|
|
484
|
-
...(fallback ? { fallback } : {}),
|
|
485
|
-
...(fallbackRef ? { fallbackRef } : {}),
|
|
486
|
-
} : {};
|
|
487
|
-
if (isPipeFork) {
|
|
488
|
-
wires.push({ from: termRef, to: toRef, pipe: true, ...lastAttrs });
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
wires.push({
|
|
492
|
-
from: termRef,
|
|
493
|
-
to: toRef,
|
|
494
|
-
...(force && isFirst ? { force: true } : {}),
|
|
495
|
-
...lastAttrs,
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
wires.push(...fallbackInternalWires);
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
throw new Error(`Line ${ln(i)}: Unrecognized line: ${line}`);
|
|
503
|
-
}
|
|
504
|
-
// ── Inline define invocations ───────────────────────────────────────
|
|
505
|
-
const nextForkSeqRef = { value: nextForkSeq };
|
|
506
|
-
for (const hb of handleBindings) {
|
|
507
|
-
if (hb.kind !== "define")
|
|
508
|
-
continue;
|
|
509
|
-
const def = previousInstructions?.find((inst) => inst.kind === "define" && inst.name === hb.name);
|
|
510
|
-
if (!def) {
|
|
511
|
-
throw new Error(`Define "${hb.name}" referenced by handle "${hb.handle}" not found`);
|
|
512
|
-
}
|
|
513
|
-
inlineDefine(hb.handle, def, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef);
|
|
514
|
-
}
|
|
515
|
-
instructions.unshift({
|
|
516
|
-
kind: "bridge",
|
|
517
|
-
type: bridgeType,
|
|
518
|
-
field: bridgeField,
|
|
519
|
-
handles: handleBindings,
|
|
520
|
-
wires,
|
|
521
|
-
arrayIterators: Object.keys(arrayIterators).length > 0 ? arrayIterators : undefined,
|
|
522
|
-
pipeHandles: pipeHandleEntries.length > 0 ? pipeHandleEntries : undefined,
|
|
523
|
-
});
|
|
524
|
-
return instructions;
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Parse a `with` declaration into handle bindings + resolution map.
|
|
528
|
-
*
|
|
529
|
-
* Supported forms:
|
|
530
|
-
* with <name> as <handle> — tool reference (dotted or simple name)
|
|
531
|
-
* with <name> — shorthand: handle defaults to last segment of name
|
|
532
|
-
* with input as <handle>
|
|
533
|
-
* with context as <handle>
|
|
534
|
-
* with context — shorthand for `with context as context`
|
|
535
|
-
*/
|
|
536
|
-
function parseWithDeclaration(line, bridgeType, bridgeField, handleRes, handleBindings, instanceCounters, instructions, lineNum) {
|
|
537
|
-
/** Guard: reject duplicate handle names */
|
|
538
|
-
const checkDuplicate = (handle) => {
|
|
539
|
-
if (handleRes.has(handle)) {
|
|
540
|
-
throw new Error(`Line ${lineNum}: Duplicate handle name "${handle}"`);
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
// with input as <handle>
|
|
544
|
-
let match = line.match(/^with\s+input\s+as\s+(\w+)$/i);
|
|
545
|
-
if (match) {
|
|
546
|
-
const handle = match[1];
|
|
547
|
-
checkDuplicate(handle);
|
|
548
|
-
handleBindings.push({ handle, kind: "input" });
|
|
549
|
-
handleRes.set(handle, {
|
|
550
|
-
module: SELF_MODULE,
|
|
551
|
-
type: bridgeType,
|
|
552
|
-
field: bridgeField,
|
|
553
|
-
});
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
// with input (shorthand — handle defaults to "input")
|
|
557
|
-
match = line.match(/^with\s+input$/i);
|
|
558
|
-
if (match) {
|
|
559
|
-
const handle = "input";
|
|
560
|
-
checkDuplicate(handle);
|
|
561
|
-
handleBindings.push({ handle, kind: "input" });
|
|
562
|
-
handleRes.set(handle, {
|
|
563
|
-
module: SELF_MODULE,
|
|
564
|
-
type: bridgeType,
|
|
565
|
-
field: bridgeField,
|
|
566
|
-
});
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
// with output as <handle>
|
|
570
|
-
match = line.match(/^with\s+output\s+as\s+(\w+)$/i);
|
|
571
|
-
if (match) {
|
|
572
|
-
const handle = match[1];
|
|
573
|
-
checkDuplicate(handle);
|
|
574
|
-
handleBindings.push({ handle, kind: "output" });
|
|
575
|
-
handleRes.set(handle, {
|
|
576
|
-
module: SELF_MODULE,
|
|
577
|
-
type: bridgeType,
|
|
578
|
-
field: bridgeField,
|
|
579
|
-
});
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
// with output (shorthand — handle defaults to "output")
|
|
583
|
-
match = line.match(/^with\s+output$/i);
|
|
584
|
-
if (match) {
|
|
585
|
-
const handle = "output";
|
|
586
|
-
checkDuplicate(handle);
|
|
587
|
-
handleBindings.push({ handle, kind: "output" });
|
|
588
|
-
handleRes.set(handle, {
|
|
589
|
-
module: SELF_MODULE,
|
|
590
|
-
type: bridgeType,
|
|
591
|
-
field: bridgeField,
|
|
592
|
-
});
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
// with context as <handle>
|
|
596
|
-
match = line.match(/^with\s+context\s+as\s+(\w+)$/i);
|
|
597
|
-
if (match) {
|
|
598
|
-
const handle = match[1];
|
|
599
|
-
checkDuplicate(handle);
|
|
600
|
-
handleBindings.push({ handle, kind: "context" });
|
|
601
|
-
handleRes.set(handle, {
|
|
602
|
-
module: SELF_MODULE,
|
|
603
|
-
type: "Context",
|
|
604
|
-
field: "context",
|
|
605
|
-
});
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
// with context (shorthand — handle defaults to "context")
|
|
609
|
-
match = line.match(/^with\s+context$/i);
|
|
610
|
-
if (match) {
|
|
611
|
-
const handle = "context";
|
|
612
|
-
checkDuplicate(handle);
|
|
613
|
-
handleBindings.push({ handle, kind: "context" });
|
|
614
|
-
handleRes.set(handle, {
|
|
615
|
-
module: SELF_MODULE,
|
|
616
|
-
type: "Context",
|
|
617
|
-
field: "context",
|
|
618
|
-
});
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
// with const as <handle>
|
|
622
|
-
match = line.match(/^with\s+const\s+as\s+(\w+)$/i);
|
|
623
|
-
if (match) {
|
|
624
|
-
const handle = match[1];
|
|
625
|
-
checkDuplicate(handle);
|
|
626
|
-
handleBindings.push({ handle, kind: "const" });
|
|
627
|
-
handleRes.set(handle, {
|
|
628
|
-
module: SELF_MODULE,
|
|
629
|
-
type: "Const",
|
|
630
|
-
field: "const",
|
|
631
|
-
});
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
// with const (shorthand — handle defaults to "const")
|
|
635
|
-
match = line.match(/^with\s+const$/i);
|
|
636
|
-
if (match) {
|
|
637
|
-
const handle = "const";
|
|
638
|
-
checkDuplicate(handle);
|
|
639
|
-
handleBindings.push({ handle, kind: "const" });
|
|
640
|
-
handleRes.set(handle, {
|
|
641
|
-
module: SELF_MODULE,
|
|
642
|
-
type: "Const",
|
|
643
|
-
field: "const",
|
|
644
|
-
});
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
// with <name> as <handle> — check for define invocation first
|
|
648
|
-
match = line.match(/^with\s+(\S+)\s+as\s+(\w+)$/i);
|
|
649
|
-
if (match) {
|
|
650
|
-
const name = match[1];
|
|
651
|
-
const handle = match[2];
|
|
652
|
-
checkDuplicate(handle);
|
|
653
|
-
assertNotReserved(handle, lineNum, "handle alias");
|
|
654
|
-
// Check if name matches a known define
|
|
655
|
-
const defineDef = instructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
656
|
-
if (defineDef) {
|
|
657
|
-
handleBindings.push({ handle, kind: "define", name });
|
|
658
|
-
handleRes.set(handle, {
|
|
659
|
-
module: `__define_${handle}`,
|
|
660
|
-
type: bridgeType,
|
|
661
|
-
field: bridgeField,
|
|
662
|
-
});
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const lastDot = name.lastIndexOf(".");
|
|
666
|
-
if (lastDot !== -1) {
|
|
667
|
-
const modulePart = name.substring(0, lastDot);
|
|
668
|
-
const fieldPart = name.substring(lastDot + 1);
|
|
669
|
-
const key = `${modulePart}:${fieldPart}`;
|
|
670
|
-
const instance = (instanceCounters.get(key) ?? 0) + 1;
|
|
671
|
-
instanceCounters.set(key, instance);
|
|
672
|
-
handleBindings.push({ handle, kind: "tool", name });
|
|
673
|
-
handleRes.set(handle, {
|
|
674
|
-
module: modulePart,
|
|
675
|
-
type: bridgeType,
|
|
676
|
-
field: fieldPart,
|
|
677
|
-
instance,
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
else {
|
|
681
|
-
// Simple name — inline tool function
|
|
682
|
-
const key = `Tools:${name}`;
|
|
683
|
-
const instance = (instanceCounters.get(key) ?? 0) + 1;
|
|
684
|
-
instanceCounters.set(key, instance);
|
|
685
|
-
handleBindings.push({ handle, kind: "tool", name });
|
|
686
|
-
handleRes.set(handle, {
|
|
687
|
-
module: SELF_MODULE,
|
|
688
|
-
type: "Tools",
|
|
689
|
-
field: name,
|
|
690
|
-
instance,
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
// with <name> — shorthand: handle defaults to the last segment of name
|
|
696
|
-
// Must come after the `with input` / `with context` guards above.
|
|
697
|
-
match = line.match(/^with\s+(\S+)$/i);
|
|
698
|
-
if (match) {
|
|
699
|
-
const name = match[1];
|
|
700
|
-
const lastDot = name.lastIndexOf(".");
|
|
701
|
-
const handle = lastDot !== -1 ? name.substring(lastDot + 1) : name;
|
|
702
|
-
checkDuplicate(handle);
|
|
703
|
-
// Check if name matches a known define
|
|
704
|
-
const defineDef = instructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
705
|
-
if (defineDef) {
|
|
706
|
-
handleBindings.push({ handle, kind: "define", name });
|
|
707
|
-
handleRes.set(handle, {
|
|
708
|
-
module: `__define_${handle}`,
|
|
709
|
-
type: bridgeType,
|
|
710
|
-
field: bridgeField,
|
|
711
|
-
});
|
|
712
|
-
return;
|
|
713
|
-
}
|
|
714
|
-
if (lastDot !== -1) {
|
|
715
|
-
const modulePart = name.substring(0, lastDot);
|
|
716
|
-
const fieldPart = name.substring(lastDot + 1);
|
|
717
|
-
const key = `${modulePart}:${fieldPart}`;
|
|
718
|
-
const instance = (instanceCounters.get(key) ?? 0) + 1;
|
|
719
|
-
instanceCounters.set(key, instance);
|
|
720
|
-
handleBindings.push({ handle, kind: "tool", name });
|
|
721
|
-
handleRes.set(handle, { module: modulePart, type: bridgeType, field: fieldPart, instance });
|
|
722
|
-
}
|
|
723
|
-
else {
|
|
724
|
-
const key = `Tools:${name}`;
|
|
725
|
-
const instance = (instanceCounters.get(key) ?? 0) + 1;
|
|
726
|
-
instanceCounters.set(key, instance);
|
|
727
|
-
handleBindings.push({ handle, kind: "tool", name });
|
|
728
|
-
handleRes.set(handle, { module: SELF_MODULE, type: "Tools", field: name, instance });
|
|
729
|
-
}
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
throw new Error(`Line ${lineNum}: Invalid with declaration: ${line}`);
|
|
733
|
-
}
|
|
734
|
-
// ── Define inlining ─────────────────────────────────────────────────────────
|
|
735
|
-
/**
|
|
736
|
-
* Inline a define invocation into a bridge's wires.
|
|
737
|
-
*
|
|
738
|
-
* Splits the define handle into separate input/output synthetic trunks,
|
|
739
|
-
* clones the define's internal wires with remapped references, and adds
|
|
740
|
-
* them to the bridge. Tool instances are remapped to avoid collisions.
|
|
741
|
-
*
|
|
742
|
-
* The executor treats synthetic trunks (module starting with `__define_`)
|
|
743
|
-
* as pass-through data containers — no tool function is called.
|
|
744
|
-
*/
|
|
745
|
-
function inlineDefine(defineHandle, defineDef, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef) {
|
|
746
|
-
const genericModule = `__define_${defineHandle}`;
|
|
747
|
-
const inModule = `__define_in_${defineHandle}`;
|
|
748
|
-
const outModule = `__define_out_${defineHandle}`;
|
|
749
|
-
// The define was parsed as synthetic `bridge Define.<name>`, so its
|
|
750
|
-
// internal refs use type="Define", field=defineName for I/O, and
|
|
751
|
-
// standard tool resolutions for tools.
|
|
752
|
-
const defType = "Define";
|
|
753
|
-
const defField = defineDef.name;
|
|
754
|
-
// ── 1. Build trunk remapping for define's tool handles ──────────────
|
|
755
|
-
// Replay define's instance counter to determine original instances
|
|
756
|
-
const defCounters = new Map();
|
|
757
|
-
const trunkRemap = new Map();
|
|
758
|
-
for (const hb of defineDef.handles) {
|
|
759
|
-
if (hb.kind === "input" || hb.kind === "output" || hb.kind === "context" || hb.kind === "const")
|
|
760
|
-
continue;
|
|
761
|
-
if (hb.kind === "define")
|
|
762
|
-
continue; // nested defines — future
|
|
763
|
-
const name = hb.kind === "tool" ? hb.name : "";
|
|
764
|
-
if (!name)
|
|
765
|
-
continue;
|
|
766
|
-
const lastDot = name.lastIndexOf(".");
|
|
767
|
-
let oldModule, oldType, oldField, instanceKey, bridgeKey;
|
|
768
|
-
if (lastDot !== -1) {
|
|
769
|
-
oldModule = name.substring(0, lastDot);
|
|
770
|
-
oldType = defType;
|
|
771
|
-
oldField = name.substring(lastDot + 1);
|
|
772
|
-
instanceKey = `${oldModule}:${oldField}`;
|
|
773
|
-
bridgeKey = instanceKey;
|
|
774
|
-
}
|
|
775
|
-
else {
|
|
776
|
-
oldModule = SELF_MODULE;
|
|
777
|
-
oldType = "Tools";
|
|
778
|
-
oldField = name;
|
|
779
|
-
instanceKey = `Tools:${name}`;
|
|
780
|
-
bridgeKey = instanceKey;
|
|
781
|
-
}
|
|
782
|
-
// Old instance (from define's isolated counter)
|
|
783
|
-
const oldInstance = (defCounters.get(instanceKey) ?? 0) + 1;
|
|
784
|
-
defCounters.set(instanceKey, oldInstance);
|
|
785
|
-
// New instance (from bridge's counter)
|
|
786
|
-
const newInstance = (instanceCounters.get(bridgeKey) ?? 0) + 1;
|
|
787
|
-
instanceCounters.set(bridgeKey, newInstance);
|
|
788
|
-
const oldKey = `${oldModule}:${oldType}:${oldField}:${oldInstance}`;
|
|
789
|
-
trunkRemap.set(oldKey, { module: oldModule, type: oldType, field: oldField, instance: newInstance });
|
|
790
|
-
// Add internal tool handle to bridge's handle bindings (namespaced)
|
|
791
|
-
handleBindings.push({ handle: `${defineHandle}$${hb.handle}`, kind: "tool", name });
|
|
792
|
-
}
|
|
793
|
-
// ── 2. Remap bridge wires involving the define handle ───────────────
|
|
794
|
-
for (const wire of wires) {
|
|
795
|
-
if ("from" in wire) {
|
|
796
|
-
if (wire.to.module === genericModule) {
|
|
797
|
-
wire.to = { ...wire.to, module: inModule };
|
|
798
|
-
}
|
|
799
|
-
if (wire.from.module === genericModule) {
|
|
800
|
-
wire.from = { ...wire.from, module: outModule };
|
|
801
|
-
}
|
|
802
|
-
if (wire.fallbackRef?.module === genericModule) {
|
|
803
|
-
wire.fallbackRef = { ...wire.fallbackRef, module: outModule };
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
if ("value" in wire && wire.to.module === genericModule) {
|
|
807
|
-
wire.to = { ...wire.to, module: inModule };
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
// ── 3. Clone, remap, and add define's wires ────────────────────────
|
|
811
|
-
// Compute fork instance offset (define's fork instances start at 100000,
|
|
812
|
-
// bridge's may overlap — offset them to avoid collision)
|
|
813
|
-
const forkOffset = nextForkSeqRef.value;
|
|
814
|
-
let maxDefForkSeq = 0;
|
|
815
|
-
function remapRef(ref, side) {
|
|
816
|
-
// Define I/O trunk → split into input/output synthetic trunks
|
|
817
|
-
if (ref.module === SELF_MODULE && ref.type === defType && ref.field === defField) {
|
|
818
|
-
const targetModule = side === "from" ? inModule : outModule;
|
|
819
|
-
return { ...ref, module: targetModule, type: bridgeType, field: bridgeField };
|
|
820
|
-
}
|
|
821
|
-
// Tool trunk → remap instance
|
|
822
|
-
const key = `${ref.module}:${ref.type}:${ref.field}:${ref.instance ?? ""}`;
|
|
823
|
-
const newTrunk = trunkRemap.get(key);
|
|
824
|
-
if (newTrunk) {
|
|
825
|
-
return { ...ref, module: newTrunk.module, type: newTrunk.type, field: newTrunk.field, instance: newTrunk.instance };
|
|
826
|
-
}
|
|
827
|
-
// Fork instance → offset (fork instances are >= 100000)
|
|
828
|
-
if (ref.instance != null && ref.instance >= 100000) {
|
|
829
|
-
const defSeq = ref.instance - 100000;
|
|
830
|
-
if (defSeq + 1 > maxDefForkSeq)
|
|
831
|
-
maxDefForkSeq = defSeq + 1;
|
|
832
|
-
return { ...ref, instance: ref.instance + forkOffset };
|
|
833
|
-
}
|
|
834
|
-
return ref;
|
|
835
|
-
}
|
|
836
|
-
for (const wire of defineDef.wires) {
|
|
837
|
-
const cloned = JSON.parse(JSON.stringify(wire));
|
|
838
|
-
if ("from" in cloned) {
|
|
839
|
-
cloned.from = remapRef(cloned.from, "from");
|
|
840
|
-
cloned.to = remapRef(cloned.to, "to");
|
|
841
|
-
if (cloned.fallbackRef) {
|
|
842
|
-
cloned.fallbackRef = remapRef(cloned.fallbackRef, "from");
|
|
843
|
-
}
|
|
844
|
-
}
|
|
845
|
-
else {
|
|
846
|
-
// Constant wire
|
|
847
|
-
cloned.to = remapRef(cloned.to, "to");
|
|
848
|
-
}
|
|
849
|
-
wires.push(cloned);
|
|
850
|
-
}
|
|
851
|
-
// Advance bridge's fork counter past define's forks
|
|
852
|
-
nextForkSeqRef.value += maxDefForkSeq;
|
|
853
|
-
// ── 4. Remap and merge pipe handles ─────────────────────────────────
|
|
854
|
-
if (defineDef.pipeHandles) {
|
|
855
|
-
for (const ph of defineDef.pipeHandles) {
|
|
856
|
-
const parts = ph.key.split(":");
|
|
857
|
-
// key format: "module:type:field:instance"
|
|
858
|
-
const phInstance = parseInt(parts[parts.length - 1]);
|
|
859
|
-
let newKey = ph.key;
|
|
860
|
-
if (phInstance >= 100000) {
|
|
861
|
-
const newInst = phInstance + forkOffset;
|
|
862
|
-
parts[parts.length - 1] = String(newInst);
|
|
863
|
-
newKey = parts.join(":");
|
|
864
|
-
}
|
|
865
|
-
// Remap baseTrunk
|
|
866
|
-
const bt = ph.baseTrunk;
|
|
867
|
-
const btKey = `${bt.module}:${defType}:${bt.field}:${bt.instance ?? ""}`;
|
|
868
|
-
const newBt = trunkRemap.get(btKey);
|
|
869
|
-
// Also try with Tools type for simple names
|
|
870
|
-
const btKey2 = `${bt.module}:Tools:${bt.field}:${bt.instance ?? ""}`;
|
|
871
|
-
const newBt2 = trunkRemap.get(btKey2);
|
|
872
|
-
const resolvedBt = newBt ?? newBt2;
|
|
873
|
-
pipeHandleEntries.push({
|
|
874
|
-
key: newKey,
|
|
875
|
-
handle: `${defineHandle}$${ph.handle}`,
|
|
876
|
-
baseTrunk: resolvedBt
|
|
877
|
-
? { module: resolvedBt.module, type: resolvedBt.type, field: resolvedBt.field, instance: resolvedBt.instance }
|
|
878
|
-
: ph.baseTrunk,
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
}
|
|
8
|
+
return parseBridgeChevrotain(text);
|
|
882
9
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
*
|
|
886
|
-
* Resolution rules:
|
|
887
|
-
* 1. No dot, but whole address is a declared handle → handle root (path: [])
|
|
888
|
-
* 2. No dot, not a handle → output field on the bridge trunk
|
|
889
|
-
* 3. Prefix matches a declared handle → resolve via handle binding
|
|
890
|
-
* 4. Otherwise → nested output path (e.g., topPick.address)
|
|
891
|
-
*/
|
|
892
|
-
function resolveAddress(address, handles, bridgeType, bridgeField, lineNum) {
|
|
893
|
-
const dotIndex = address.indexOf(".");
|
|
894
|
-
if (dotIndex === -1) {
|
|
895
|
-
// Whole address is a declared handle → resolve to its root (path: [])
|
|
896
|
-
const resolution = handles.get(address);
|
|
897
|
-
if (resolution) {
|
|
898
|
-
const ref = {
|
|
899
|
-
module: resolution.module,
|
|
900
|
-
type: resolution.type,
|
|
901
|
-
field: resolution.field,
|
|
902
|
-
path: [],
|
|
903
|
-
};
|
|
904
|
-
if (resolution.instance != null)
|
|
905
|
-
ref.instance = resolution.instance;
|
|
906
|
-
return ref;
|
|
907
|
-
}
|
|
908
|
-
// Strict scoping: every reference must go through a declared handle.
|
|
909
|
-
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared reference "${address}". ` +
|
|
910
|
-
`Add 'with output as o' for output fields, or 'with ${address}' for a tool.`);
|
|
911
|
-
}
|
|
912
|
-
const prefix = address.substring(0, dotIndex);
|
|
913
|
-
const rest = address.substring(dotIndex + 1);
|
|
914
|
-
const pathParts = parsePath(rest);
|
|
915
|
-
// Known handle
|
|
916
|
-
const resolution = handles.get(prefix);
|
|
917
|
-
if (resolution) {
|
|
918
|
-
const ref = {
|
|
919
|
-
module: resolution.module,
|
|
920
|
-
type: resolution.type,
|
|
921
|
-
field: resolution.field,
|
|
922
|
-
path: pathParts,
|
|
923
|
-
};
|
|
924
|
-
if (resolution.instance != null) {
|
|
925
|
-
ref.instance = resolution.instance;
|
|
926
|
-
}
|
|
927
|
-
return ref;
|
|
928
|
-
}
|
|
929
|
-
// Strict scoping: prefix must be a known handle.
|
|
930
|
-
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}Undeclared handle "${prefix}". ` +
|
|
931
|
-
`Add 'with ${prefix}' or 'with ${prefix} as ${prefix}' to the bridge header.`);
|
|
932
|
-
}
|
|
933
|
-
/** Reject explicit array indices on the target (LHS) of a wire. */
|
|
934
|
-
function assertNoTargetIndices(ref, lineNum) {
|
|
935
|
-
if (ref.path.some((seg) => /^\d+$/.test(seg))) {
|
|
936
|
-
throw new Error(`${lineNum != null ? `Line ${lineNum}: ` : ""}` +
|
|
937
|
-
`Explicit array index in wire target is not supported. ` +
|
|
938
|
-
`Use array mapping (\`[] as iter { }\`) instead.`);
|
|
939
|
-
}
|
|
940
|
-
}
|
|
941
|
-
// ── Const block parser ──────────────────────────────────────────────────────
|
|
942
|
-
/**
|
|
943
|
-
* Parse `const` declarations into ConstDef instructions.
|
|
944
|
-
*
|
|
945
|
-
* Supports single-line and multi-line JSON values:
|
|
946
|
-
* const fallbackGeo = { "lat": 0, "lon": 0 }
|
|
947
|
-
* const bigConfig = {
|
|
948
|
-
* "timeout": 5000,
|
|
949
|
-
* "retries": 3
|
|
950
|
-
* }
|
|
951
|
-
* const defaultCurrency = "EUR"
|
|
952
|
-
* const limit = 10
|
|
953
|
-
*/
|
|
954
|
-
function parseConstLines(block, lineOffset) {
|
|
955
|
-
const lines = block.split("\n");
|
|
956
|
-
const results = [];
|
|
957
|
-
const ln = (i) => lineOffset + i + 1;
|
|
958
|
-
let i = 0;
|
|
959
|
-
while (i < lines.length) {
|
|
960
|
-
const line = lines[i].trim();
|
|
961
|
-
if (!line || line.startsWith("#")) {
|
|
962
|
-
i++;
|
|
963
|
-
continue;
|
|
964
|
-
}
|
|
965
|
-
const constMatch = line.match(/^const\s+(\w+)\s*=\s*(.*)/i);
|
|
966
|
-
if (!constMatch) {
|
|
967
|
-
throw new Error(`Line ${ln(i)}: Expected const declaration, got: ${line}`);
|
|
968
|
-
}
|
|
969
|
-
const name = constMatch[1];
|
|
970
|
-
assertNotReserved(name, ln(i), "const name");
|
|
971
|
-
let valuePart = constMatch[2].trim();
|
|
972
|
-
// Multi-line: if value starts with { or [ and isn't balanced, read more lines
|
|
973
|
-
if (/^[{[]/.test(valuePart)) {
|
|
974
|
-
let depth = 0;
|
|
975
|
-
for (const ch of valuePart) {
|
|
976
|
-
if (ch === "{" || ch === "[")
|
|
977
|
-
depth++;
|
|
978
|
-
if (ch === "}" || ch === "]")
|
|
979
|
-
depth--;
|
|
980
|
-
}
|
|
981
|
-
while (depth > 0 && i + 1 < lines.length) {
|
|
982
|
-
i++;
|
|
983
|
-
const nextLine = lines[i];
|
|
984
|
-
valuePart += "\n" + nextLine;
|
|
985
|
-
for (const ch of nextLine) {
|
|
986
|
-
if (ch === "{" || ch === "[")
|
|
987
|
-
depth++;
|
|
988
|
-
if (ch === "}" || ch === "]")
|
|
989
|
-
depth--;
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
if (depth !== 0) {
|
|
993
|
-
throw new Error(`Line ${ln(i)}: Unbalanced brackets in const "${name}"`);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
// Validate the value is parseable JSON
|
|
997
|
-
const jsonValue = valuePart.trim();
|
|
998
|
-
try {
|
|
999
|
-
JSON.parse(jsonValue);
|
|
1000
|
-
}
|
|
1001
|
-
catch {
|
|
1002
|
-
throw new Error(`Line ${ln(i)}: Invalid JSON value for const "${name}": ${jsonValue}`);
|
|
1003
|
-
}
|
|
1004
|
-
results.push({ kind: "const", name, value: jsonValue });
|
|
1005
|
-
i++;
|
|
1006
|
-
}
|
|
1007
|
-
return results;
|
|
1008
|
-
}
|
|
1009
|
-
// ── Define block parser ─────────────────────────────────────────────────────
|
|
1010
|
-
/**
|
|
1011
|
-
* Parse a `define` block into a DefineDef instruction.
|
|
1012
|
-
*
|
|
1013
|
-
* Delegates to parseBridgeBlock with a synthetic `bridge Define.<name>` header,
|
|
1014
|
-
* then converts the resulting Bridge to a DefineDef template.
|
|
1015
|
-
*
|
|
1016
|
-
* Example:
|
|
1017
|
-
* define secureProfile {
|
|
1018
|
-
* with userApi as api
|
|
1019
|
-
* with input as i
|
|
1020
|
-
* with output as o
|
|
1021
|
-
* api.id <- i.userId
|
|
1022
|
-
* o.name <- api.login
|
|
1023
|
-
* }
|
|
1024
|
-
*/
|
|
1025
|
-
function parseDefineBlock(block, lineOffset) {
|
|
1026
|
-
const rawLines = block.split("\n");
|
|
1027
|
-
// Find the define header line
|
|
1028
|
-
let headerIdx = -1;
|
|
1029
|
-
let defineName = "";
|
|
1030
|
-
for (let i = 0; i < rawLines.length; i++) {
|
|
1031
|
-
const line = rawLines[i].trim();
|
|
1032
|
-
if (!line || line.startsWith("#"))
|
|
1033
|
-
continue;
|
|
1034
|
-
const m = line.match(/^define\s+(\w+)\s*\{?\s*$/i);
|
|
1035
|
-
if (!m) {
|
|
1036
|
-
throw new Error(`Line ${lineOffset + i + 1}: Expected define declaration: define <name> {. Got: ${line}`);
|
|
1037
|
-
}
|
|
1038
|
-
defineName = m[1];
|
|
1039
|
-
assertNotReserved(defineName, lineOffset + i + 1, "define name");
|
|
1040
|
-
headerIdx = i;
|
|
1041
|
-
break;
|
|
1042
|
-
}
|
|
1043
|
-
if (!defineName) {
|
|
1044
|
-
throw new Error(`Line ${lineOffset + 1}: Missing define declaration`);
|
|
1045
|
-
}
|
|
1046
|
-
// Validate braces
|
|
1047
|
-
const kw = rawLines[headerIdx].trim();
|
|
1048
|
-
if (!kw.endsWith("{")) {
|
|
1049
|
-
throw new Error(`Line ${lineOffset + headerIdx + 1}: define block must use braces: define ${defineName} {`);
|
|
1050
|
-
}
|
|
1051
|
-
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
1052
|
-
if (!hasClose) {
|
|
1053
|
-
throw new Error(`Line ${lineOffset + headerIdx + 1}: define block missing closing }`);
|
|
1054
|
-
}
|
|
1055
|
-
// Rewrite header to a synthetic bridge: `bridge Define.<name> {`
|
|
1056
|
-
const syntheticLines = [...rawLines];
|
|
1057
|
-
syntheticLines[headerIdx] = rawLines[headerIdx]
|
|
1058
|
-
.replace(/^(\s*)define\s+\w+/i, `$1bridge Define.${defineName}`);
|
|
1059
|
-
const syntheticBlock = syntheticLines.join("\n");
|
|
1060
|
-
const results = parseBridgeBlock(syntheticBlock, lineOffset);
|
|
1061
|
-
const bridge = results[0];
|
|
1062
|
-
return {
|
|
1063
|
-
kind: "define",
|
|
1064
|
-
name: defineName,
|
|
1065
|
-
handles: bridge.handles,
|
|
1066
|
-
wires: bridge.wires,
|
|
1067
|
-
...(bridge.arrayIterators ? { arrayIterators: bridge.arrayIterators } : {}),
|
|
1068
|
-
...(bridge.pipeHandles ? { pipeHandles: bridge.pipeHandles } : {}),
|
|
1069
|
-
};
|
|
1070
|
-
}
|
|
1071
|
-
// ── Tool block parser ───────────────────────────────────────────────────────
|
|
1072
|
-
/**
|
|
1073
|
-
* Parse a `tool` block into a ToolDef instruction.
|
|
1074
|
-
*
|
|
1075
|
-
* Format:
|
|
1076
|
-
* tool hereapi from httpCall {
|
|
1077
|
-
* with context
|
|
1078
|
-
* .baseUrl = "https://geocode.search.hereapi.com/v1"
|
|
1079
|
-
* .headers.apiKey <- context.hereapi.apiKey
|
|
1080
|
-
* }
|
|
1081
|
-
*
|
|
1082
|
-
* tool hereapi.geocode from hereapi {
|
|
1083
|
-
* .method = GET
|
|
1084
|
-
* .path = /geocode
|
|
1085
|
-
* }
|
|
1086
|
-
*
|
|
1087
|
-
* When the source matches a previously-defined tool name,
|
|
1088
|
-
* it's treated as inheritance (child inherits parent). Otherwise the source
|
|
1089
|
-
* is treated as a function name.
|
|
1090
|
-
*/
|
|
1091
|
-
function parseToolBlock(block, lineOffset, previousInstructions) {
|
|
1092
|
-
// Validate mandatory braces for blocks that have a body (deps / wires)
|
|
1093
|
-
const rawLines = block.split("\n");
|
|
1094
|
-
const keywordIdx = rawLines.findIndex((l) => /^tool\s/i.test(l.trim()));
|
|
1095
|
-
if (keywordIdx !== -1) {
|
|
1096
|
-
// Check if there are non-blank, non-comment body lines after the keyword
|
|
1097
|
-
const bodyLines = rawLines.slice(keywordIdx + 1).filter((l) => {
|
|
1098
|
-
const t = l.trim();
|
|
1099
|
-
return t !== "" && !t.startsWith("#") && t !== "}";
|
|
1100
|
-
});
|
|
1101
|
-
const kw = rawLines[keywordIdx].trim();
|
|
1102
|
-
if (bodyLines.length > 0) {
|
|
1103
|
-
if (!kw.endsWith("{")) {
|
|
1104
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block with body must use braces: tool foo from bar {`);
|
|
1105
|
-
}
|
|
1106
|
-
const hasClose = rawLines.some((l) => l.trimEnd() === "}");
|
|
1107
|
-
if (!hasClose) {
|
|
1108
|
-
throw new Error(`Line ${lineOffset + keywordIdx + 1}: tool block missing closing }`);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
// Strip braces for internal parsing
|
|
1113
|
-
const lines = rawLines.map((l) => {
|
|
1114
|
-
const trimmed = l.trimEnd();
|
|
1115
|
-
if (trimmed === "}")
|
|
1116
|
-
return "";
|
|
1117
|
-
if (/^tool\s/i.test(trimmed) && trimmed.endsWith("{"))
|
|
1118
|
-
return trimmed.replace(/\s*\{\s*$/, "");
|
|
1119
|
-
return trimmed;
|
|
1120
|
-
});
|
|
1121
|
-
/** 1-based global line number for error messages */
|
|
1122
|
-
const ln = (i) => lineOffset + i + 1;
|
|
1123
|
-
let toolName = "";
|
|
1124
|
-
let toolFn;
|
|
1125
|
-
let toolExtends;
|
|
1126
|
-
const deps = [];
|
|
1127
|
-
const wires = [];
|
|
1128
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1129
|
-
const raw = lines[i];
|
|
1130
|
-
const line = raw.trim();
|
|
1131
|
-
if (!line || line.startsWith("#"))
|
|
1132
|
-
continue;
|
|
1133
|
-
// Tool declaration: tool <name> from <source>
|
|
1134
|
-
if (/^tool\s/i.test(line)) {
|
|
1135
|
-
const toolMatch = line.match(/^tool\s+(\S+)\s+from\s+(\S+)$/i);
|
|
1136
|
-
if (!toolMatch) {
|
|
1137
|
-
throw new Error(`Line ${ln(i)}: Invalid tool declaration: ${line}. Expected: tool <name> from <source>`);
|
|
1138
|
-
}
|
|
1139
|
-
toolName = toolMatch[1];
|
|
1140
|
-
const source = toolMatch[2];
|
|
1141
|
-
assertNotReserved(toolName, ln(i), "tool name");
|
|
1142
|
-
// If source matches a previously-defined tool, it's inheritance; otherwise it's a function name
|
|
1143
|
-
const isKnownTool = previousInstructions?.some((inst) => inst.kind === "tool" && inst.name === source);
|
|
1144
|
-
if (isKnownTool) {
|
|
1145
|
-
toolExtends = source;
|
|
1146
|
-
}
|
|
1147
|
-
else {
|
|
1148
|
-
toolFn = source;
|
|
1149
|
-
}
|
|
1150
|
-
continue;
|
|
1151
|
-
}
|
|
1152
|
-
// with context or with context as <handle>
|
|
1153
|
-
const contextMatch = line.match(/^with\s+context(?:\s+as\s+(\w+))?$/i);
|
|
1154
|
-
if (contextMatch) {
|
|
1155
|
-
const handle = contextMatch[1] ?? "context";
|
|
1156
|
-
deps.push({ kind: "context", handle });
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
// with const or with const as <handle>
|
|
1160
|
-
const constDepMatch = line.match(/^with\s+const(?:\s+as\s+(\w+))?$/i);
|
|
1161
|
-
if (constDepMatch) {
|
|
1162
|
-
const handle = constDepMatch[1] ?? "const";
|
|
1163
|
-
deps.push({ kind: "const", handle });
|
|
1164
|
-
continue;
|
|
1165
|
-
}
|
|
1166
|
-
// with <tool> as <handle>
|
|
1167
|
-
const toolDepMatch = line.match(/^with\s+(\S+)\s+as\s+(\w+)$/i);
|
|
1168
|
-
if (toolDepMatch) {
|
|
1169
|
-
deps.push({ kind: "tool", handle: toolDepMatch[2], tool: toolDepMatch[1] });
|
|
1170
|
-
continue;
|
|
1171
|
-
}
|
|
1172
|
-
// on error = <json> (constant fallback)
|
|
1173
|
-
const onErrorConstMatch = line.match(/^on\s+error\s*=\s*(.+)$/i);
|
|
1174
|
-
if (onErrorConstMatch) {
|
|
1175
|
-
let valuePart = onErrorConstMatch[1].trim();
|
|
1176
|
-
// Multi-line JSON: if starts with { or [ and isn't balanced, read more lines
|
|
1177
|
-
if (/^[{[]/.test(valuePart)) {
|
|
1178
|
-
let depth = 0;
|
|
1179
|
-
for (const ch of valuePart) {
|
|
1180
|
-
if (ch === "{" || ch === "[")
|
|
1181
|
-
depth++;
|
|
1182
|
-
if (ch === "}" || ch === "]")
|
|
1183
|
-
depth--;
|
|
1184
|
-
}
|
|
1185
|
-
while (depth > 0 && i + 1 < lines.length) {
|
|
1186
|
-
i++;
|
|
1187
|
-
const nextLine = lines[i];
|
|
1188
|
-
valuePart += "\n" + nextLine;
|
|
1189
|
-
for (const ch of nextLine) {
|
|
1190
|
-
if (ch === "{" || ch === "[")
|
|
1191
|
-
depth++;
|
|
1192
|
-
if (ch === "}" || ch === "]")
|
|
1193
|
-
depth--;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
wires.push({ kind: "onError", value: valuePart.trim() });
|
|
1198
|
-
continue;
|
|
1199
|
-
}
|
|
1200
|
-
// on error <- source (pull fallback from context/dep)
|
|
1201
|
-
const onErrorPullMatch = line.match(/^on\s+error\s*<-\s*(\S+)$/i);
|
|
1202
|
-
if (onErrorPullMatch) {
|
|
1203
|
-
wires.push({ kind: "onError", source: onErrorPullMatch[1] });
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
// Constant wire: .target = "value" or .target = value (unquoted)
|
|
1207
|
-
const constantMatch = line.match(/^\.(\S+)\s*=\s*(?:"([^"]*)"|(\S+))$/);
|
|
1208
|
-
if (constantMatch) {
|
|
1209
|
-
const value = constantMatch[2] ?? constantMatch[3];
|
|
1210
|
-
wires.push({
|
|
1211
|
-
target: constantMatch[1],
|
|
1212
|
-
kind: "constant",
|
|
1213
|
-
value,
|
|
1214
|
-
});
|
|
1215
|
-
continue;
|
|
1216
|
-
}
|
|
1217
|
-
// Pull wire: .target <- source
|
|
1218
|
-
const pullMatch = line.match(/^\.(\S+)\s*<-\s*(\S+)$/);
|
|
1219
|
-
if (pullMatch) {
|
|
1220
|
-
wires.push({ target: pullMatch[1], kind: "pull", source: pullMatch[2] });
|
|
1221
|
-
continue;
|
|
1222
|
-
}
|
|
1223
|
-
// Catch bare param lines without leading dot — give a helpful error
|
|
1224
|
-
if (/^[a-zA-Z]/.test(line)) {
|
|
1225
|
-
throw new Error(`Line ${ln(i)}: Tool params require a dot prefix: ".${line.split(/[\s=<]/)[0]} ...". Only 'with' and 'on error' lines are unprefixed.`);
|
|
1226
|
-
}
|
|
1227
|
-
throw new Error(`Line ${ln(i)}: Unrecognized tool line: ${line}`);
|
|
1228
|
-
}
|
|
1229
|
-
if (!toolName)
|
|
1230
|
-
throw new Error(`Line ${ln(0)}: Missing tool name`);
|
|
1231
|
-
return {
|
|
1232
|
-
kind: "tool",
|
|
1233
|
-
name: toolName,
|
|
1234
|
-
fn: toolFn,
|
|
1235
|
-
extends: toolExtends,
|
|
1236
|
-
deps,
|
|
1237
|
-
wires,
|
|
1238
|
-
};
|
|
1239
|
-
}
|
|
1240
|
-
// ── Path parser ─────────────────────────────────────────────────────────────
|
|
1241
|
-
/**
|
|
1242
|
-
* Parse a dot-separated path with optional array indices.
|
|
1243
|
-
*
|
|
1244
|
-
* "items[0].position.lat" → ["items", "0", "position", "lat"]
|
|
1245
|
-
* "properties[]" → ["properties"] ([] is stripped, signals array)
|
|
1246
|
-
* "x-message-id" → ["x-message-id"]
|
|
1247
|
-
*/
|
|
1248
|
-
export function parsePath(text) {
|
|
1249
|
-
const parts = [];
|
|
1250
|
-
for (const segment of text.split(".")) {
|
|
1251
|
-
const match = segment.match(/^([^[]+)(?:\[(\d*)\])?$/);
|
|
1252
|
-
if (match) {
|
|
1253
|
-
parts.push(match[1]);
|
|
1254
|
-
if (match[2] !== undefined && match[2] !== "") {
|
|
1255
|
-
parts.push(match[2]);
|
|
1256
|
-
}
|
|
1257
|
-
}
|
|
1258
|
-
else {
|
|
1259
|
-
parts.push(segment);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
return parts;
|
|
1263
|
-
}
|
|
1264
|
-
// ── Serializer ──────────────────────────────────────────────────────────────
|
|
1265
|
-
/**
|
|
1266
|
-
* Serialize structured instructions back to .bridge text format.
|
|
1267
|
-
*/
|
|
10
|
+
const BRIDGE_VERSION = "1.4";
|
|
11
|
+
// ── Serializer ───────────────────────────────────────────────────────────────
|
|
1268
12
|
export function serializeBridge(instructions) {
|
|
1269
13
|
const bridges = instructions.filter((i) => i.kind === "bridge");
|
|
1270
14
|
const tools = instructions.filter((i) => i.kind === "tool");
|
|
@@ -1288,6 +32,21 @@ export function serializeBridge(instructions) {
|
|
|
1288
32
|
}
|
|
1289
33
|
return `version ${BRIDGE_VERSION}\n\n` + blocks.join("\n\n") + "\n";
|
|
1290
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a value string needs quoting to be re-parseable as a bare value.
|
|
37
|
+
* Safe unquoted: number, boolean, null, /path, simple-identifier, keyword.
|
|
38
|
+
*/
|
|
39
|
+
function needsQuoting(v) {
|
|
40
|
+
if (v === "" || v === "true" || v === "false" || v === "null")
|
|
41
|
+
return false;
|
|
42
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(v))
|
|
43
|
+
return false; // number
|
|
44
|
+
if (/^\/[\w./-]*$/.test(v))
|
|
45
|
+
return false; // /path
|
|
46
|
+
if (/^[a-zA-Z_][\w-]*$/.test(v))
|
|
47
|
+
return false; // identifier / keyword
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
1291
50
|
function serializeToolBlock(tool) {
|
|
1292
51
|
const lines = [];
|
|
1293
52
|
const hasBody = tool.deps.length > 0 || tool.wires.length > 0;
|
|
@@ -1327,8 +86,7 @@ function serializeToolBlock(tool) {
|
|
|
1327
86
|
}
|
|
1328
87
|
}
|
|
1329
88
|
else if (wire.kind === "constant") {
|
|
1330
|
-
|
|
1331
|
-
if (/\s/.test(wire.value) || wire.value === "") {
|
|
89
|
+
if (needsQuoting(wire.value)) {
|
|
1332
90
|
lines.push(` .${wire.target} = "${wire.value}"`);
|
|
1333
91
|
}
|
|
1334
92
|
else {
|