@stackables/bridge-compiler 0.0.1
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/LICENSE +21 -0
- package/build/bridge-format.d.ts +8 -0
- package/build/bridge-format.d.ts.map +1 -0
- package/build/bridge-format.js +1334 -0
- package/build/bridge-lint.d.ts +3 -0
- package/build/bridge-lint.d.ts.map +1 -0
- package/build/bridge-lint.js +73 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +13 -0
- package/build/language-service.d.ts +50 -0
- package/build/language-service.d.ts.map +1 -0
- package/build/language-service.js +243 -0
- package/build/parser/index.d.ts +9 -0
- package/build/parser/index.d.ts.map +1 -0
- package/build/parser/index.js +7 -0
- package/build/parser/lexer.d.ts +68 -0
- package/build/parser/lexer.d.ts.map +1 -0
- package/build/parser/lexer.js +160 -0
- package/build/parser/parser.d.ts +29 -0
- package/build/parser/parser.d.ts.map +1 -0
- package/build/parser/parser.js +4538 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1334 @@
|
|
|
1
|
+
import { SELF_MODULE } from "@stackables/bridge-core";
|
|
2
|
+
import { parseBridgeChevrotain } from "./parser/index.js";
|
|
3
|
+
export { parsePath } from "@stackables/bridge-core";
|
|
4
|
+
/**
|
|
5
|
+
* Parse .bridge text — delegates to the Chevrotain parser.
|
|
6
|
+
*/
|
|
7
|
+
export function parseBridge(text) {
|
|
8
|
+
return parseBridgeChevrotain(text);
|
|
9
|
+
}
|
|
10
|
+
const BRIDGE_VERSION = "1.5";
|
|
11
|
+
/** Serialize a ControlFlowInstruction to its textual form. */
|
|
12
|
+
function serializeControl(ctrl) {
|
|
13
|
+
if (ctrl.kind === "throw")
|
|
14
|
+
return `throw ${JSON.stringify(ctrl.message)}`;
|
|
15
|
+
if (ctrl.kind === "panic")
|
|
16
|
+
return `panic ${JSON.stringify(ctrl.message)}`;
|
|
17
|
+
if (ctrl.kind === "continue")
|
|
18
|
+
return "continue";
|
|
19
|
+
return "break";
|
|
20
|
+
}
|
|
21
|
+
// ── Serializer ───────────────────────────────────────────────────────────────
|
|
22
|
+
export function serializeBridge(instructions) {
|
|
23
|
+
const bridges = instructions.filter((i) => i.kind === "bridge");
|
|
24
|
+
const tools = instructions.filter((i) => i.kind === "tool");
|
|
25
|
+
const consts = instructions.filter((i) => i.kind === "const");
|
|
26
|
+
const defines = instructions.filter((i) => i.kind === "define");
|
|
27
|
+
if (bridges.length === 0 &&
|
|
28
|
+
tools.length === 0 &&
|
|
29
|
+
consts.length === 0 &&
|
|
30
|
+
defines.length === 0)
|
|
31
|
+
return "";
|
|
32
|
+
const blocks = [];
|
|
33
|
+
// Group const declarations into a single block
|
|
34
|
+
if (consts.length > 0) {
|
|
35
|
+
blocks.push(consts.map((c) => `const ${c.name} = ${c.value}`).join("\n"));
|
|
36
|
+
}
|
|
37
|
+
for (const tool of tools) {
|
|
38
|
+
blocks.push(serializeToolBlock(tool));
|
|
39
|
+
}
|
|
40
|
+
for (const def of defines) {
|
|
41
|
+
blocks.push(serializeDefineBlock(def));
|
|
42
|
+
}
|
|
43
|
+
for (const bridge of bridges) {
|
|
44
|
+
blocks.push(serializeBridgeBlock(bridge));
|
|
45
|
+
}
|
|
46
|
+
return `version ${BRIDGE_VERSION}\n\n` + blocks.join("\n\n") + "\n";
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a value string needs quoting to be re-parseable as a bare value.
|
|
50
|
+
* Safe unquoted: number, boolean, null, /path, simple-identifier, keyword.
|
|
51
|
+
* Already-quoted JSON strings (produced by the updated parser) are also safe.
|
|
52
|
+
*/
|
|
53
|
+
function needsQuoting(v) {
|
|
54
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2)
|
|
55
|
+
return false; // JSON string literal
|
|
56
|
+
if (v === "" || v === "true" || v === "false" || v === "null")
|
|
57
|
+
return false;
|
|
58
|
+
if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(v))
|
|
59
|
+
return false; // number
|
|
60
|
+
if (/^\/[\w./-]*$/.test(v))
|
|
61
|
+
return false; // /path
|
|
62
|
+
if (/^[a-zA-Z_][\w-]*$/.test(v))
|
|
63
|
+
return false; // identifier / keyword
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Format a bare-value string for output.
|
|
68
|
+
* Pre-quoted JSON strings are emitted as-is; everything else goes through
|
|
69
|
+
* the same quoting logic as needsQuoting.
|
|
70
|
+
*/
|
|
71
|
+
function formatBareValue(v) {
|
|
72
|
+
if (v.startsWith('"') && v.endsWith('"') && v.length >= 2)
|
|
73
|
+
return v;
|
|
74
|
+
return needsQuoting(v) ? `"${v}"` : v;
|
|
75
|
+
}
|
|
76
|
+
function serializeToolBlock(tool) {
|
|
77
|
+
const lines = [];
|
|
78
|
+
const hasBody = tool.deps.length > 0 || tool.wires.length > 0;
|
|
79
|
+
// Declaration line — use `tool <name> from <source>` format
|
|
80
|
+
const source = tool.extends ?? tool.fn;
|
|
81
|
+
lines.push(hasBody
|
|
82
|
+
? `tool ${tool.name} from ${source} {`
|
|
83
|
+
: `tool ${tool.name} from ${source}`);
|
|
84
|
+
// Dependencies
|
|
85
|
+
for (const dep of tool.deps) {
|
|
86
|
+
if (dep.kind === "context") {
|
|
87
|
+
if (dep.handle === "context") {
|
|
88
|
+
lines.push(` with context`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
lines.push(` with context as ${dep.handle}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (dep.kind === "const") {
|
|
95
|
+
if (dep.handle === "const") {
|
|
96
|
+
lines.push(` with const`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
lines.push(` with const as ${dep.handle}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
lines.push(` with ${dep.tool} as ${dep.handle}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Wires
|
|
107
|
+
for (const wire of tool.wires) {
|
|
108
|
+
if (wire.kind === "onError") {
|
|
109
|
+
if ("value" in wire) {
|
|
110
|
+
lines.push(` on error = ${wire.value}`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
lines.push(` on error <- ${wire.source}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (wire.kind === "constant") {
|
|
117
|
+
if (needsQuoting(wire.value)) {
|
|
118
|
+
lines.push(` .${wire.target} = "${wire.value}"`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
lines.push(` .${wire.target} = ${wire.value}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lines.push(` .${wire.target} <- ${wire.source}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (hasBody)
|
|
129
|
+
lines.push(`}`);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Serialize a fallback NodeRef as a human-readable source string.
|
|
134
|
+
*
|
|
135
|
+
* If the ref is a pipe-fork root, reconstructs the pipe chain by walking
|
|
136
|
+
* the `toInMap` backward (same logic as the main pipe serializer).
|
|
137
|
+
* Otherwise delegates to `serializeRef`.
|
|
138
|
+
*
|
|
139
|
+
* This is used to emit `catch handle.path` or `catch pipe:source` for wire
|
|
140
|
+
* `catchFallbackRef` values, or `?? ref` for `nullishFallbackRef`.
|
|
141
|
+
*/
|
|
142
|
+
function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle) {
|
|
143
|
+
const refTk = ref.instance != null
|
|
144
|
+
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
145
|
+
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
146
|
+
if (ref.path.length === 0 && pipeHandleTrunkKeys.has(refTk)) {
|
|
147
|
+
// Pipe-fork root — walk the chain to reconstruct `pipe:source` notation
|
|
148
|
+
const handleChain = [];
|
|
149
|
+
let currentTk = refTk;
|
|
150
|
+
let actualSourceRef = null;
|
|
151
|
+
for (;;) {
|
|
152
|
+
const handleName = handleMap.get(currentTk);
|
|
153
|
+
if (!handleName)
|
|
154
|
+
break;
|
|
155
|
+
const inWire = toInMap.get(currentTk);
|
|
156
|
+
const fieldName = inWire?.to.path[0] ?? "in";
|
|
157
|
+
const token = fieldName === "in" ? handleName : `${handleName}.${fieldName}`;
|
|
158
|
+
handleChain.push(token);
|
|
159
|
+
if (!inWire)
|
|
160
|
+
break;
|
|
161
|
+
const fromTk = inWire.from.instance != null
|
|
162
|
+
? `${inWire.from.module}:${inWire.from.type}:${inWire.from.field}:${inWire.from.instance}`
|
|
163
|
+
: `${inWire.from.module}:${inWire.from.type}:${inWire.from.field}`;
|
|
164
|
+
if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) {
|
|
165
|
+
currentTk = fromTk;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
actualSourceRef = inWire.from;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (actualSourceRef && handleChain.length > 0) {
|
|
173
|
+
const sourceStr = serializeRef(actualSourceRef, bridge, handleMap, inputHandle, outputHandle, true);
|
|
174
|
+
return `${handleChain.join(":")}:${sourceStr}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, true);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Serialize a DefineDef into its textual form.
|
|
181
|
+
*
|
|
182
|
+
* Delegates to serializeBridgeBlock with a synthetic Bridge, then replaces
|
|
183
|
+
* the `bridge Define.<name>` header with `define <name>`.
|
|
184
|
+
*/
|
|
185
|
+
function serializeDefineBlock(def) {
|
|
186
|
+
const syntheticBridge = {
|
|
187
|
+
kind: "bridge",
|
|
188
|
+
type: "Define",
|
|
189
|
+
field: def.name,
|
|
190
|
+
handles: def.handles,
|
|
191
|
+
wires: def.wires,
|
|
192
|
+
arrayIterators: def.arrayIterators,
|
|
193
|
+
pipeHandles: def.pipeHandles,
|
|
194
|
+
};
|
|
195
|
+
const bridgeText = serializeBridgeBlock(syntheticBridge);
|
|
196
|
+
// Replace "bridge Define.<name>" → "define <name>"
|
|
197
|
+
return bridgeText.replace(/^bridge Define\.(\w+)/, "define $1");
|
|
198
|
+
}
|
|
199
|
+
function serializeBridgeBlock(bridge) {
|
|
200
|
+
// ── Passthrough shorthand ───────────────────────────────────────────
|
|
201
|
+
if (bridge.passthrough) {
|
|
202
|
+
return `bridge ${bridge.type}.${bridge.field} with ${bridge.passthrough}`;
|
|
203
|
+
}
|
|
204
|
+
const lines = [];
|
|
205
|
+
// ── Header ──────────────────────────────────────────────────────────
|
|
206
|
+
lines.push(`bridge ${bridge.type}.${bridge.field} {`);
|
|
207
|
+
for (const h of bridge.handles) {
|
|
208
|
+
switch (h.kind) {
|
|
209
|
+
case "tool": {
|
|
210
|
+
// Short form `with <name>` when handle == last segment of name
|
|
211
|
+
const lastDot = h.name.lastIndexOf(".");
|
|
212
|
+
const defaultHandle = lastDot !== -1 ? h.name.substring(lastDot + 1) : h.name;
|
|
213
|
+
if (h.handle === defaultHandle) {
|
|
214
|
+
lines.push(` with ${h.name}`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
lines.push(` with ${h.name} as ${h.handle}`);
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
case "input":
|
|
222
|
+
if (h.handle === "input") {
|
|
223
|
+
lines.push(` with input`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
lines.push(` with input as ${h.handle}`);
|
|
227
|
+
}
|
|
228
|
+
break;
|
|
229
|
+
case "output":
|
|
230
|
+
if (h.handle === "output") {
|
|
231
|
+
lines.push(` with output`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
lines.push(` with output as ${h.handle}`);
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case "context":
|
|
238
|
+
lines.push(` with context as ${h.handle}`);
|
|
239
|
+
break;
|
|
240
|
+
case "const":
|
|
241
|
+
if (h.handle === "const") {
|
|
242
|
+
lines.push(` with const`);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
lines.push(` with const as ${h.handle}`);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
case "define":
|
|
249
|
+
lines.push(` with ${h.name} as ${h.handle}`);
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
lines.push("");
|
|
254
|
+
// Mark where the wire body starts — everything after this gets 2-space indent
|
|
255
|
+
const wireBodyStart = lines.length;
|
|
256
|
+
// ── Build handle map for reverse resolution ─────────────────────────
|
|
257
|
+
const { handleMap, inputHandle, outputHandle } = buildHandleMap(bridge);
|
|
258
|
+
// ── Pipe fork registry ──────────────────────────────────────────────
|
|
259
|
+
const pipeHandleTrunkKeys = new Set();
|
|
260
|
+
for (const ph of bridge.pipeHandles ?? []) {
|
|
261
|
+
handleMap.set(ph.key, ph.handle);
|
|
262
|
+
pipeHandleTrunkKeys.add(ph.key);
|
|
263
|
+
}
|
|
264
|
+
// ── Pipe wire detection ─────────────────────────────────────────────
|
|
265
|
+
const refTrunkKey = (ref) => ref.instance != null
|
|
266
|
+
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
267
|
+
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
268
|
+
const toInMap = new Map();
|
|
269
|
+
const fromOutMap = new Map();
|
|
270
|
+
const pipeWireSet = new Set();
|
|
271
|
+
for (const w of bridge.wires) {
|
|
272
|
+
if (!("from" in w) || !w.pipe)
|
|
273
|
+
continue;
|
|
274
|
+
const fw = w;
|
|
275
|
+
pipeWireSet.add(w);
|
|
276
|
+
const toTk = refTrunkKey(fw.to);
|
|
277
|
+
if (fw.to.path.length === 1 && pipeHandleTrunkKeys.has(toTk)) {
|
|
278
|
+
toInMap.set(toTk, fw);
|
|
279
|
+
}
|
|
280
|
+
const fromTk = refTrunkKey(fw.from);
|
|
281
|
+
if (fw.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) {
|
|
282
|
+
fromOutMap.set(fromTk, fw);
|
|
283
|
+
}
|
|
284
|
+
// Concat fork output: from.path=["value"], target is not a pipe handle
|
|
285
|
+
if (fw.from.path.length === 1 &&
|
|
286
|
+
fw.from.path[0] === "value" &&
|
|
287
|
+
pipeHandleTrunkKeys.has(fromTk) &&
|
|
288
|
+
!pipeHandleTrunkKeys.has(toTk)) {
|
|
289
|
+
fromOutMap.set(fromTk, fw);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── Expression fork detection ──────────────────────────────────────────
|
|
293
|
+
// Operator tool name → infix operator symbol
|
|
294
|
+
const FN_TO_OP = {
|
|
295
|
+
multiply: "*",
|
|
296
|
+
divide: "/",
|
|
297
|
+
add: "+",
|
|
298
|
+
subtract: "-",
|
|
299
|
+
eq: "==",
|
|
300
|
+
neq: "!=",
|
|
301
|
+
gt: ">",
|
|
302
|
+
gte: ">=",
|
|
303
|
+
lt: "<",
|
|
304
|
+
lte: "<=",
|
|
305
|
+
__and: "and",
|
|
306
|
+
__or: "or",
|
|
307
|
+
not: "not",
|
|
308
|
+
};
|
|
309
|
+
const OP_PREC_SER = {
|
|
310
|
+
"*": 4,
|
|
311
|
+
"/": 4,
|
|
312
|
+
"+": 3,
|
|
313
|
+
"-": 3,
|
|
314
|
+
"==": 2,
|
|
315
|
+
"!=": 2,
|
|
316
|
+
">": 2,
|
|
317
|
+
">=": 2,
|
|
318
|
+
"<": 2,
|
|
319
|
+
"<=": 2,
|
|
320
|
+
and: 1,
|
|
321
|
+
or: 0,
|
|
322
|
+
not: -1,
|
|
323
|
+
};
|
|
324
|
+
const exprForks = new Map();
|
|
325
|
+
const exprPipeWireSet = new Set(); // wires that belong to expression forks
|
|
326
|
+
for (const ph of bridge.pipeHandles ?? []) {
|
|
327
|
+
if (!ph.handle.startsWith("__expr_"))
|
|
328
|
+
continue;
|
|
329
|
+
const op = FN_TO_OP[ph.baseTrunk.field];
|
|
330
|
+
if (!op)
|
|
331
|
+
continue;
|
|
332
|
+
// For condAnd/condOr wires (field === "__and" or "__or")
|
|
333
|
+
if (ph.baseTrunk.field === "__and" || ph.baseTrunk.field === "__or") {
|
|
334
|
+
const logicWire = bridge.wires.find((w) => {
|
|
335
|
+
const prop = ph.baseTrunk.field === "__and" ? "condAnd" : "condOr";
|
|
336
|
+
return prop in w && refTrunkKey(w.to) === ph.key;
|
|
337
|
+
});
|
|
338
|
+
if (logicWire) {
|
|
339
|
+
exprForks.set(ph.key, {
|
|
340
|
+
op,
|
|
341
|
+
bWire: undefined,
|
|
342
|
+
aWire: undefined,
|
|
343
|
+
logicWire,
|
|
344
|
+
});
|
|
345
|
+
exprPipeWireSet.add(logicWire);
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// Find the .a and .b wires for this fork
|
|
350
|
+
let aWire;
|
|
351
|
+
let bWire;
|
|
352
|
+
for (const w of bridge.wires) {
|
|
353
|
+
const wTo = w.to;
|
|
354
|
+
if (!wTo || refTrunkKey(wTo) !== ph.key || wTo.path.length !== 1)
|
|
355
|
+
continue;
|
|
356
|
+
if (wTo.path[0] === "a" && "from" in w)
|
|
357
|
+
aWire = w;
|
|
358
|
+
else if (wTo.path[0] === "b")
|
|
359
|
+
bWire = w;
|
|
360
|
+
}
|
|
361
|
+
exprForks.set(ph.key, { op, bWire, aWire });
|
|
362
|
+
if (bWire)
|
|
363
|
+
exprPipeWireSet.add(bWire);
|
|
364
|
+
if (aWire)
|
|
365
|
+
exprPipeWireSet.add(aWire);
|
|
366
|
+
}
|
|
367
|
+
const concatForks = new Map();
|
|
368
|
+
const concatPipeWireSet = new Set(); // wires that belong to concat forks
|
|
369
|
+
for (const ph of bridge.pipeHandles ?? []) {
|
|
370
|
+
if (!ph.handle.startsWith("__concat_"))
|
|
371
|
+
continue;
|
|
372
|
+
if (ph.baseTrunk.field !== "concat")
|
|
373
|
+
continue;
|
|
374
|
+
// Collect parts.N wires (constant or pull)
|
|
375
|
+
const partsMap = new Map();
|
|
376
|
+
for (const w of bridge.wires) {
|
|
377
|
+
const wTo = w.to;
|
|
378
|
+
if (!wTo || refTrunkKey(wTo) !== ph.key)
|
|
379
|
+
continue;
|
|
380
|
+
if (wTo.path.length !== 2 || wTo.path[0] !== "parts")
|
|
381
|
+
continue;
|
|
382
|
+
const idx = parseInt(wTo.path[1], 10);
|
|
383
|
+
if (isNaN(idx))
|
|
384
|
+
continue;
|
|
385
|
+
if ("value" in w && !("from" in w)) {
|
|
386
|
+
partsMap.set(idx, { kind: "text", value: w.value });
|
|
387
|
+
}
|
|
388
|
+
else if ("from" in w) {
|
|
389
|
+
partsMap.set(idx, { kind: "ref", ref: w.from });
|
|
390
|
+
}
|
|
391
|
+
concatPipeWireSet.add(w);
|
|
392
|
+
}
|
|
393
|
+
// Build ordered parts array
|
|
394
|
+
const maxIdx = Math.max(...partsMap.keys(), -1);
|
|
395
|
+
const parts = [];
|
|
396
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
397
|
+
const part = partsMap.get(i);
|
|
398
|
+
if (part)
|
|
399
|
+
parts.push(part);
|
|
400
|
+
}
|
|
401
|
+
concatForks.set(ph.key, { parts });
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Reconstruct a template string from a concat fork.
|
|
405
|
+
* Returns `"literal{ref}literal"` notation.
|
|
406
|
+
*/
|
|
407
|
+
function reconstructTemplateString(forkTk) {
|
|
408
|
+
const info = concatForks.get(forkTk);
|
|
409
|
+
if (!info || info.parts.length === 0)
|
|
410
|
+
return null;
|
|
411
|
+
let result = "";
|
|
412
|
+
for (const part of info.parts) {
|
|
413
|
+
if (part.kind === "text") {
|
|
414
|
+
// Escape backslashes before braces first, then escape literal braces
|
|
415
|
+
result += part.value.replace(/\\/g, "\\\\").replace(/\{/g, "\\{");
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const refStr = part.ref.element
|
|
419
|
+
? "ITER." + serPath(part.ref.path)
|
|
420
|
+
: sRef(part.ref, true);
|
|
421
|
+
result += `{${refStr}}`;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return `"${result}"`;
|
|
425
|
+
}
|
|
426
|
+
// ── Group element wires by array-destination field ──────────────────
|
|
427
|
+
// Pull wires: from.element=true
|
|
428
|
+
const elementPullWires = bridge.wires.filter((w) => "from" in w && !!w.from.element);
|
|
429
|
+
// Constant wires: "value" in w && to.element=true
|
|
430
|
+
const elementConstWires = bridge.wires.filter((w) => "value" in w && !!w.to.element);
|
|
431
|
+
// Build grouped maps keyed by the full array-destination path (to.path joined)
|
|
432
|
+
// For a 1-level array o.items <- src[], element paths are like ["items", "name"]
|
|
433
|
+
// For a root-level array o <- src[], element paths are like ["name"]
|
|
434
|
+
// For nested arrays, inner element paths are like ["items", "legs", "trainName"]
|
|
435
|
+
const elementPullAll = elementPullWires.filter((w) => !exprPipeWireSet.has(w) &&
|
|
436
|
+
!pipeWireSet.has(w) &&
|
|
437
|
+
!concatPipeWireSet.has(w));
|
|
438
|
+
const elementConstAll = elementConstWires.filter((w) => !exprPipeWireSet.has(w) && !concatPipeWireSet.has(w));
|
|
439
|
+
const elementExprWires = [];
|
|
440
|
+
// Detect array source wires: a regular wire whose to.path (joined) matches
|
|
441
|
+
// a key in arrayIterators. This includes root-level arrays (path=[]).
|
|
442
|
+
const arrayIterators = bridge.arrayIterators ?? {};
|
|
443
|
+
// ── Exclude pipe, element-pull, element-const, expression-internal, concat-internal, and __local wires from main loop
|
|
444
|
+
const regularWires = bridge.wires.filter((w) => !pipeWireSet.has(w) &&
|
|
445
|
+
!exprPipeWireSet.has(w) &&
|
|
446
|
+
!concatPipeWireSet.has(w) &&
|
|
447
|
+
(!("from" in w) || !w.from.element) &&
|
|
448
|
+
(!("value" in w) || !w.to.element) &&
|
|
449
|
+
w.to.module !== "__local" &&
|
|
450
|
+
(!("from" in w) || w.from.module !== "__local"));
|
|
451
|
+
const localBindingsByAlias = new Map();
|
|
452
|
+
const localReadWires = [];
|
|
453
|
+
for (const w of bridge.wires) {
|
|
454
|
+
if (w.to.module === "__local" && "from" in w) {
|
|
455
|
+
localBindingsByAlias.set(w.to.field, {
|
|
456
|
+
alias: w.to.field,
|
|
457
|
+
sourceWire: w,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if ("from" in w && w.from.module === "__local") {
|
|
461
|
+
localReadWires.push(w);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const serializedArrays = new Set();
|
|
465
|
+
// ── Helper: serialize a reference (forward outputHandle) ─────────────
|
|
466
|
+
const sRef = (ref, isFrom) => serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom);
|
|
467
|
+
const sPipeOrRef = (ref) => serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge, inputHandle, outputHandle);
|
|
468
|
+
// ── Pre-compute element expression wires ────────────────────────────
|
|
469
|
+
// Walk expression trees from fromOutMap that target element refs
|
|
470
|
+
for (const [tk, outWire] of fromOutMap.entries()) {
|
|
471
|
+
if (!exprForks.has(tk) || !outWire.to.element)
|
|
472
|
+
continue;
|
|
473
|
+
// Recursively serialize expression fork tree
|
|
474
|
+
function serializeElemExprTree(forkTk, parentPrec) {
|
|
475
|
+
const info = exprForks.get(forkTk);
|
|
476
|
+
if (!info)
|
|
477
|
+
return null;
|
|
478
|
+
// condAnd/condOr logic wire — reconstruct from leftRef/rightRef
|
|
479
|
+
if (info.logicWire) {
|
|
480
|
+
const logic = "condAnd" in info.logicWire
|
|
481
|
+
? info.logicWire.condAnd
|
|
482
|
+
: info.logicWire.condOr;
|
|
483
|
+
let leftStr;
|
|
484
|
+
const leftTk = refTrunkKey(logic.leftRef);
|
|
485
|
+
if (logic.leftRef.path.length === 0 && exprForks.has(leftTk)) {
|
|
486
|
+
leftStr =
|
|
487
|
+
serializeElemExprTree(leftTk, OP_PREC_SER[info.op] ?? 0) ??
|
|
488
|
+
sRef(logic.leftRef, true);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
leftStr = logic.leftRef.element
|
|
492
|
+
? "ITER." + serPath(logic.leftRef.path)
|
|
493
|
+
: sRef(logic.leftRef, true);
|
|
494
|
+
}
|
|
495
|
+
let rightStr;
|
|
496
|
+
if (logic.rightRef) {
|
|
497
|
+
const rightTk = refTrunkKey(logic.rightRef);
|
|
498
|
+
if (logic.rightRef.path.length === 0 && exprForks.has(rightTk)) {
|
|
499
|
+
rightStr =
|
|
500
|
+
serializeElemExprTree(rightTk, OP_PREC_SER[info.op] ?? 0) ??
|
|
501
|
+
sRef(logic.rightRef, true);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
rightStr = logic.rightRef.element
|
|
505
|
+
? "ITER." + serPath(logic.rightRef.path)
|
|
506
|
+
: sRef(logic.rightRef, true);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
else if (logic.rightValue != null) {
|
|
510
|
+
rightStr = logic.rightValue;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
rightStr = "0";
|
|
514
|
+
}
|
|
515
|
+
let result = `${leftStr} ${info.op} ${rightStr}`;
|
|
516
|
+
const myPrec = OP_PREC_SER[info.op] ?? 0;
|
|
517
|
+
if (parentPrec != null && myPrec < parentPrec)
|
|
518
|
+
result = `(${result})`;
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
let leftStr = null;
|
|
522
|
+
if (info.aWire) {
|
|
523
|
+
const fromTk = refTrunkKey(info.aWire.from);
|
|
524
|
+
if (info.aWire.from.path.length === 0 && exprForks.has(fromTk)) {
|
|
525
|
+
leftStr = serializeElemExprTree(fromTk, OP_PREC_SER[info.op] ?? 0);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
leftStr = info.aWire.from.element
|
|
529
|
+
? "ITER." + serPath(info.aWire.from.path)
|
|
530
|
+
: sRef(info.aWire.from, true);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
let rightStr;
|
|
534
|
+
if (info.bWire && "value" in info.bWire) {
|
|
535
|
+
rightStr = info.bWire.value;
|
|
536
|
+
}
|
|
537
|
+
else if (info.bWire && "from" in info.bWire) {
|
|
538
|
+
const bFrom = info.bWire.from;
|
|
539
|
+
const bTk = refTrunkKey(bFrom);
|
|
540
|
+
if (bFrom.path.length === 0 && exprForks.has(bTk)) {
|
|
541
|
+
rightStr =
|
|
542
|
+
serializeElemExprTree(bTk, OP_PREC_SER[info.op] ?? 0) ??
|
|
543
|
+
sRef(bFrom, true);
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
rightStr = bFrom.element
|
|
547
|
+
? "ITER." + serPath(bFrom.path)
|
|
548
|
+
: sRef(bFrom, true);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
rightStr = "0";
|
|
553
|
+
}
|
|
554
|
+
if (leftStr == null)
|
|
555
|
+
return rightStr;
|
|
556
|
+
if (info.op === "not")
|
|
557
|
+
return `not ${leftStr}`;
|
|
558
|
+
let result = `${leftStr} ${info.op} ${rightStr}`;
|
|
559
|
+
const myPrec = OP_PREC_SER[info.op] ?? 0;
|
|
560
|
+
if (parentPrec != null && myPrec < parentPrec)
|
|
561
|
+
result = `(${result})`;
|
|
562
|
+
return result;
|
|
563
|
+
}
|
|
564
|
+
const exprStr = serializeElemExprTree(tk);
|
|
565
|
+
if (exprStr) {
|
|
566
|
+
elementExprWires.push({
|
|
567
|
+
toPath: outWire.to.path,
|
|
568
|
+
sourceStr: exprStr,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Pre-compute element-targeting concat (template string) wires
|
|
573
|
+
for (const [tk, outWire] of fromOutMap.entries()) {
|
|
574
|
+
if (!concatForks.has(tk) || !outWire.to.element)
|
|
575
|
+
continue;
|
|
576
|
+
const templateStr = reconstructTemplateString(tk);
|
|
577
|
+
if (templateStr) {
|
|
578
|
+
elementExprWires.push({
|
|
579
|
+
toPath: outWire.to.path,
|
|
580
|
+
sourceStr: templateStr,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Recursively serialize element wires for an array mapping block.
|
|
586
|
+
* Handles nested array-in-array mappings by detecting inner iterators.
|
|
587
|
+
*/
|
|
588
|
+
function serializeArrayElements(arrayPath, parentIterName, indent) {
|
|
589
|
+
const arrayPathStr = arrayPath.join(".");
|
|
590
|
+
const pathDepth = arrayPath.length;
|
|
591
|
+
// Find element constant wires at this level (path starts with arrayPath + one more segment)
|
|
592
|
+
const levelConsts = elementConstAll.filter((ew) => {
|
|
593
|
+
if (ew.to.path.length !== pathDepth + 1)
|
|
594
|
+
return false;
|
|
595
|
+
for (let i = 0; i < pathDepth; i++) {
|
|
596
|
+
if (ew.to.path[i] !== arrayPath[i])
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
return true;
|
|
600
|
+
});
|
|
601
|
+
// Find element pull wires at this level (direct fields, not nested array children)
|
|
602
|
+
const levelPulls = elementPullAll.filter((ew) => {
|
|
603
|
+
if (ew.to.path.length < pathDepth + 1)
|
|
604
|
+
return false;
|
|
605
|
+
for (let i = 0; i < pathDepth; i++) {
|
|
606
|
+
if (ew.to.path[i] !== arrayPath[i])
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
// Check this wire is a direct field (depth == pathDepth+1)
|
|
610
|
+
// or a nested array source (its path matches a nested iterator key)
|
|
611
|
+
return true;
|
|
612
|
+
});
|
|
613
|
+
// Partition pulls into direct-level fields vs nested-array sources
|
|
614
|
+
const nestedArrayPaths = new Set();
|
|
615
|
+
for (const key of Object.keys(arrayIterators)) {
|
|
616
|
+
// A nested array key starts with the current array path
|
|
617
|
+
if (key.length > arrayPathStr.length &&
|
|
618
|
+
(arrayPathStr === "" ? true : key.startsWith(arrayPathStr + ".")) &&
|
|
619
|
+
!key
|
|
620
|
+
.substring(arrayPathStr === "" ? 0 : arrayPathStr.length + 1)
|
|
621
|
+
.includes(".")) {
|
|
622
|
+
nestedArrayPaths.add(key);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Emit block-scoped local bindings: alias <source> as <name>
|
|
626
|
+
for (const [alias, info] of localBindingsByAlias) {
|
|
627
|
+
const srcWire = info.sourceWire;
|
|
628
|
+
// Reconstruct the source expression
|
|
629
|
+
const fromRef = srcWire.from;
|
|
630
|
+
let sourcePart;
|
|
631
|
+
if (fromRef.element) {
|
|
632
|
+
sourcePart =
|
|
633
|
+
parentIterName +
|
|
634
|
+
(fromRef.path.length > 0 ? "." + serPath(fromRef.path) : "");
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
// Check if the source is a pipe fork — reconstruct pipe:source syntax
|
|
638
|
+
const srcTk = refTrunkKey(fromRef);
|
|
639
|
+
if (fromRef.path.length === 0 && pipeHandleTrunkKeys.has(srcTk)) {
|
|
640
|
+
// Walk the pipe chain backward to reconstruct pipe:source
|
|
641
|
+
const parts = [];
|
|
642
|
+
let currentTk = srcTk;
|
|
643
|
+
while (true) {
|
|
644
|
+
const handleName = handleMap.get(currentTk);
|
|
645
|
+
if (!handleName)
|
|
646
|
+
break;
|
|
647
|
+
parts.push(handleName);
|
|
648
|
+
const inWire = toInMap.get(currentTk);
|
|
649
|
+
if (!inWire)
|
|
650
|
+
break;
|
|
651
|
+
if (inWire.from.element) {
|
|
652
|
+
parts.push(parentIterName +
|
|
653
|
+
(inWire.from.path.length > 0
|
|
654
|
+
? "." + serPath(inWire.from.path)
|
|
655
|
+
: ""));
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
const innerTk = refTrunkKey(inWire.from);
|
|
659
|
+
if (inWire.from.path.length === 0 &&
|
|
660
|
+
pipeHandleTrunkKeys.has(innerTk)) {
|
|
661
|
+
currentTk = innerTk;
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
parts.push(sRef(inWire.from, true));
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
sourcePart = parts.join(":");
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
sourcePart = sRef(fromRef, true);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
lines.push(`${indent}alias ${sourcePart} as ${alias}`);
|
|
675
|
+
}
|
|
676
|
+
// Emit constant element wires
|
|
677
|
+
for (const ew of levelConsts) {
|
|
678
|
+
const fieldPath = ew.to.path.slice(pathDepth);
|
|
679
|
+
const elemTo = "." + serPath(fieldPath);
|
|
680
|
+
lines.push(`${indent}${elemTo} = ${formatBareValue(ew.value)}`);
|
|
681
|
+
}
|
|
682
|
+
// Emit pull element wires (direct level only)
|
|
683
|
+
for (const ew of levelPulls) {
|
|
684
|
+
const toPathStr = ew.to.path.join(".");
|
|
685
|
+
// Skip wires that belong to a nested array level
|
|
686
|
+
if (ew.to.path.length > pathDepth + 1) {
|
|
687
|
+
// Check if this wire's immediate child segment forms a nested array
|
|
688
|
+
const childPath = ew.to.path.slice(0, pathDepth + 1).join(".");
|
|
689
|
+
if (nestedArrayPaths.has(childPath))
|
|
690
|
+
continue; // handled by nested block
|
|
691
|
+
}
|
|
692
|
+
// Check if this wire IS a nested array source
|
|
693
|
+
if (nestedArrayPaths.has(toPathStr) && !serializedArrays.has(toPathStr)) {
|
|
694
|
+
serializedArrays.add(toPathStr);
|
|
695
|
+
const nestedIterName = arrayIterators[toPathStr];
|
|
696
|
+
const fromPart = ew.from.element
|
|
697
|
+
? parentIterName + "." + serPath(ew.from.path)
|
|
698
|
+
: sRef(ew.from, true);
|
|
699
|
+
const fieldPath = ew.to.path.slice(pathDepth);
|
|
700
|
+
const elemTo = "." + serPath(fieldPath);
|
|
701
|
+
lines.push(`${indent}${elemTo} <- ${fromPart}[] as ${nestedIterName} {`);
|
|
702
|
+
serializeArrayElements(ew.to.path, nestedIterName, indent + " ");
|
|
703
|
+
lines.push(`${indent}}`);
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
// Regular element pull wire
|
|
707
|
+
const fromPart = ew.from.element
|
|
708
|
+
? parentIterName + "." + serPath(ew.from.path)
|
|
709
|
+
: sRef(ew.from, true);
|
|
710
|
+
const fieldPath = ew.to.path.slice(pathDepth);
|
|
711
|
+
const elemTo = "." + serPath(fieldPath);
|
|
712
|
+
const ffr = ew.falsyFallbackRefs?.map((r) => ` || ${sPipeOrRef(r)}`).join("") ?? "";
|
|
713
|
+
const nfb = ffr +
|
|
714
|
+
("falsyControl" in ew && ew.falsyControl
|
|
715
|
+
? ` || ${serializeControl(ew.falsyControl)}`
|
|
716
|
+
: "falsyFallback" in ew && ew.falsyFallback
|
|
717
|
+
? ` || ${ew.falsyFallback}`
|
|
718
|
+
: "");
|
|
719
|
+
const nuf = "nullishControl" in ew && ew.nullishControl
|
|
720
|
+
? ` ?? ${serializeControl(ew.nullishControl)}`
|
|
721
|
+
: "nullishFallbackRef" in ew && ew.nullishFallbackRef
|
|
722
|
+
? ` ?? ${sPipeOrRef(ew.nullishFallbackRef)}`
|
|
723
|
+
: "nullishFallback" in ew && ew.nullishFallback
|
|
724
|
+
? ` ?? ${ew.nullishFallback}`
|
|
725
|
+
: "";
|
|
726
|
+
const errf = "catchControl" in ew && ew.catchControl
|
|
727
|
+
? ` catch ${serializeControl(ew.catchControl)}`
|
|
728
|
+
: "catchFallbackRef" in ew && ew.catchFallbackRef
|
|
729
|
+
? ` catch ${sPipeOrRef(ew.catchFallbackRef)}`
|
|
730
|
+
: "catchFallback" in ew && ew.catchFallback
|
|
731
|
+
? ` catch ${ew.catchFallback}`
|
|
732
|
+
: "";
|
|
733
|
+
lines.push(`${indent}${elemTo} <- ${fromPart}${nfb}${nuf}${errf}`);
|
|
734
|
+
}
|
|
735
|
+
// Emit expression element wires at this level
|
|
736
|
+
for (const eew of elementExprWires) {
|
|
737
|
+
if (eew.toPath.length !== pathDepth + 1)
|
|
738
|
+
continue;
|
|
739
|
+
let match = true;
|
|
740
|
+
for (let i = 0; i < pathDepth; i++) {
|
|
741
|
+
if (eew.toPath[i] !== arrayPath[i]) {
|
|
742
|
+
match = false;
|
|
743
|
+
break;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (!match)
|
|
747
|
+
continue;
|
|
748
|
+
const fieldPath = eew.toPath.slice(pathDepth);
|
|
749
|
+
const elemTo = "." + serPath(fieldPath);
|
|
750
|
+
// Replace ITER. placeholder with actual iterator name
|
|
751
|
+
const src = eew.sourceStr.replaceAll("ITER.", parentIterName + ".");
|
|
752
|
+
lines.push(`${indent}${elemTo} <- ${src}`);
|
|
753
|
+
}
|
|
754
|
+
// Emit local-binding read wires at this level (.field <- alias.path)
|
|
755
|
+
for (const lw of localReadWires) {
|
|
756
|
+
if (lw.to.path.length < pathDepth + 1)
|
|
757
|
+
continue;
|
|
758
|
+
let match = true;
|
|
759
|
+
for (let i = 0; i < pathDepth; i++) {
|
|
760
|
+
if (lw.to.path[i] !== arrayPath[i]) {
|
|
761
|
+
match = false;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (!match)
|
|
766
|
+
continue;
|
|
767
|
+
const fieldPath = lw.to.path.slice(pathDepth);
|
|
768
|
+
const elemTo = "." + serPath(fieldPath);
|
|
769
|
+
const alias = lw.from.field; // __local:Shadow:<alias>
|
|
770
|
+
const fromPart = lw.from.path.length > 0 ? alias + "." + serPath(lw.from.path) : alias;
|
|
771
|
+
lines.push(`${indent}${elemTo} <- ${fromPart}`);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
// ── Helper: serialize an expression fork tree for a ref (used for cond) ──
|
|
775
|
+
function serializeExprOrRef(ref) {
|
|
776
|
+
const tk = refTrunkKey(ref);
|
|
777
|
+
if (ref.path.length === 0 && exprForks.has(tk)) {
|
|
778
|
+
// Recursively serialize expression fork
|
|
779
|
+
function serFork(forkTk) {
|
|
780
|
+
const info = exprForks.get(forkTk);
|
|
781
|
+
if (!info)
|
|
782
|
+
return "?";
|
|
783
|
+
let leftStr = null;
|
|
784
|
+
if (info.aWire) {
|
|
785
|
+
const aTk = refTrunkKey(info.aWire.from);
|
|
786
|
+
if (info.aWire.from.path.length === 0 && exprForks.has(aTk)) {
|
|
787
|
+
leftStr = serFork(aTk);
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
leftStr = sRef(info.aWire.from, true);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
let rightStr;
|
|
794
|
+
if (info.bWire && "value" in info.bWire) {
|
|
795
|
+
rightStr = info.bWire.value;
|
|
796
|
+
}
|
|
797
|
+
else if (info.bWire && "from" in info.bWire) {
|
|
798
|
+
const bFrom = info.bWire.from;
|
|
799
|
+
const bTk = refTrunkKey(bFrom);
|
|
800
|
+
rightStr =
|
|
801
|
+
bFrom.path.length === 0 && exprForks.has(bTk)
|
|
802
|
+
? serFork(bTk)
|
|
803
|
+
: sRef(bFrom, true);
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
rightStr = "0";
|
|
807
|
+
}
|
|
808
|
+
if (leftStr == null)
|
|
809
|
+
return rightStr;
|
|
810
|
+
if (info.op === "not")
|
|
811
|
+
return `not ${leftStr}`;
|
|
812
|
+
return `${leftStr} ${info.op} ${rightStr}`;
|
|
813
|
+
}
|
|
814
|
+
return serFork(tk) ?? sRef(ref, true);
|
|
815
|
+
}
|
|
816
|
+
return sRef(ref, true);
|
|
817
|
+
}
|
|
818
|
+
for (const w of regularWires) {
|
|
819
|
+
// Conditional (ternary) wire
|
|
820
|
+
if ("cond" in w) {
|
|
821
|
+
const toStr = sRef(w.to, false);
|
|
822
|
+
const condStr = serializeExprOrRef(w.cond);
|
|
823
|
+
const thenStr = w.thenRef
|
|
824
|
+
? sRef(w.thenRef, true)
|
|
825
|
+
: (w.thenValue ?? "null");
|
|
826
|
+
const elseStr = w.elseRef
|
|
827
|
+
? sRef(w.elseRef, true)
|
|
828
|
+
: (w.elseValue ?? "null");
|
|
829
|
+
const ffr = w.falsyFallbackRefs?.map((r) => ` || ${sPipeOrRef(r)}`).join("") ?? "";
|
|
830
|
+
const nfb = ffr +
|
|
831
|
+
("falsyControl" in w && w.falsyControl
|
|
832
|
+
? ` || ${serializeControl(w.falsyControl)}`
|
|
833
|
+
: w.falsyFallback
|
|
834
|
+
? ` || ${w.falsyFallback}`
|
|
835
|
+
: "");
|
|
836
|
+
const nuf = "nullishControl" in w && w.nullishControl
|
|
837
|
+
? ` ?? ${serializeControl(w.nullishControl)}`
|
|
838
|
+
: w.nullishFallbackRef
|
|
839
|
+
? ` ?? ${sPipeOrRef(w.nullishFallbackRef)}`
|
|
840
|
+
: w.nullishFallback
|
|
841
|
+
? ` ?? ${w.nullishFallback}`
|
|
842
|
+
: "";
|
|
843
|
+
const errf = "catchControl" in w && w.catchControl
|
|
844
|
+
? ` catch ${serializeControl(w.catchControl)}`
|
|
845
|
+
: w.catchFallbackRef
|
|
846
|
+
? ` catch ${sPipeOrRef(w.catchFallbackRef)}`
|
|
847
|
+
: w.catchFallback
|
|
848
|
+
? ` catch ${w.catchFallback}`
|
|
849
|
+
: "";
|
|
850
|
+
lines.push(`${toStr} <- ${condStr} ? ${thenStr} : ${elseStr}${nfb}${nuf}${errf}`);
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
// Constant wire
|
|
854
|
+
if ("value" in w) {
|
|
855
|
+
const toStr = sRef(w.to, false);
|
|
856
|
+
lines.push(`${toStr} = ${formatBareValue(w.value)}`);
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
// Skip condAnd/condOr wires (handled in expression tree serialization)
|
|
860
|
+
if ("condAnd" in w || "condOr" in w)
|
|
861
|
+
continue;
|
|
862
|
+
// Array mapping — emit brace-delimited element block
|
|
863
|
+
const arrayKey = w.to.path.join(".");
|
|
864
|
+
if (arrayKey in arrayIterators && !serializedArrays.has(arrayKey)) {
|
|
865
|
+
serializedArrays.add(arrayKey);
|
|
866
|
+
const iterName = arrayIterators[arrayKey];
|
|
867
|
+
const fromStr = sRef(w.from, true) + "[]";
|
|
868
|
+
const toStr = sRef(w.to, false);
|
|
869
|
+
lines.push(`${toStr} <- ${fromStr} as ${iterName} {`);
|
|
870
|
+
serializeArrayElements(w.to.path, iterName, " ");
|
|
871
|
+
lines.push(`}`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
// Regular wire
|
|
875
|
+
let fromStr = sRef(w.from, true);
|
|
876
|
+
// Per-segment safe navigation: insert ?. at correct positions
|
|
877
|
+
if (w.safe) {
|
|
878
|
+
const ref = w.from;
|
|
879
|
+
if (ref.rootSafe || ref.pathSafe?.some((s) => s)) {
|
|
880
|
+
// Re-serialize the path with per-segment safety
|
|
881
|
+
const handle = fromStr.split(".")[0].split("[")[0];
|
|
882
|
+
const parts = [handle];
|
|
883
|
+
for (let i = 0; i < ref.path.length; i++) {
|
|
884
|
+
const seg = ref.path[i];
|
|
885
|
+
const isSafe = i === 0 ? !!ref.rootSafe : !!ref.pathSafe?.[i];
|
|
886
|
+
if (/^\d+$/.test(seg)) {
|
|
887
|
+
parts.push(`[${seg}]`);
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
parts.push(`${isSafe ? "?." : "."}${seg}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
fromStr = parts.join("");
|
|
894
|
+
}
|
|
895
|
+
else if (fromStr.includes(".")) {
|
|
896
|
+
// Legacy behavior: safe flag without per-segment info, put ?. after root
|
|
897
|
+
fromStr = fromStr.replace(".", "?.");
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
const toStr = sRef(w.to, false);
|
|
901
|
+
const ffr = w.falsyFallbackRefs?.map((r) => ` || ${sPipeOrRef(r)}`).join("") ?? "";
|
|
902
|
+
const nfb = ffr +
|
|
903
|
+
("falsyControl" in w && w.falsyControl
|
|
904
|
+
? ` || ${serializeControl(w.falsyControl)}`
|
|
905
|
+
: w.falsyFallback
|
|
906
|
+
? ` || ${w.falsyFallback}`
|
|
907
|
+
: "");
|
|
908
|
+
const nuf = "nullishControl" in w && w.nullishControl
|
|
909
|
+
? ` ?? ${serializeControl(w.nullishControl)}`
|
|
910
|
+
: w.nullishFallbackRef
|
|
911
|
+
? ` ?? ${sPipeOrRef(w.nullishFallbackRef)}`
|
|
912
|
+
: w.nullishFallback
|
|
913
|
+
? ` ?? ${w.nullishFallback}`
|
|
914
|
+
: "";
|
|
915
|
+
const errf = "catchControl" in w && w.catchControl
|
|
916
|
+
? ` catch ${serializeControl(w.catchControl)}`
|
|
917
|
+
: w.catchFallbackRef
|
|
918
|
+
? ` catch ${sPipeOrRef(w.catchFallbackRef)}`
|
|
919
|
+
: w.catchFallback
|
|
920
|
+
? ` catch ${w.catchFallback}`
|
|
921
|
+
: "";
|
|
922
|
+
lines.push(`${toStr} <- ${fromStr}${nfb}${nuf}${errf}`);
|
|
923
|
+
}
|
|
924
|
+
// ── Top-level alias declarations ─────────────────────────────────────
|
|
925
|
+
// Emit `alias <source> as <name>` for __local bindings that are NOT
|
|
926
|
+
// element-scoped (those are handled inside serializeArrayElements).
|
|
927
|
+
for (const [alias, info] of localBindingsByAlias) {
|
|
928
|
+
const srcWire = info.sourceWire;
|
|
929
|
+
const fromRef = srcWire.from;
|
|
930
|
+
// Element-scoped bindings are emitted inside array blocks
|
|
931
|
+
if (fromRef.element)
|
|
932
|
+
continue;
|
|
933
|
+
// Check if source is a pipe fork with element-sourced input (array-scoped)
|
|
934
|
+
const srcTk = refTrunkKey(fromRef);
|
|
935
|
+
if (fromRef.path.length === 0 && pipeHandleTrunkKeys.has(srcTk)) {
|
|
936
|
+
const inWire = toInMap.get(srcTk);
|
|
937
|
+
if (inWire && inWire.from.element)
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
// Reconstruct source expression
|
|
941
|
+
let sourcePart;
|
|
942
|
+
if (fromRef.path.length === 0 && pipeHandleTrunkKeys.has(srcTk)) {
|
|
943
|
+
const parts = [];
|
|
944
|
+
let currentTk = srcTk;
|
|
945
|
+
while (true) {
|
|
946
|
+
const handleName = handleMap.get(currentTk);
|
|
947
|
+
if (!handleName)
|
|
948
|
+
break;
|
|
949
|
+
parts.push(handleName);
|
|
950
|
+
const inWire = toInMap.get(currentTk);
|
|
951
|
+
if (!inWire)
|
|
952
|
+
break;
|
|
953
|
+
const innerTk = refTrunkKey(inWire.from);
|
|
954
|
+
if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(innerTk)) {
|
|
955
|
+
currentTk = innerTk;
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
parts.push(sRef(inWire.from, true));
|
|
959
|
+
break;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
sourcePart = parts.join(":");
|
|
963
|
+
}
|
|
964
|
+
else {
|
|
965
|
+
sourcePart = sRef(fromRef, true);
|
|
966
|
+
}
|
|
967
|
+
lines.push(`alias ${sourcePart} as ${alias}`);
|
|
968
|
+
}
|
|
969
|
+
// Also emit wires reading from top-level __local bindings
|
|
970
|
+
for (const lw of localReadWires) {
|
|
971
|
+
// Skip element-targeting reads (emitted inside array blocks)
|
|
972
|
+
if (lw.to.module === SELF_MODULE &&
|
|
973
|
+
lw.to.type === bridge.type &&
|
|
974
|
+
lw.to.field === bridge.field) {
|
|
975
|
+
// Check if this targets an array element path
|
|
976
|
+
const toPathStr = lw.to.path.join(".");
|
|
977
|
+
if (toPathStr in arrayIterators)
|
|
978
|
+
continue;
|
|
979
|
+
// Check if any array iterator path is a prefix of this path
|
|
980
|
+
let isArrayElement = false;
|
|
981
|
+
for (const iterPath of Object.keys(arrayIterators)) {
|
|
982
|
+
if (iterPath === "" || toPathStr.startsWith(iterPath + ".")) {
|
|
983
|
+
isArrayElement = true;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (isArrayElement)
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const alias = lw.from.field;
|
|
991
|
+
const fromPart = lw.from.path.length > 0 ? alias + "." + serPath(lw.from.path) : alias;
|
|
992
|
+
const toStr = sRef(lw.to, false);
|
|
993
|
+
lines.push(`${toStr} <- ${fromPart}`);
|
|
994
|
+
}
|
|
995
|
+
// ── Pipe wires ───────────────────────────────────────────────────────
|
|
996
|
+
for (const [tk, outWire] of fromOutMap.entries()) {
|
|
997
|
+
if (pipeHandleTrunkKeys.has(refTrunkKey(outWire.to)))
|
|
998
|
+
continue;
|
|
999
|
+
// ── Expression chain detection ────────────────────────────────────
|
|
1000
|
+
// If the outermost fork is an expression fork, recursively reconstruct
|
|
1001
|
+
// the infix expression tree, respecting precedence grouping.
|
|
1002
|
+
if (exprForks.has(tk)) {
|
|
1003
|
+
// Element-targeting expressions are handled in serializeArrayElements
|
|
1004
|
+
if (outWire.to.element)
|
|
1005
|
+
continue;
|
|
1006
|
+
// Recursively serialize an expression fork into infix notation.
|
|
1007
|
+
function serializeExprTree(forkTk, parentPrec) {
|
|
1008
|
+
const info = exprForks.get(forkTk);
|
|
1009
|
+
if (!info)
|
|
1010
|
+
return null;
|
|
1011
|
+
// condAnd/condOr logic wire — reconstruct from leftRef/rightRef
|
|
1012
|
+
if (info.logicWire) {
|
|
1013
|
+
const logic = "condAnd" in info.logicWire
|
|
1014
|
+
? info.logicWire.condAnd
|
|
1015
|
+
: info.logicWire.condOr;
|
|
1016
|
+
let leftStr;
|
|
1017
|
+
const leftTk = refTrunkKey(logic.leftRef);
|
|
1018
|
+
if (logic.leftRef.path.length === 0 && exprForks.has(leftTk)) {
|
|
1019
|
+
leftStr =
|
|
1020
|
+
serializeExprTree(leftTk, OP_PREC_SER[info.op] ?? 0) ??
|
|
1021
|
+
sRef(logic.leftRef, true);
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
leftStr = logic.leftRef.element
|
|
1025
|
+
? "ITER." + serPath(logic.leftRef.path)
|
|
1026
|
+
: sRef(logic.leftRef, true);
|
|
1027
|
+
}
|
|
1028
|
+
let rightStr;
|
|
1029
|
+
if (logic.rightRef) {
|
|
1030
|
+
const rightTk = refTrunkKey(logic.rightRef);
|
|
1031
|
+
if (logic.rightRef.path.length === 0 && exprForks.has(rightTk)) {
|
|
1032
|
+
rightStr =
|
|
1033
|
+
serializeExprTree(rightTk, OP_PREC_SER[info.op] ?? 0) ??
|
|
1034
|
+
sRef(logic.rightRef, true);
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
rightStr = logic.rightRef.element
|
|
1038
|
+
? "ITER." + serPath(logic.rightRef.path)
|
|
1039
|
+
: sRef(logic.rightRef, true);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
else if (logic.rightValue != null) {
|
|
1043
|
+
rightStr = logic.rightValue;
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
rightStr = "0";
|
|
1047
|
+
}
|
|
1048
|
+
let result = `${leftStr} ${info.op} ${rightStr}`;
|
|
1049
|
+
const myPrec = OP_PREC_SER[info.op] ?? 0;
|
|
1050
|
+
if (parentPrec != null && myPrec < parentPrec)
|
|
1051
|
+
result = `(${result})`;
|
|
1052
|
+
return result;
|
|
1053
|
+
}
|
|
1054
|
+
// Serialize left operand (from .a wire)
|
|
1055
|
+
let leftStr = null;
|
|
1056
|
+
if (info.aWire) {
|
|
1057
|
+
const fromTk = refTrunkKey(info.aWire.from);
|
|
1058
|
+
if (info.aWire.from.path.length === 0 && exprForks.has(fromTk)) {
|
|
1059
|
+
leftStr = serializeExprTree(fromTk, OP_PREC_SER[info.op] ?? 0);
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
leftStr = info.aWire.from.element
|
|
1063
|
+
? "ITER." + serPath(info.aWire.from.path)
|
|
1064
|
+
: sRef(info.aWire.from, true);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
// Serialize right operand (from .b wire)
|
|
1068
|
+
let rightStr;
|
|
1069
|
+
if (info.bWire && "value" in info.bWire) {
|
|
1070
|
+
rightStr = info.bWire.value;
|
|
1071
|
+
}
|
|
1072
|
+
else if (info.bWire && "from" in info.bWire) {
|
|
1073
|
+
const bFrom = info.bWire.from;
|
|
1074
|
+
const bTk = refTrunkKey(bFrom);
|
|
1075
|
+
if (bFrom.path.length === 0 && exprForks.has(bTk)) {
|
|
1076
|
+
rightStr =
|
|
1077
|
+
serializeExprTree(bTk, OP_PREC_SER[info.op] ?? 0) ??
|
|
1078
|
+
sRef(bFrom, true);
|
|
1079
|
+
}
|
|
1080
|
+
else {
|
|
1081
|
+
rightStr = bFrom.element
|
|
1082
|
+
? "ITER." + serPath(bFrom.path)
|
|
1083
|
+
: sRef(bFrom, true);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
else {
|
|
1087
|
+
rightStr = "0";
|
|
1088
|
+
}
|
|
1089
|
+
if (leftStr == null)
|
|
1090
|
+
return rightStr;
|
|
1091
|
+
// Unary `not` — only has .a operand
|
|
1092
|
+
if (info.op === "not")
|
|
1093
|
+
return `not ${leftStr}`;
|
|
1094
|
+
let result = `${leftStr} ${info.op} ${rightStr}`;
|
|
1095
|
+
const myPrec = OP_PREC_SER[info.op] ?? 0;
|
|
1096
|
+
if (parentPrec != null && myPrec < parentPrec)
|
|
1097
|
+
result = `(${result})`;
|
|
1098
|
+
return result;
|
|
1099
|
+
}
|
|
1100
|
+
const exprStr = serializeExprTree(tk);
|
|
1101
|
+
if (exprStr) {
|
|
1102
|
+
const destStr = sRef(outWire.to, false);
|
|
1103
|
+
const ffr = outWire.falsyFallbackRefs
|
|
1104
|
+
?.map((r) => ` || ${sPipeOrRef(r)}`)
|
|
1105
|
+
.join("") ?? "";
|
|
1106
|
+
const nfb = ffr +
|
|
1107
|
+
("falsyControl" in outWire && outWire.falsyControl
|
|
1108
|
+
? ` || ${serializeControl(outWire.falsyControl)}`
|
|
1109
|
+
: outWire.falsyFallback
|
|
1110
|
+
? ` || ${outWire.falsyFallback}`
|
|
1111
|
+
: "");
|
|
1112
|
+
const nuf = "nullishControl" in outWire && outWire.nullishControl
|
|
1113
|
+
? ` ?? ${serializeControl(outWire.nullishControl)}`
|
|
1114
|
+
: outWire.nullishFallbackRef
|
|
1115
|
+
? ` ?? ${sPipeOrRef(outWire.nullishFallbackRef)}`
|
|
1116
|
+
: outWire.nullishFallback
|
|
1117
|
+
? ` ?? ${outWire.nullishFallback}`
|
|
1118
|
+
: "";
|
|
1119
|
+
const errf = "catchControl" in outWire && outWire.catchControl
|
|
1120
|
+
? ` catch ${serializeControl(outWire.catchControl)}`
|
|
1121
|
+
: outWire.catchFallbackRef
|
|
1122
|
+
? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}`
|
|
1123
|
+
: outWire.catchFallback
|
|
1124
|
+
? ` catch ${outWire.catchFallback}`
|
|
1125
|
+
: "";
|
|
1126
|
+
lines.push(`${destStr} <- ${exprStr}${nfb}${nuf}${errf}`);
|
|
1127
|
+
}
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
// ── Concat (template string) detection ───────────────────────────
|
|
1131
|
+
if (concatForks.has(tk)) {
|
|
1132
|
+
if (outWire.to.element)
|
|
1133
|
+
continue; // handled in serializeArrayElements
|
|
1134
|
+
const templateStr = reconstructTemplateString(tk);
|
|
1135
|
+
if (templateStr) {
|
|
1136
|
+
const destStr = sRef(outWire.to, false);
|
|
1137
|
+
const ffr = outWire.falsyFallbackRefs
|
|
1138
|
+
?.map((r) => ` || ${sPipeOrRef(r)}`)
|
|
1139
|
+
.join("") ?? "";
|
|
1140
|
+
const nfb = ffr +
|
|
1141
|
+
("falsyControl" in outWire && outWire.falsyControl
|
|
1142
|
+
? ` || ${serializeControl(outWire.falsyControl)}`
|
|
1143
|
+
: outWire.falsyFallback
|
|
1144
|
+
? ` || ${outWire.falsyFallback}`
|
|
1145
|
+
: "");
|
|
1146
|
+
const nuf = "nullishControl" in outWire && outWire.nullishControl
|
|
1147
|
+
? ` ?? ${serializeControl(outWire.nullishControl)}`
|
|
1148
|
+
: outWire.nullishFallbackRef
|
|
1149
|
+
? ` ?? ${sPipeOrRef(outWire.nullishFallbackRef)}`
|
|
1150
|
+
: outWire.nullishFallback
|
|
1151
|
+
? ` ?? ${outWire.nullishFallback}`
|
|
1152
|
+
: "";
|
|
1153
|
+
const errf = "catchControl" in outWire && outWire.catchControl
|
|
1154
|
+
? ` catch ${serializeControl(outWire.catchControl)}`
|
|
1155
|
+
: outWire.catchFallbackRef
|
|
1156
|
+
? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}`
|
|
1157
|
+
: outWire.catchFallback
|
|
1158
|
+
? ` catch ${outWire.catchFallback}`
|
|
1159
|
+
: "";
|
|
1160
|
+
lines.push(`${destStr} <- ${templateStr}${nfb}${nuf}${errf}`);
|
|
1161
|
+
}
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
// ── Normal pipe chain ─────────────────────────────────────────────
|
|
1165
|
+
const handleChain = [];
|
|
1166
|
+
let currentTk = tk;
|
|
1167
|
+
let actualSourceRef = null;
|
|
1168
|
+
for (;;) {
|
|
1169
|
+
const handleName = handleMap.get(currentTk);
|
|
1170
|
+
if (!handleName)
|
|
1171
|
+
break;
|
|
1172
|
+
const inWire = toInMap.get(currentTk);
|
|
1173
|
+
const fieldName = inWire?.to.path[0] ?? "in";
|
|
1174
|
+
const token = fieldName === "in" ? handleName : `${handleName}.${fieldName}`;
|
|
1175
|
+
handleChain.push(token);
|
|
1176
|
+
if (!inWire)
|
|
1177
|
+
break;
|
|
1178
|
+
const fromTk = refTrunkKey(inWire.from);
|
|
1179
|
+
if (inWire.from.path.length === 0 && pipeHandleTrunkKeys.has(fromTk)) {
|
|
1180
|
+
currentTk = fromTk;
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
actualSourceRef = inWire.from;
|
|
1184
|
+
break;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
if (actualSourceRef && handleChain.length > 0) {
|
|
1188
|
+
const sourceStr = sRef(actualSourceRef, true);
|
|
1189
|
+
const destStr = sRef(outWire.to, false);
|
|
1190
|
+
const ffr = outWire.falsyFallbackRefs
|
|
1191
|
+
?.map((r) => ` || ${sPipeOrRef(r)}`)
|
|
1192
|
+
.join("") ?? "";
|
|
1193
|
+
const nfb = ffr +
|
|
1194
|
+
("falsyControl" in outWire && outWire.falsyControl
|
|
1195
|
+
? ` || ${serializeControl(outWire.falsyControl)}`
|
|
1196
|
+
: outWire.falsyFallback
|
|
1197
|
+
? ` || ${outWire.falsyFallback}`
|
|
1198
|
+
: "");
|
|
1199
|
+
const nuf = "nullishControl" in outWire && outWire.nullishControl
|
|
1200
|
+
? ` ?? ${serializeControl(outWire.nullishControl)}`
|
|
1201
|
+
: outWire.nullishFallbackRef
|
|
1202
|
+
? ` ?? ${sPipeOrRef(outWire.nullishFallbackRef)}`
|
|
1203
|
+
: outWire.nullishFallback
|
|
1204
|
+
? ` ?? ${outWire.nullishFallback}`
|
|
1205
|
+
: "";
|
|
1206
|
+
const errf = "catchControl" in outWire && outWire.catchControl
|
|
1207
|
+
? ` catch ${serializeControl(outWire.catchControl)}`
|
|
1208
|
+
: outWire.catchFallbackRef
|
|
1209
|
+
? ` catch ${sPipeOrRef(outWire.catchFallbackRef)}`
|
|
1210
|
+
: outWire.catchFallback
|
|
1211
|
+
? ` catch ${outWire.catchFallback}`
|
|
1212
|
+
: "";
|
|
1213
|
+
lines.push(`${destStr} <- ${handleChain.join(":")}:${sourceStr}${nfb}${nuf}${errf}`);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// Force statements
|
|
1217
|
+
if (bridge.forces) {
|
|
1218
|
+
for (const f of bridge.forces) {
|
|
1219
|
+
lines.push(f.catchError ? `force ${f.handle} catch null` : `force ${f.handle}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
// Indent wire body lines and close the block
|
|
1223
|
+
for (let i = wireBodyStart; i < lines.length; i++) {
|
|
1224
|
+
if (lines[i] !== "")
|
|
1225
|
+
lines[i] = ` ${lines[i]}`;
|
|
1226
|
+
}
|
|
1227
|
+
lines.push(`}`);
|
|
1228
|
+
return lines.join("\n");
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Recomputes instance numbers from handle bindings in declaration order.
|
|
1232
|
+
*/
|
|
1233
|
+
function buildHandleMap(bridge) {
|
|
1234
|
+
const handleMap = new Map();
|
|
1235
|
+
const instanceCounters = new Map();
|
|
1236
|
+
let inputHandle;
|
|
1237
|
+
let outputHandle;
|
|
1238
|
+
for (const h of bridge.handles) {
|
|
1239
|
+
switch (h.kind) {
|
|
1240
|
+
case "tool": {
|
|
1241
|
+
const lastDot = h.name.lastIndexOf(".");
|
|
1242
|
+
if (lastDot !== -1) {
|
|
1243
|
+
// Dotted name: module.field
|
|
1244
|
+
const modulePart = h.name.substring(0, lastDot);
|
|
1245
|
+
const fieldPart = h.name.substring(lastDot + 1);
|
|
1246
|
+
const ik = `${modulePart}:${fieldPart}`;
|
|
1247
|
+
const instance = (instanceCounters.get(ik) ?? 0) + 1;
|
|
1248
|
+
instanceCounters.set(ik, instance);
|
|
1249
|
+
handleMap.set(`${modulePart}:${bridge.type}:${fieldPart}:${instance}`, h.handle);
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
// Simple name: inline tool
|
|
1253
|
+
const ik = `Tools:${h.name}`;
|
|
1254
|
+
const instance = (instanceCounters.get(ik) ?? 0) + 1;
|
|
1255
|
+
instanceCounters.set(ik, instance);
|
|
1256
|
+
handleMap.set(`${SELF_MODULE}:Tools:${h.name}:${instance}`, h.handle);
|
|
1257
|
+
}
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
case "input":
|
|
1261
|
+
inputHandle = h.handle;
|
|
1262
|
+
break;
|
|
1263
|
+
case "output":
|
|
1264
|
+
outputHandle = h.handle;
|
|
1265
|
+
break;
|
|
1266
|
+
case "context":
|
|
1267
|
+
handleMap.set(`${SELF_MODULE}:Context:context`, h.handle);
|
|
1268
|
+
break;
|
|
1269
|
+
case "const":
|
|
1270
|
+
handleMap.set(`${SELF_MODULE}:Const:const`, h.handle);
|
|
1271
|
+
break;
|
|
1272
|
+
case "define":
|
|
1273
|
+
handleMap.set(`__define_${h.handle}:${bridge.type}:${bridge.field}`, h.handle);
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
return { handleMap, inputHandle, outputHandle };
|
|
1278
|
+
}
|
|
1279
|
+
function serializeRef(ref, bridge, handleMap, inputHandle, outputHandle, isFrom) {
|
|
1280
|
+
if (ref.element) {
|
|
1281
|
+
// Element refs are only serialized inside brace blocks (using the iterator name).
|
|
1282
|
+
// This path should not be reached in normal serialization.
|
|
1283
|
+
return "item." + serPath(ref.path);
|
|
1284
|
+
}
|
|
1285
|
+
// Bridge's own trunk (no instance, no element)
|
|
1286
|
+
const isBridgeTrunk = ref.module === SELF_MODULE &&
|
|
1287
|
+
ref.type === bridge.type &&
|
|
1288
|
+
ref.field === bridge.field &&
|
|
1289
|
+
!ref.instance &&
|
|
1290
|
+
!ref.element;
|
|
1291
|
+
if (isBridgeTrunk) {
|
|
1292
|
+
if (isFrom && inputHandle) {
|
|
1293
|
+
// From side: use input handle (data comes from args)
|
|
1294
|
+
return ref.path.length > 0
|
|
1295
|
+
? inputHandle + "." + serPath(ref.path)
|
|
1296
|
+
: inputHandle;
|
|
1297
|
+
}
|
|
1298
|
+
if (!isFrom && outputHandle) {
|
|
1299
|
+
// To side: use output handle
|
|
1300
|
+
return ref.path.length > 0
|
|
1301
|
+
? outputHandle + "." + serPath(ref.path)
|
|
1302
|
+
: outputHandle;
|
|
1303
|
+
}
|
|
1304
|
+
// Fallback (no handle declared — legacy/serializer-only path)
|
|
1305
|
+
return serPath(ref.path);
|
|
1306
|
+
}
|
|
1307
|
+
// Lookup by trunk key
|
|
1308
|
+
const trunkStr = ref.instance != null
|
|
1309
|
+
? `${ref.module}:${ref.type}:${ref.field}:${ref.instance}`
|
|
1310
|
+
: `${ref.module}:${ref.type}:${ref.field}`;
|
|
1311
|
+
const handle = handleMap.get(trunkStr);
|
|
1312
|
+
if (handle) {
|
|
1313
|
+
if (ref.path.length === 0)
|
|
1314
|
+
return handle;
|
|
1315
|
+
return handle + "." + serPath(ref.path);
|
|
1316
|
+
}
|
|
1317
|
+
// Fallback: bare path
|
|
1318
|
+
return serPath(ref.path);
|
|
1319
|
+
}
|
|
1320
|
+
/** Serialize a path array to dot notation with [n] for numeric indices */
|
|
1321
|
+
function serPath(path) {
|
|
1322
|
+
let result = "";
|
|
1323
|
+
for (const segment of path) {
|
|
1324
|
+
if (/^\d+$/.test(segment)) {
|
|
1325
|
+
result += `[${segment}]`;
|
|
1326
|
+
}
|
|
1327
|
+
else {
|
|
1328
|
+
if (result.length > 0)
|
|
1329
|
+
result += ".";
|
|
1330
|
+
result += segment;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return result;
|
|
1334
|
+
}
|