@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.
@@ -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
+ }